Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: export command supported formats #118

Merged
merged 2 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
refactor: export command supported formats
Signed-off-by: Jonathan Howard <[email protected]>
  • Loading branch information
jhoward-lm committed Aug 5, 2024
commit fe69830cb54e1598fb17efa2b9a2210f650b43b5
197 changes: 150 additions & 47 deletions cmd/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,52 19,61 @@
package cmd

import (
"errors"
"fmt"
"os"
"path/filepath"

"regexp"
"slices"
"strings"

"github.com/protobom/protobom/pkg/formats"
"github.com/protobom/protobom/pkg/native"
"github.com/protobom/protobom/pkg/native/serializers"
"github.com/protobom/protobom/pkg/writer"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/bomctl/bomctl/internal/pkg/db"
"github.com/bomctl/bomctl/internal/pkg/export"
"github.com/bomctl/bomctl/internal/pkg/options"
"github.com/bomctl/bomctl/internal/pkg/utils"
"github.com/bomctl/bomctl/internal/pkg/utils/format"
)

var (
errUnknownEncoding = errors.New("unknown encoding")
errUnknownFormat = errors.New("unknown format")
)

func exportCmd() *cobra.Command {
documentIDs := []string{}
opts := &export.Options{}
opts := &export.Options{
Options: options.New(options.WithLogger(utils.NewLogger("export"))),
}

outputFile := outputFileValue("")
formatString := formatStringValue(format.DefaultFormatString())
formatEncoding := formatEncodingValue(format.DefaultEncoding())

exportCmd := &cobra.Command{
Use: "export [flags] SBOM_URL...",
Args: cobra.MinimumNArgs(1),
Short: "Export SBOM file(s) from Storage",
Long: "Export SBOM file(s) from Storage",
PreRun: func(_ *cobra.Command, args []string) {
documentIDs = append(documentIDs, args...)
},
Run: func(cmd *cobra.Command, _ []string) {
cfgFile, err := cmd.Flags().GetString("config")
Use: "export [flags] SBOM_ID...",
Args: cobra.MinimumNArgs(1),
Short: "Export stored SBOM(s) to filesystem",
Long: "Export stored SBOM(s) to filesystem",
PreRun: preRun(opts.Options),
Run: func(cmd *cobra.Command, args []string) {
formatString, err := cmd.Flags().GetString("format")
cobra.CheckErr(err)

verbosity, err := cmd.Flags().GetCount("verbose")
encoding, err := cmd.Flags().GetString("encoding")
cobra.CheckErr(err)

opts.Debug = verbosity >= minDebugLevel
format, err := parseFormat(formatString, encoding)
cobra.CheckErr(err)

initOpts(opts, cfgFile, string(formatString), string(formatEncoding))
backend := initBackend(opts)
opts.Format = format

if string(outputFile) != "" {
if len(documentIDs) > 1 {
opts.Logger.Fatal("The --output-file option cannot be used when more than one SBOM is provided.")
if outputFile != "" {
if len(args) > 1 {
opts.Logger.Fatal("The --output-file option cannot be used when more than one SBOM is provided.")
}

out, err := os.Create(string(outputFile))
out, err := os.Create(outputFile.String())
if err != nil {
opts.Logger.Fatal("error creating output file", "outputFile", outputFile)
}
Expand All @@ -73,42 82,136 @@ func exportCmd() *cobra.Command {

defer opts.OutputFile.Close()
}
Export(documentIDs, opts, backend)

for _, id := range args {
if err := export.Export(id, opts); err != nil {
opts.Logger.Fatal(err)
}
}
},
}

exportCmd.Flags().VarP(&outputFile, "output-file", "o", "Path to output file")
exportCmd.Flags().VarP(&formatString, "format", "f", format.FormatStringOptions)
exportCmd.Flags().VarP(&formatEncoding, "encoding", "e", "Output encoding [spdx: [text, json] cyclonedx: [json]")
exportCmd.Flags().VarP(&outputFile, "output-file", "o", "path to output file")
exportCmd.Flags().StringP("format", "f", formats.CDXFORMAT, formatHelp())
exportCmd.Flags().StringP("encoding", "e", formats.JSON, encodingHelp())

return exportCmd
}

func Export(documentIDs []string, opts *export.Options, backend *db.Backend) {
for _, id := range documentIDs {
if err := export.Export(id, opts, backend); err != nil {
opts.Logger.Fatal(err)
func encodingHelp() string {
return fmt.Sprintf("output encoding [%s: [%s], %s: [%s]]",
formats.SPDXFORMAT, strings.Join(encodingOptionsSPDX(), ", "),
formats.CDXFORMAT, strings.Join(encodingOptionsCycloneDX(), ", "),
)
}

func encodingOptions() []string {
return []string{formats.JSON, formats.TEXT, formats.XML}
}

func encodingOptionsCycloneDX() []string {
return []string{formats.JSON, formats.XML}
}

func encodingOptionsSPDX() []string {
return []string{formats.JSON, formats.TEXT}
}

func formatHelp() string {
return fmt.Sprintf("output format [%s]", strings.Join(formatOptions(), ", "))
}

func formatOptions() []string {
spdxFormats := []string{
formats.SPDXFORMAT,
formats.SPDXFORMAT "-2.3",
}

cdxFormats := []string{
formats.CDXFORMAT,
formats.CDXFORMAT "-1.0",
formats.CDXFORMAT "-1.1",
formats.CDXFORMAT "-1.2",
formats.CDXFORMAT "-1.3",
formats.CDXFORMAT "-1.4",
formats.CDXFORMAT "-1.5",
}

return append(spdxFormats, cdxFormats...)
}

func parseFormat(fs, encoding string) (formats.Format, error) {
ashearin marked this conversation as resolved.
Show resolved Hide resolved
if err := validateEncoding(encoding); err != nil {
return formats.EmptyFormat, err
}

results := map[string]string{}
pattern := regexp.MustCompile("^(?P<name>[^-] )(?:-(?P<version>.*))?")
match := pattern.FindStringSubmatch(fs)

for idx, name := range match {
results[pattern.SubexpNames()[idx]] = name
}

baseFormat := results["name"]
version := results["version"]

if err := validateFormat(baseFormat); err != nil {
return formats.EmptyFormat, err
}

var serializer native.Serializer

switch baseFormat {
case formats.CDXFORMAT:
if version == "" {
version = "1.5"
}

baseFormat = "application/vnd.cyclonedx"
serializer = serializers.NewCDX(version, encoding)
case formats.SPDXFORMAT:
if version == "" {
version = "2.3"
}

baseFormat = "text/spdx"
serializer = serializers.NewSPDX23()
}

format := formats.Format(fmt.Sprintf("%s %s;version=%s", baseFormat, encoding, version))
writer.RegisterSerializer(format, serializer)

return format, nil
}

func initOpts(opts *export.Options, cfgFile, formatString, formatEncoding string) {
opts.CacheDir = viper.GetString("cache_dir")
opts.ConfigFile = cfgFile
opts.FormatString = formatString
opts.Encoding = formatEncoding
func preRun(opts *options.Options) func(*cobra.Command, []string) {
return func(cmd *cobra.Command, _ []string) {
cfgFile, err := cmd.Flags().GetString("config")
cobra.CheckErr(err)

verbosity, err := cmd.Flags().GetCount("verbose")
cobra.CheckErr(err)

opts.
WithCacheDir(viper.GetString("cache_dir")).
WithConfigFile(cfgFile).
WithDebug(verbosity >= minDebugLevel)
}
}

func initBackend(opts *export.Options) *db.Backend {
backend := db.NewBackend(func(b *db.Backend) {
b.Options.DatabaseFile = filepath.Join(opts.CacheDir, db.DatabaseFile)
b.Options.Debug = opts.Debug
b.Logger = utils.NewLogger("export")
})
func validateEncoding(encoding string) error {
if !slices.Contains(encodingOptions(), encoding) {
return fmt.Errorf("%w: %s", errUnknownEncoding, encoding)
}

return nil
}

if err := backend.InitClient(); err != nil {
backend.Logger.Fatalf("failed to initialize backend client: %v", err)
func validateFormat(format string) error {
if !slices.Contains(formatOptions(), format) {
return fmt.Errorf("%w: %s", errUnknownFormat, format)
}

return backend
return nil
}
20 changes: 0 additions & 20 deletions cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 32,6 @@ type (
directoryValue string
existingFileValue string
outputFileValue string
formatStringValue string
formatEncodingValue string
urlValue string
directorySliceValue []string
fileSliceValue []string
Expand Down Expand Up @@ -66,8 64,6 @@ func (dsv *directorySliceValue) String() string { return fmt.Sprintf("%v", *dsv)
func (efv *existingFileValue) String() string { return fmt.Sprintf("%v", *efv) }
func (fsv *fileSliceValue) String() string { return fmt.Sprintf("%v", *fsv) }
func (ofv *outputFileValue) String() string { return fmt.Sprintf("%v", *ofv) }
func (fstv *formatStringValue) String() string { return fmt.Sprintf("%v", *fstv) }
func (fev *formatEncodingValue) String() string { return fmt.Sprintf("%v", *fev) }

func (uv *urlValue) String() string { return fmt.Sprintf("%v", *uv) }
func (usv *urlSliceValue) String() string { return fmt.Sprintf("%v", *usv) }
Expand Down Expand Up @@ -106,18 102,6 @@ func (ofv *outputFileValue) Set(value string) error {
return nil
}

func (fstv *formatStringValue) Set(value string) error {
*fstv = formatStringValue(value)

return nil
}

func (fev *formatEncodingValue) Set(value string) error {
*fev = formatEncodingValue(value)

return nil
}

func (uv *urlValue) Set(value string) error {
*uv = urlValue(value)

Expand All @@ -142,8 126,6 @@ func (*directorySliceValue) Type() string { return valueTypeDir }
func (*existingFileValue) Type() string { return valueTypeFile }
func (*fileSliceValue) Type() string { return valueTypeFile }
func (*outputFileValue) Type() string { return valueTypeFile }
func (*formatStringValue) Type() string { return valueTypeString }
func (*formatEncodingValue) Type() string { return valueTypeString }
func (*urlValue) Type() string { return valueTypeURL }
func (*urlSliceValue) Type() string { return valueTypeURL }

Expand All @@ -153,8 135,6 @@ var (
_ pflag.Value = (*existingFileValue)(nil)
_ pflag.Value = (*fileSliceValue)(nil)
_ pflag.Value = (*outputFileValue)(nil)
_ pflag.Value = (*formatStringValue)(nil)
_ pflag.Value = (*formatEncodingValue)(nil)
_ pflag.Value = (*urlValue)(nil)
_ pflag.Value = (*urlSliceValue)(nil)
)
35 changes: 18 additions & 17 deletions internal/pkg/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 21,36 @@ package export
import (
"fmt"
"os"
"path/filepath"

"github.com/charmbracelet/log"
"github.com/protobom/protobom/pkg/formats"
"github.com/protobom/protobom/pkg/writer"

"github.com/bomctl/bomctl/internal/pkg/db"
"github.com/bomctl/bomctl/internal/pkg/utils/format"
"github.com/bomctl/bomctl/internal/pkg/options"
)

type Options struct {
Logger *log.Logger
OutputFile *os.File
FormatString string
Encoding string
CacheDir string
ConfigFile string
Debug bool
*options.Options
OutputFile *os.File
Format formats.Format
}

func Export(sbomID string, opts *Options, backend *db.Backend) error {
backend.Logger.Info("Exporting Document", "sbomID", sbomID)
func Export(sbomID string, opts *Options) error {
opts.Logger.Info("Exporting Document", "sbomID", sbomID)

parsedFormat, err := format.Parse(opts.FormatString, opts.Encoding)
if err != nil {
return fmt.Errorf("%w", err)
backend := db.NewBackend().
Debug(opts.Debug).
WithDatabaseFile(filepath.Join(opts.CacheDir, db.DatabaseFile)).
WithLogger(opts.Logger)

if err := backend.InitClient(); err != nil {
return fmt.Errorf("failed to initialize backend client: %w", err)
}

wr := writer.New(
writer.WithFormat(parsedFormat),
)
defer backend.CloseClient()

wr := writer.New(writer.WithFormat(opts.Format))

document, err := backend.GetDocumentByID(sbomID)
if err != nil {
Expand Down
Loading