diff --git a/internal/cmd/discoverworkload/discoverworkload.go b/internal/cmd/discoverworkload/discoverworkload.go index c13f67b..8eb75ee 100644 --- a/internal/cmd/discoverworkload/discoverworkload.go +++ b/internal/cmd/discoverworkload/discoverworkload.go @@ -33,6 +33,7 @@ type config struct { KubeconfigPath string LabelSelector string FieldSelector string + CompactOutput bool } func NewCommand(ctx context.Context) *cobra.Command { @@ -73,6 +74,10 @@ func NewCommand(ctx context.Context) *cobra.Command { go discover.StartNotifier(ctx, logger, 15*time.Second, 30*time.Second) logger.Info("starting to watch for workloads", "duration", cfg.Timeout) + + opts := discover.NewManifestJSONProcessorFnOptions{ + CompactOutput: cfg.CompactOutput, + } err = discover.WatchForWorkloads( ctx, logger, @@ -82,7 +87,7 @@ func NewCommand(ctx context.Context) *cobra.Command { FieldSelector: cfg.FieldSelector, }, k8sclient, - discover.NewManifestJSONProcessorFn(os.Stdout), + discover.NewManifestJSONProcessorFn(os.Stdout, opts), ) if err != nil { switch { @@ -108,6 +113,7 @@ func NewCommand(ctx context.Context) *cobra.Command { flags.StringVarP(&cfg.KubeconfigPath, "kubeconfig", "k", clientcmd.RecommendedHomeFile, "The kubeconfig to use for cluster access.") flags.StringVarP(&cfg.LabelSelector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") flags.StringVar(&cfg.FieldSelector, "field-selector", "", "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.") + flags.BoolVarP(&cfg.CompactOutput, "compact", "c", false, "Print JSON in compact format instead of pretty-printed output") return c } diff --git a/internal/discover/processor.go b/internal/discover/processor.go index 7dbcc42..2d94fc1 100644 --- a/internal/discover/processor.go +++ b/internal/discover/processor.go @@ -13,10 +13,14 @@ import ( "github.com/opdev/discover-workload/discovery" ) +type NewManifestJSONProcessorFnOptions struct { + CompactOutput bool +} + // NewManifestJSONProcessorFn produces a ProcessingFunction that will write a // Manifest in JSON to out. This Processor finds all images from containers, // initContainers, and ephemeralContainers. -func NewManifestJSONProcessorFn(out io.Writer) ProcessingFunction { +func NewManifestJSONProcessorFn(out io.Writer, opts NewManifestJSONProcessorFnOptions) ProcessingFunction { return func(ctx context.Context, source <-chan *corev1.Pod, logger *slog.Logger) error { m := discovery.Manifest{} @@ -41,7 +45,14 @@ func NewManifestJSONProcessorFn(out io.Writer) ProcessingFunction { return nil } - manifestJSON, err := json.Marshal(m) + var manifestJSON []byte + var err error + if opts.CompactOutput { + manifestJSON, err = json.Marshal(m) + } else { + manifestJSON, err = json.MarshalIndent(m, "", " ") + } + if err != nil { logger.Error("unable to convert output manifest to JSON", "errMsg", err) return err diff --git a/internal/discover/processor_test.go b/internal/discover/processor_test.go index 3389fcb..d2122d7 100644 --- a/internal/discover/processor_test.go +++ b/internal/discover/processor_test.go @@ -13,7 +13,7 @@ import ( "github.com/opdev/discover-workload/discovery" ) -func TestManifestInsert(t *testing.T) { +func TestManifestAppend(t *testing.T) { t.Parallel() testcases := map[string]struct { ctx context.Context @@ -391,6 +391,7 @@ func TestManifestJSONProcessor(t *testing.T) { testcases := map[string]struct { ctx context.Context input []corev1.Pod + compact bool expected []byte }{ "initContainer only": { @@ -408,7 +409,26 @@ func TestManifestJSONProcessor(t *testing.T) { }, }, }, - expected: []byte("{\"DiscoveredImages\":[{\"Image\":\"example.com/namespace/image:0.0.1\",\"Containers\":[{\"Name\":\"init-cname\",\"Type\":\"InitContainer\",\"Pod\":{\"Name\":\"init-podname\",\"Namespace\":\"\"}}]}]}\n"), + compact: false, + expected: []byte("{\n \"DiscoveredImages\": [\n {\n \"Image\": \"example.com/namespace/image:0.0.1\",\n \"Containers\": [\n {\n \"Name\": \"init-cname\",\n \"Type\": \"InitContainer\",\n \"Pod\": {\n \"Name\": \"init-podname\",\n \"Namespace\": \"\"\n }\n }\n ]\n }\n ]\n}\n"), + }, + "with raw printed JSON": { + ctx: context.TODO(), + input: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "podname"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "cname", + Image: "example.com/namespace/image:0.0.1", + }, + }, + }, + }, + }, + compact: true, + expected: []byte("{\"DiscoveredImages\":[{\"Image\":\"example.com/namespace/image:0.0.1\",\"Containers\":[{\"Name\":\"cname\",\"Type\":\"Container\",\"Pod\":{\"Name\":\"podname\",\"Namespace\":\"\"}}]}]}\n"), }, } @@ -417,7 +437,10 @@ func TestManifestJSONProcessor(t *testing.T) { t.Run(description, func(t *testing.T) { t.Parallel() buffer := bytes.NewBuffer([]byte{}) - fn := NewManifestJSONProcessorFn(buffer) + opts := NewManifestJSONProcessorFnOptions{ + CompactOutput: tc.compact, + } + fn := NewManifestJSONProcessorFn(buffer, opts) ch := make(chan *corev1.Pod)