-
Couldn't load subscription status.
- Fork 727
Add support for multiple output files in different formats #732
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
Changes from all commits
5c0b8e8
20acbb2
6572d2c
86aeb59
98f12cf
5a6b757
f6a649a
771eac3
44be52b
c526bae
8f83297
6ab05a3
81a923f
69e00e2
69c7cf1
b719dd6
c11f4f9
0e191cf
6c0dce1
fa3beef
d2b80bf
da7f39d
49b227a
b77595f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/anchore/syft/internal/formats" | ||
| "github.com/anchore/syft/internal/output" | ||
| "github.com/anchore/syft/syft/format" | ||
| "github.com/anchore/syft/syft/sbom" | ||
| "github.com/hashicorp/go-multierror" | ||
| ) | ||
|
|
||
| // makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer | ||
| // or an error but neither both and if there is no error, sbom.Writer.Close() should be called | ||
| func makeWriter(outputs []string, defaultFile string) (sbom.Writer, error) { | ||
| outputOptions, err := parseOptions(outputs, defaultFile) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| writer, err := output.MakeWriter(outputOptions...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return writer, nil | ||
| } | ||
|
|
||
| // parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file | ||
| func parseOptions(outputs []string, defaultFile string) (out []output.WriterOption, errs error) { | ||
| // always should have one option -- we generally get the default of "table", but just make sure | ||
| if len(outputs) == 0 { | ||
| outputs = append(outputs, string(format.TableOption)) | ||
| } | ||
|
|
||
| for _, name := range outputs { | ||
| name = strings.TrimSpace(name) | ||
|
|
||
| // split to at most two parts for <format>=<file> | ||
| parts := strings.SplitN(name, "=", 2) | ||
|
|
||
| // the format option is the first part | ||
| name = parts[0] | ||
|
|
||
| // default to the --file or empty string if not specified | ||
| file := defaultFile | ||
|
|
||
| // If a file is specified as part of the output option, use that | ||
| if len(parts) > 1 { | ||
| file = parts[1] | ||
| } | ||
|
|
||
| option := format.ParseOption(name) | ||
| if option == format.UnknownFormatOption { | ||
| errs = multierror.Append(errs, fmt.Errorf("bad output format: '%s'", name)) | ||
| continue | ||
| } | ||
|
|
||
| encoder := formats.ByOption(option) | ||
| if encoder == nil { | ||
| errs = multierror.Append(errs, fmt.Errorf("unknown format: %s", outputFormat)) | ||
| continue | ||
| } | ||
|
|
||
| out = append(out, output.WriterOption{ | ||
| Format: *encoder, | ||
| Path: file, | ||
| }) | ||
| } | ||
| return out, errs | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestOutputWriterConfig(t *testing.T) { | ||
| tmp := t.TempDir() + "/" | ||
|
|
||
| tests := []struct { | ||
| outputs []string | ||
| file string | ||
| err bool | ||
| expected []string | ||
| }{ | ||
| { | ||
| outputs: []string{}, | ||
| expected: []string{""}, | ||
| }, | ||
| { | ||
| outputs: []string{"json"}, | ||
| expected: []string{""}, | ||
| }, | ||
| { | ||
| file: "test-1.json", | ||
| expected: []string{"test-1.json"}, | ||
| }, | ||
| { | ||
| outputs: []string{"json=test-2.json"}, | ||
| expected: []string{"test-2.json"}, | ||
| }, | ||
| { | ||
| outputs: []string{"json=test-3-1.json", "spdx-json=test-3-2.json"}, | ||
| expected: []string{"test-3-1.json", "test-3-2.json"}, | ||
| }, | ||
| { | ||
| outputs: []string{"text", "json=test-4.json"}, | ||
| expected: []string{"", "test-4.json"}, | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(fmt.Sprintf("%s/%s", test.outputs, test.file), func(t *testing.T) { | ||
| outputs := test.outputs | ||
| for i, val := range outputs { | ||
| outputs[i] = strings.Replace(val, "=", "="+tmp, 1) | ||
| } | ||
|
|
||
| file := test.file | ||
| if file != "" { | ||
| file = tmp + file | ||
| } | ||
|
|
||
| _, err := makeWriter(test.outputs, file) | ||
|
|
||
| if test.err { | ||
| assert.Error(t, err) | ||
| return | ||
| } else { | ||
| assert.NoError(t, err) | ||
| } | ||
|
|
||
| for _, expected := range test.expected { | ||
kzantow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if expected != "" { | ||
| assert.FileExists(t, tmp+expected) | ||
| } else if file != "" { | ||
| assert.FileExists(t, file) | ||
| } else { | ||
| assert.NoFileExists(t, expected) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,6 @@ import ( | |
| "github.com/anchore/syft/internal" | ||
| "github.com/anchore/syft/internal/anchore" | ||
| "github.com/anchore/syft/internal/bus" | ||
| "github.com/anchore/syft/internal/formats" | ||
| "github.com/anchore/syft/internal/log" | ||
| "github.com/anchore/syft/internal/ui" | ||
| "github.com/anchore/syft/internal/version" | ||
|
|
@@ -51,8 +50,7 @@ const ( | |
| ) | ||
|
|
||
| var ( | ||
| packagesPresenterOpt format.Option | ||
| packagesCmd = &cobra.Command{ | ||
| packagesCmd = &cobra.Command{ | ||
| Use: "packages [SOURCE]", | ||
| Short: "Generate a package SBOM", | ||
| Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems", | ||
|
|
@@ -63,14 +61,7 @@ var ( | |
| Args: validateInputArgs, | ||
| SilenceUsage: true, | ||
| SilenceErrors: true, | ||
| PreRunE: func(cmd *cobra.Command, args []string) error { | ||
| // set the presenter | ||
| presenterOption := format.ParseOption(appConfig.Output) | ||
| if presenterOption == format.UnknownFormatOption { | ||
| return fmt.Errorf("bad --output value '%s'", appConfig.Output) | ||
| } | ||
| packagesPresenterOpt = presenterOption | ||
|
|
||
| PreRunE: func(cmd *cobra.Command, args []string) (err error) { | ||
| if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem { | ||
| return fmt.Errorf("cannot profile CPU and memory simultaneously") | ||
| } | ||
|
|
@@ -102,14 +93,14 @@ func setPackageFlags(flags *pflag.FlagSet) { | |
| "scope", "s", cataloger.DefaultSearchConfig().Scope.String(), | ||
| fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes)) | ||
|
|
||
| flags.StringP( | ||
| "output", "o", string(format.TableOption), | ||
| fmt.Sprintf("report output formatter, options=%v", format.AllOptions), | ||
| flags.StringArrayP( | ||
| "output", "o", []string{string(format.TableOption)}, | ||
| fmt.Sprintf("report output format, options=%v", format.AllOptions), | ||
| ) | ||
|
|
||
| flags.StringP( | ||
| "file", "", "", | ||
| "file to write the report output to (default is STDOUT)", | ||
| "file to write the default report output to (default is STDOUT)", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be deprecated? if so, how would that be done? just updates to this and the readme? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really know, but the functionality is redundant. it would seem outputting a table to a file is fairly useless, so generally a user would have to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see your point on redundancy. My take: I think it makes sense to leave the |
||
| ) | ||
|
|
||
| // Upload options ////////////////////////////////////////////////////////// | ||
|
|
@@ -210,26 +201,26 @@ func validateInputArgs(cmd *cobra.Command, args []string) error { | |
| } | ||
|
|
||
| func packagesExec(_ *cobra.Command, args []string) error { | ||
| // could be an image or a directory, with or without a scheme | ||
| userInput := args[0] | ||
| writer, err := makeWriter(appConfig.Output, appConfig.File) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| reporter, closer, err := reportWriter() | ||
| defer func() { | ||
| if err := closer(); err != nil { | ||
| log.Warnf("unable to write to report destination: %+v", err) | ||
| if err := writer.Close(); err != nil { | ||
| log.Warnf("unable to write to report destination: %w", err) | ||
| } | ||
| }() | ||
|
|
||
| if err != nil { | ||
| return err | ||
| } | ||
| // could be an image or a directory, with or without a scheme | ||
| userInput := args[0] | ||
|
|
||
| return eventLoop( | ||
| packagesExecWorker(userInput), | ||
| packagesExecWorker(userInput, writer), | ||
| setupSignals(), | ||
| eventSubscription, | ||
| stereoscope.Cleanup, | ||
| ui.Select(isVerbose(), appConfig.Quiet, reporter)..., | ||
| ui.Select(isVerbose(), appConfig.Quiet)..., | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -244,7 +235,7 @@ func isVerbose() (result bool) { | |
| return appConfig.CliOptions.Verbosity > 0 || isPipedInput | ||
| } | ||
|
|
||
| func packagesExecWorker(userInput string) <-chan error { | ||
| func packagesExecWorker(userInput string, writer sbom.Writer) <-chan error { | ||
| errs := make(chan error) | ||
| go func() { | ||
| defer close(errs) | ||
|
|
@@ -255,12 +246,6 @@ func packagesExecWorker(userInput string) <-chan error { | |
| return | ||
| } | ||
|
|
||
| f := formats.ByOption(packagesPresenterOpt) | ||
| if f == nil { | ||
| errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt) | ||
| return | ||
| } | ||
|
|
||
| src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions(), appConfig.Exclusions) | ||
| if err != nil { | ||
| errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) | ||
|
|
@@ -296,8 +281,8 @@ func packagesExecWorker(userInput string) <-chan error { | |
| } | ||
|
|
||
| bus.Publish(partybus.Event{ | ||
| Type: event.PresenterReady, | ||
| Value: f.Presenter(s), | ||
| Type: event.Exit, | ||
| Value: func() error { return writer.Write(s) }, | ||
| }) | ||
| }() | ||
| return errs | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.