diff --git a/cmd/check.go b/cmd/check.go index e5d4fa006..1848016c2 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -1,15 +1,11 @@ package cmd import ( - "bytes" "context" - "encoding/json" "fmt" - "golang.org/x/exp/maps" "io" "os" "strings" - "sync" "time" "github.com/spf13/cobra" @@ -18,7 +14,6 @@ import ( "github.com/turbot/steampipe/pkg/cmdconfig" "github.com/turbot/steampipe/pkg/constants" "github.com/turbot/steampipe/pkg/contexthelpers" - "github.com/turbot/steampipe/pkg/control" "github.com/turbot/steampipe/pkg/control/controldisplay" "github.com/turbot/steampipe/pkg/control/controlexecute" "github.com/turbot/steampipe/pkg/control/controlstatus" @@ -129,9 +124,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) { workspace := initData.Workspace client := initData.Client failures := 0 - var exportErrors []error - exportErrorsLock := &sync.Mutex{} - exportWaitGroup := &sync.WaitGroup{} var durations []time.Duration shouldShare := viper.IsSet(constants.ArgShare) @@ -145,7 +137,7 @@ func runCheckCmd(cmd *cobra.Command, args []string) { } // treat each arg as a separate execution - for _, arg := range args { + for _, targetName := range args { if utils.IsContextCancelled(ctx) { durations = append(durations, 0) // skip over this arg, since the execution was cancelled @@ -153,12 +145,8 @@ func runCheckCmd(cmd *cobra.Command, args []string) { continue } - // get the export formats for this argument - exportTargets, err := getExportTargets(arg) - error_helpers.FailOnError(err) - // create the execution tree - executionTree, err := controlexecute.NewExecutionTree(ctx, workspace, client, arg) + executionTree, err := controlexecute.NewExecutionTree(ctx, workspace, client, targetName) error_helpers.FailOnError(err) // execute controls synchronously (execute returns the number of failures) @@ -166,16 +154,9 @@ func runCheckCmd(cmd *cobra.Command, args []string) { err = displayControlResults(ctx, executionTree) error_helpers.FailOnError(err) - if len(exportTargets) > 0 { - d := &control.ExportData{ - ExecutionTree: executionTree, - Targets: exportTargets, - ErrorsLock: exportErrorsLock, - Errors: exportErrors, - WaitGroup: exportWaitGroup, - } - exportCheckResult(ctx, d) - } + exportArgs := viper.GetStringSlice(constants.ArgExport) + err = initData.ExportManager.DoExport(ctx, targetName, executionTree, exportArgs) + error_helpers.FailOnError(err) // if the share args are set, create a snapshot and share it if generateSnapshot { @@ -185,13 +166,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) { durations = append(durations, executionTree.EndTime.Sub(executionTree.StartTime)) } - // wait for exports to complete - exportWaitGroup.Wait() - - if len(exportErrors) > 0 { - error_helpers.ShowError(ctx, error_helpers.CombineErrors(exportErrors...)) - } - if shouldPrintTiming() { printTiming(args, durations) } @@ -241,6 +215,10 @@ func initialiseCheck(ctx context.Context) *initialisation.InitData { return initData } + if len(viper.GetStringSlice(constants.ArgExport)) > 0 { + registerCheckExporters(initData) + } + // control specific init if !w.ModfileExists() { initData.Result.Error = workspace.ErrorNoModDefinition @@ -269,6 +247,15 @@ func initialiseCheck(ctx context.Context) *initialisation.InitData { return initData } +// register exporters for each of the supported check formats +func registerCheckExporters(initData *initialisation.InitData) { + exporters, err := controldisplay.GetExporters() + error_helpers.FailOnErrorWithMessage(err, "failed to load exporters") + + // register all exporters + initData.RegisterExporters(exporters...) +} + func initialiseCheckColorScheme() error { theme := viper.GetString(constants.ArgTheme) if !viper.GetBool(constants.ConfigKeyIsTerminalTTY) { @@ -306,17 +293,6 @@ func shouldPrintTiming() bool { (outputFormat == constants.OutputFormatText || outputFormat == constants.OutputFormatBrief) } -func exportCheckResult(ctx context.Context, d *control.ExportData) { - d.WaitGroup.Add(1) - go func() { - err := exportControlResults(ctx, d.ExecutionTree, d.Targets) - if len(err) > 0 { - d.AddErrors(err) - } - d.WaitGroup.Done() - }() -} - func displayControlResults(ctx context.Context, executionTree *controlexecute.ExecutionTree) error { output := viper.GetString(constants.ArgOutput) formatter, err := parseOutputArg(output) @@ -328,141 +304,10 @@ func displayControlResults(ctx context.Context, executionTree *controlexecute.Ex if err != nil { return err } - // tactical solution to prettify the json output - if output == constants.OutputFormatJSON { - reader, err = prettifyJsonFromReader(reader) - if err != nil { - return err - } - } _, err = io.Copy(os.Stdout, reader) return err } -func exportControlResults(ctx context.Context, executionTree *controlexecute.ExecutionTree, targets []*controldisplay.CheckExportTarget) []error { - errors := []error{} - for _, target := range targets { - if utils.IsContextCancelled(ctx) { - // set the error - errors = append(errors, ctx.Err()) - // and skip forward - continue - } - - dataToExport, err := target.Formatter.Format(ctx, executionTree) - if err != nil { - errors = append(errors, err) - continue - } - if utils.IsContextCancelled(ctx) { - errors = append(errors, ctx.Err()) - continue - } - // create the output file - destination, err := os.Create(target.File) - if err != nil { - errors = append(errors, err) - continue - } - // tactical solution to prettify the json output - if target.Formatter.Name() == constants.OutputFormatJSON { - dataToExport, err = prettifyJsonFromReader(dataToExport) - if err != nil { - errors = append(errors, err) - continue - } - } - - _, err = io.Copy(destination, dataToExport) - if err != nil { - errors = append(errors, err) - continue - } - destination.Close() - } - - return errors -} - -func prettifyJsonFromReader(dataToExport io.Reader) (io.Reader, error) { - b, err := io.ReadAll(dataToExport) - if err != nil { - return nil, err - } - var prettyJSON bytes.Buffer - - err = json.Indent(&prettyJSON, b, "", " ") - if err != nil { - return nil, err - } - dataToExport = &prettyJSON - return dataToExport, nil -} - -func getExportTargets(executionName string) ([]*controldisplay.CheckExportTarget, error) { - var targets = make(map[string]*controldisplay.CheckExportTarget) - var targetErrors []error - - exports := viper.GetStringSlice(constants.ArgExport) - for _, export := range exports { - export = strings.TrimSpace(export) - if len(export) == 0 { - // if this is an empty string, ignore - continue - } - - newTarget, err := getExportTarget(executionName, export) - if err != nil { - targetErrors = append(targetErrors, err) - continue - } - if newTarget == nil { - targetErrors = append(targetErrors, fmt.Errorf("formatter satisfying '%s' not found", export)) - continue - } - // add to map if not already there - if _, ok := targets[newTarget.File]; !ok { - targets[newTarget.File] = newTarget - } - } - - // convert target map into array - targetList := maps.Values(targets) - return targetList, error_helpers.CombineErrors(targetErrors...) -} - -// getExportTarget parses the flag value, finds a matching formatter and returns an export target -func getExportTarget(executionName string, export string) (*controldisplay.CheckExportTarget, error) { - formatResolver, err := controldisplay.NewFormatResolver() - if err != nil { - return nil, err - } - - var fileName string - formatter, err := formatResolver.GetFormatter(export) - if err != nil { - // as we are resolving the format by extension, the arg will be the filename so return it - formatter, err = formatResolver.GetFormatterByExtension(export) - if err != nil { - return nil, err - } - if formatter == nil { - return nil, nil - } - // use the export arg as the filename - fileName = export - } - - if fileName == "" { - // we need to generate a filename - fileName = generateDefaultExportFileName(formatter, executionName) - } - - target := controldisplay.NewCheckExportTarget(formatter, fileName) - - return target, nil -} - // parseOutputArg parses the --output flag value and returns the Formatter that can format the data func parseOutputArg(arg string) (formatter controldisplay.Formatter, err error) { formatResolver, err := controldisplay.NewFormatResolver() @@ -472,9 +317,3 @@ func parseOutputArg(arg string) (formatter controldisplay.Formatter, err error) return formatResolver.GetFormatter(arg) } - -func generateDefaultExportFileName(formatter controldisplay.Formatter, executionName string) string { - now := time.Now() - timeFormatted := fmt.Sprintf("%d%02d%02d-%02d%02d%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) - return fmt.Sprintf("%s-%s%s", executionName, timeFormatted, formatter.FileExtension()) -} diff --git a/cmd/dashboard.go b/cmd/dashboard.go index df3e2a760..7613377f8 100644 --- a/cmd/dashboard.go +++ b/cmd/dashboard.go @@ -265,8 +265,8 @@ func getInitData(ctx context.Context, w *workspace.Workspace) *initialisation.In return initData } -func dashboardExporters() []*export.SnapshotExporter { - return []*export.SnapshotExporter{&export.SnapshotExporter{}} +func dashboardExporters() []export.Exporter { + return []export.Exporter{&export.SnapshotExporter{}} } func runSingleDashboard(ctx context.Context, targetName string, inputs map[string]interface{}) error { @@ -303,7 +303,8 @@ func runSingleDashboard(ctx context.Context, targetName string, inputs map[strin error_helpers.FailOnErrorWithMessage(err, "failed to upload snapshot") // export the result (if needed) - err = exportSnapshot(initData.ExportResolver, targetName, snap) + exportArgs := viper.GetStringSlice(constants.ArgExport) + err = initData.ExportManager.DoExport(ctx, targetName, snap, exportArgs) error_helpers.FailOnErrorWithMessage(err, "failed to export snapshot") return nil diff --git a/cmd/query.go b/cmd/query.go index 10e91efd1..10298f97e 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -21,7 +21,6 @@ import ( "github.com/turbot/steampipe/pkg/dashboard/dashboardtypes" "github.com/turbot/steampipe/pkg/display" "github.com/turbot/steampipe/pkg/error_helpers" - "github.com/turbot/steampipe/pkg/export" "github.com/turbot/steampipe/pkg/interactive" "github.com/turbot/steampipe/pkg/query" "github.com/turbot/steampipe/pkg/query/queryexecute" @@ -212,40 +211,14 @@ func executeSnapshotQuery(initData *query.InitData, w *workspace.Workspace, ctx err = uploadSnapshot(snap) // export the result if necessary - err = exportSnapshot(initData.ExportResolver, targetName, snap) + exportArgs := viper.GetStringSlice(constants.ArgExport) + err = initData.ExportManager.DoExport(ctx, targetName, snap, exportArgs) error_helpers.FailOnErrorWithMessage(err, "failed to export snapshot") } } return 0 } -func exportSnapshot(exportResolver *export.Resolver, targetName string, snap *dashboardtypes.SteampipeSnapshot) error { - exports := viper.GetStringSlice(constants.ArgExport) - if len(exports) == 0 { - return nil - } - - // get the short name for the target - parsedResource, err := modconfig.ParseResourceName(targetName) - if err != nil { - return err - } - shortName := parsedResource.Name - - targets, err := exportResolver.ResolveTargetsFromArgs(exports, shortName) - if err != nil { - return err - } - - var errors []error - for _, target := range targets { - if err := target.Export(snap); err != nil { - errors = append(errors, err) - } - } - return error_helpers.CombineErrors(errors...) -} - func snapshotToQueryResult(snap *dashboardtypes.SteampipeSnapshot, name string) (*queryresult.Result, error) { // find chart nde - we expect only 1 parsedName, err := modconfig.ParseResourceName(name) diff --git a/pkg/control/controldisplay/check_export_target.go b/pkg/control/controldisplay/check_export_target.go deleted file mode 100644 index 335c2e1bf..000000000 --- a/pkg/control/controldisplay/check_export_target.go +++ /dev/null @@ -1,13 +0,0 @@ -package controldisplay - -type CheckExportTarget struct { - Formatter Formatter - File string -} - -func NewCheckExportTarget(formatter Formatter, file string) *CheckExportTarget { - return &CheckExportTarget{ - Formatter: formatter, - File: file, - } -} diff --git a/pkg/control/controldisplay/control_exporter.go b/pkg/control/controldisplay/control_exporter.go new file mode 100644 index 000000000..a67406f40 --- /dev/null +++ b/pkg/control/controldisplay/control_exporter.go @@ -0,0 +1,43 @@ +package controldisplay + +import ( + "context" + "fmt" + "github.com/turbot/steampipe/pkg/control/controlexecute" + + "github.com/turbot/steampipe/pkg/export" +) + +type ControlExporter struct { + formatter Formatter +} + +func NewControlExporter(formatter Formatter) *ControlExporter { + return &ControlExporter{formatter} +} + +func (e *ControlExporter) Export(ctx context.Context, input export.ExportSourceData, destPath string) error { + // input must be control execution tree + tree, ok := input.(*controlexecute.ExecutionTree) + if !ok { + return fmt.Errorf("ControlExporter input must be *controlexecute.ExecutionTree") + } + res, err := e.formatter.Format(ctx, tree) + if err != nil { + return err + } + + return export.Write(destPath, res) +} + +func (e *ControlExporter) FileExtension() string { + return e.formatter.FileExtension() +} + +func (e *ControlExporter) Name() string { + return e.formatter.Name() +} + +func (e *ControlExporter) Alias() string { + return e.formatter.Alias() +} diff --git a/pkg/control/controldisplay/format_resolver.go b/pkg/control/controldisplay/format_resolver.go index 6fa83bd74..4a1978a89 100644 --- a/pkg/control/controldisplay/format_resolver.go +++ b/pkg/control/controldisplay/format_resolver.go @@ -3,15 +3,17 @@ package controldisplay import ( "fmt" "github.com/turbot/steampipe/pkg/constants" + "github.com/turbot/steampipe/pkg/export" "github.com/turbot/steampipe/pkg/filepaths" "os" "path/filepath" - "strings" ) type FormatResolver struct { - templates []*OutputTemplate - outputFormatters map[string]Formatter + templates []*OutputTemplate + formatterByName map[string]Formatter + // array of unique formatters used for export + exportFormatters []Formatter } func NewFormatResolver() (*FormatResolver, error) { @@ -19,107 +21,72 @@ func NewFormatResolver() (*FormatResolver, error) { if err != nil { return nil, err } - var outputFormatters = map[string]Formatter{ - constants.OutputFormatNone: &NullFormatter{}, - constants.OutputFormatText: &TextFormatter{}, - constants.OutputFormatBrief: &TextFormatter{}, - constants.OutputFormatSnapshot: &SnapshotFormatter{}, + + formatters := []Formatter{ + &NullFormatter{}, + &TextFormatter{}, + &SnapshotFormatter{}, } - return &FormatResolver{templates: templates, outputFormatters: outputFormatters}, nil + res := &FormatResolver{ + formatterByName: make(map[string]Formatter), + } + + for _, f := range formatters { + if err := res.registerFormatter(f); err != nil { + return nil, err + } + } + for _, t := range templates { + f, err := NewTemplateFormatter(t) + if err != nil { + return nil, err + } + + if err := res.registerFormatter(f); err != nil { + return nil, err + } + } + + return res, nil } func (r *FormatResolver) GetFormatter(arg string) (Formatter, error) { - if formatter, found := r.outputFormatters[arg]; found { + if formatter, found := r.formatterByName[arg]; found { return formatter, nil } - // otherwise look for a template - templateFormat, err := r.resolveOutputTemplate(arg) - if err != nil { - return nil, err - } - return NewTemplateFormatter(templateFormat) + return nil, fmt.Errorf("could not resolve formatter for %s", arg) } -func (r *FormatResolver) GetFormatterByExtension(filename string) (Formatter, error) { - // so we failed to exactly match an existing format or template name - // instead, treat the arg as a filename and try to infer the formatter from the extension +func (r *FormatResolver) registerFormatter(f Formatter) error { + name := f.Name() - extension := filepath.Ext(filename) - // first try the defined formatters - for _, formatter := range r.outputFormatters { - if formatter.FileExtension() == extension { - return formatter, nil + if _, ok := r.formatterByName[name]; ok { + return fmt.Errorf("failed to register output formatter - duplicate format name %s", name) + } + r.formatterByName[name] = f + // if the formatter has an alias, also register by alias + if alias := f.Alias(); alias != "" { + if _, ok := r.formatterByName[alias]; ok { + return fmt.Errorf("failed to register output formatter - duplicate format name %s", alias) } + r.formatterByName[alias] = f } - - // try to find the target template by the given filename - matchedTemplate, err := r.findTemplateByExtension(filename) - if err != nil { - return nil, err + // add to exportFormatters list (exclude 'None') + if f.Name() != constants.OutputFormatNone { + r.exportFormatters = append(r.exportFormatters, f) } - return NewTemplateFormatter(matchedTemplate) + return nil } -// resolveOutputTemplate accepts the export argument and resolves the template to use. -// If an exact match to the available templates is not found, and if 'allowFilenameEvaluation' is true -// then the 'export' value is parsed as a filename and the suffix is used to match to available templates -// returns -// - the export template to use -// - the path of the file to write to -// - error (if any) -func (r *FormatResolver) resolveOutputTemplate(export string) (format *OutputTemplate, err error) { - // try an exact match - for _, t := range r.templates { - if t.FormatName == export || t.FormatFullName == export { - return t, nil - } +func (r *FormatResolver) controlExporters() []export.Exporter { + res := make([]export.Exporter, len(r.exportFormatters)) + for i, formatter := range r.exportFormatters { + res[i] = NewControlExporter(formatter) + } - - return nil, fmt.Errorf("template %s not found", export) -} - -func (r *FormatResolver) findTemplateByExtension(filename string) (format *OutputTemplate, err error) { - // does the filename end with this exact format? - for _, t := range r.templates { - if strings.HasSuffix(filename, t.FormatFullName) { - return t, nil - } - } - - extension := filepath.Ext(filename) - if len(extension) == 0 { - // we don't have anything to work with - return nil, fmt.Errorf("template %s not found", filename) - } - var matchingTemplates []*OutputTemplate - - // does the given extension match with one of the template extension? - for _, t := range r.templates { - if strings.HasSuffix(t.OutputExtension, extension) { - matchingTemplates = append(matchingTemplates, t) - } - } - - if len(matchingTemplates) > 1 { - matchNames := []string{} - // find out if any of them has preference - for _, match := range matchingTemplates { - if match.DefaultTemplateForExtension { - return match, nil - } - matchNames = append(matchNames, match.FormatName) - } - // there's ambiguity - we have more than one matching templates based on extension - return nil, fmt.Errorf("ambiguous templates found: %v", matchNames) - } - - if len(matchingTemplates) == 1 { - return matchingTemplates[0], nil - } - - return nil, fmt.Errorf("template %s not found", filename) + return res } func loadAvailableTemplates() ([]*OutputTemplate, error) { diff --git a/pkg/control/controldisplay/formatter.go b/pkg/control/controldisplay/formatter.go index 9c85f1272..00c7a5b50 100644 --- a/pkg/control/controldisplay/formatter.go +++ b/pkg/control/controldisplay/formatter.go @@ -10,4 +10,11 @@ type Formatter interface { Format(ctx context.Context, tree *controlexecute.ExecutionTree) (io.Reader, error) FileExtension() string Name() string + Alias() string +} + +type FormatterBase struct{} + +func (*FormatterBase) Alias() string { + return "" } diff --git a/pkg/control/controldisplay/formatter_null.go b/pkg/control/controldisplay/formatter_null.go index a93a05c6d..cde8a5a40 100644 --- a/pkg/control/controldisplay/formatter_null.go +++ b/pkg/control/controldisplay/formatter_null.go @@ -2,6 +2,7 @@ package controldisplay import ( "context" + "github.com/turbot/steampipe/pkg/constants" "github.com/turbot/steampipe/pkg/control/controlexecute" "io" "strings" @@ -9,7 +10,9 @@ import ( // NullFormatter is to be used when no output is expected. It always returns a `io.Reader` which // reads an empty string -type NullFormatter struct{} +type NullFormatter struct { + FormatterBase +} func (j *NullFormatter) Format(ctx context.Context, tree *controlexecute.ExecutionTree) (io.Reader, error) { return strings.NewReader(""), nil @@ -21,6 +24,5 @@ func (j *NullFormatter) FileExtension() string { } func (j *NullFormatter) Name() string { - // will not be called - return "" + return constants.OutputFormatNone } diff --git a/pkg/control/controldisplay/formatter_snapshot.go b/pkg/control/controldisplay/formatter_snapshot.go index 4e9db52f9..e2172a55d 100644 --- a/pkg/control/controldisplay/formatter_snapshot.go +++ b/pkg/control/controldisplay/formatter_snapshot.go @@ -11,7 +11,9 @@ import ( "github.com/turbot/steampipe/pkg/control/controlexecute" ) -type SnapshotFormatter struct{} +type SnapshotFormatter struct { + FormatterBase +} func (f *SnapshotFormatter) Format(_ context.Context, tree *controlexecute.ExecutionTree) (io.Reader, error) { snapshot, err := executionTreeToSnapshot(tree) @@ -36,3 +38,7 @@ func (f *SnapshotFormatter) FileExtension() string { func (f SnapshotFormatter) Name() string { return constants.OutputFormatSnapshot } + +func (f *SnapshotFormatter) Alias() string { + return "" +} diff --git a/pkg/control/controldisplay/formatter_template.go b/pkg/control/controldisplay/formatter_template.go index 5ca8f0dc8..1be0d5d1d 100644 --- a/pkg/control/controldisplay/formatter_template.go +++ b/pkg/control/controldisplay/formatter_template.go @@ -2,7 +2,7 @@ package controldisplay import ( "context" - "fmt" + "github.com/turbot/steampipe/pkg/utils" "io" "os" "text/template" @@ -13,20 +13,6 @@ import ( "github.com/turbot/steampipe/pkg/version" ) -type TemplateRenderConfig struct { - RenderHeader bool -} -type TemplateRenderConstants struct { - SteampipeVersion string - WorkingDir string -} - -type TemplateRenderContext struct { - Constants TemplateRenderConstants - Config TemplateRenderConfig - Data *controlexecute.ExecutionTree -} - // TemplateFormatter implements the 'Formatter' interface and exposes a generic template based output mechanism // for 'check' execution trees type TemplateFormatter struct { @@ -34,6 +20,22 @@ type TemplateFormatter struct { exportFormat *OutputTemplate } +func NewTemplateFormatter(input *OutputTemplate) (*TemplateFormatter, error) { + templateFuncs := templateFuncs() + + // add a stub "render_context" function + // this will be overwritten before we execute the template + // if we don't put this here, then templates which use this + // won't parse and will throw Error: template: ****: function "render_context" not defined + templateFuncs["render_context"] = func() TemplateRenderContext { return TemplateRenderContext{} } + + t := template.Must(template.New("outlet"). + Funcs(templateFuncs). + ParseFS(os.DirFS(input.TemplatePath), "*")) + + return &TemplateFormatter{exportFormat: input, template: t}, nil +} + func (tf TemplateFormatter) Format(ctx context.Context, tree *controlexecute.ExecutionTree) (io.Reader, error) { reader, writer := io.Pipe() go func() { @@ -70,35 +72,30 @@ func (tf TemplateFormatter) Format(ctx context.Context, tree *controlexecute.Exe writer.Close() } }() + + // tactical - for json, prettify the output + if tf.shouldPrettify(){ + return utils.PrettifyJsonFromReader(reader) + } + return reader, nil } func (tf TemplateFormatter) FileExtension() string { - // if the extension is the same as the format name, return just the extension - if tf.exportFormat.DefaultTemplateForExtension { - return tf.exportFormat.OutputExtension - } else { - // otherwise return the fullname - return fmt.Sprintf(".%s", tf.exportFormat.FormatFullName) - } + return tf.exportFormat.FileExtension } func (tf TemplateFormatter) Name() string { return tf.exportFormat.FormatName } -func NewTemplateFormatter(input *OutputTemplate) (*TemplateFormatter, error) { - templateFuncs := templateFuncs() - - // add a stub "render_context" function - // this will be overwritten before we execute the template - // if we don't put this here, then templates which use this - // won't parse and will throw Error: template: ****: function "render_context" not defined - templateFuncs["render_context"] = func() TemplateRenderContext { return TemplateRenderContext{} } - - t := template.Must(template.New("outlet"). - Funcs(templateFuncs). - ParseFS(os.DirFS(input.TemplatePath), "*")) - - return &TemplateFormatter{exportFormat: input, template: t}, nil +func (tf TemplateFormatter) Alias() string { + if tf.exportFormat.FormatFullName != tf.exportFormat.FormatName { + return tf.exportFormat.FormatFullName + } + return "" +} + +func (tf TemplateFormatter) shouldPrettify() bool { + return tf.Name() == constants.OutputFormatJSON } diff --git a/pkg/control/controldisplay/formatter_template_test.go b/pkg/control/controldisplay/formatter_template_test.go index 14a73350c..c075694ee 100644 --- a/pkg/control/controldisplay/formatter_template_test.go +++ b/pkg/control/controldisplay/formatter_template_test.go @@ -1,192 +1,183 @@ package controldisplay -import ( - "log" - "os" - "path/filepath" - "testing" - - "github.com/otiai10/copy" - "github.com/turbot/steampipe/pkg/filepaths" -) - -type resolveOutputTemplateTestCase struct { - input string // --export - expected interface{} -} - -// TODO KAI change this test to GetFormatter and GetFormatterByExtension - -var resolveOutputTemplateTestCases = map[string]resolveOutputTemplateTestCase{ - "html": { - input: "html", - expected: OutputTemplate{ - FormatFullName: "html.html", - OutputExtension: ".html", - }, - }, - "nunit3": { - input: "nunit3", - expected: OutputTemplate{ - FormatFullName: "nunit3.xml", - OutputExtension: ".xml", - }, - }, - "markdown": { - input: "md", - expected: OutputTemplate{ - FormatFullName: "md.md", - OutputExtension: ".md", - }, - }, - - "nunit3.xml": { - input: "nunit3.xml", - expected: OutputTemplate{ - FormatFullName: "nunit3.xml", - OutputExtension: ".xml", - }, - }, - "markdown.md": { - input: "markdown.md", - expected: "ERROR", - //OutputTemplate{ - // FormatFullName: "md.md", - // OutputExtension: ".md", - //}, - }, - // "txt.dat": { - // input: "txt.dat", - // expected: OutputTemplate{ - // FormatFullName: "txt.dat", - // OutputExtension: ".dat", - // }, - // }, - // "custom.txt": { - // input: "custom.txt", - // expected: OutputTemplate{ - // FormatFullName: "custom.txt", - // OutputExtension: ".txt", - // }, - // }, - "foo.xml": { - input: "foo.xml", - - expected: "ERROR", - // OutputTemplate{ - // FormatFullName: "nunit3.xml", - // OutputExtension: ".xml", - //}, - }, - "brief.html": { - input: "brief.html", - expected: "ERROR", - // OutputTemplate{ - // FormatFullName: "html.html", - // OutputExtension: ".html", - //}, - }, - "output.html": { - input: "output.html", - expected: "ERROR", - // OutputTemplate{ - // FormatFullName: "html.html", - // OutputExtension: ".html", - //}, - }, - "output.md": { - input: "output.md", - expected: "ERROR", - // OutputTemplate{ - // FormatFullName: "md.md", - // OutputExtension: ".md", - //}, - }, - // "output.txt": { - // input: "output.txt", - // expected: OutputTemplate{ - // FormatFullName: "custom.txt", - // OutputExtension: ".txt", - // }, - // }, - // "output.dat": { - // input: "output.dat", - // expected: OutputTemplate{ - // FormatFullName: "txt.dat", - // OutputExtension: ".dat", - // }, - // }, - "output.brief.html": { - input: "output.brief.html", - expected: "ERROR", - // OutputTemplate{ - // FormatFullName: "html.html", - // OutputExtension: ".html", - //}, - }, - "output.nunit3.xml": { - input: "output.nunit3.xml", - expected: "ERROR", - // OutputTemplate{ - // FormatFullName: "nunit3.xml", - // OutputExtension: ".xml", - //}, - }, - "output.foo.xml": { - input: "output.foo.xml", - expected: "ERROR", - // OutputTemplate{ - // FormatFullName: "nunit3.xml", - // OutputExtension: ".xml", - //}, - }, -} - -func setup() { - filepaths.SteampipeDir = "~/.steampipe" - source, err := filepath.Abs("templates") - if err != nil { - log.Fatal(err) - } - - dest, err := filepath.Abs("~/.steampipe/check/templates") - if err != nil { - log.Fatal(err) - } - - err = copy.Copy(source, dest) - if err != nil { - log.Fatal(err) - } -} - -func teardown() { - os.RemoveAll("~/.steampipe/check/templates") -} - -func TestResolveOutputTemplate(t *testing.T) { - setup() - resolver, _ := NewFormatResolver() - for name, test := range resolveOutputTemplateTestCases { - outputTemplate, err := resolver.resolveOutputTemplate(test.input) - if err != nil { - if test.expected != "ERROR" { - t.Errorf("Test: '%s'' FAILED with unexpected error: %v", name, err) - } - continue - } - if test.expected == "ERROR" { - t.Errorf("Test: '%s'' FAILED - expected error", name) - continue - } - expectedFormat := test.expected.(OutputTemplate) - if !FormatEqual(outputTemplate, &expectedFormat) { - t.Errorf("Test: '%s'' FAILED : expected:\n%s\n\ngot:\n%s", name, expectedFormat.FormatName, outputTemplate) - } - } - teardown() -} - -func FormatEqual(l, r *OutputTemplate) bool { - return (l.FormatFullName == r.FormatFullName) -} +// +//type resolveOutputTemplateTestCase struct { +// input string // --export +// expected interface{} +//} +// +//// TODO KAI change this test to GetFormatter and getFormatterByExtension +// +//var resolveOutputTemplateTestCases = map[string]resolveOutputTemplateTestCase{ +// "html": { +// input: "html", +// expected: OutputTemplate{ +// FormatFullName: "html.html", +// FileExtension: ".html", +// }, +// }, +// "nunit3": { +// input: "nunit3", +// expected: OutputTemplate{ +// FormatFullName: "nunit3.xml", +// FileExtension: ".xml", +// }, +// }, +// "markdown": { +// input: "md", +// expected: OutputTemplate{ +// FormatFullName: "md.md", +// FileExtension: ".md", +// }, +// }, +// +// "nunit3.xml": { +// input: "nunit3.xml", +// expected: OutputTemplate{ +// FormatFullName: "nunit3.xml", +// FileExtension: ".xml", +// }, +// }, +// "markdown.md": { +// input: "markdown.md", +// expected: "ERROR", +// //OutputTemplate{ +// // FormatFullName: "md.md", +// // OutputExtension: ".md", +// //}, +// }, +// // "txt.dat": { +// // input: "txt.dat", +// // expected: OutputTemplate{ +// // FormatFullName: "txt.dat", +// // OutputExtension: ".dat", +// // }, +// // }, +// // "custom.txt": { +// // input: "custom.txt", +// // expected: OutputTemplate{ +// // FormatFullName: "custom.txt", +// // OutputExtension: ".txt", +// // }, +// // }, +// "foo.xml": { +// input: "foo.xml", +// +// expected: "ERROR", +// // OutputTemplate{ +// // FormatFullName: "nunit3.xml", +// // OutputExtension: ".xml", +// //}, +// }, +// "brief.html": { +// input: "brief.html", +// expected: "ERROR", +// // OutputTemplate{ +// // FormatFullName: "html.html", +// // OutputExtension: ".html", +// //}, +// }, +// "output.html": { +// input: "output.html", +// expected: "ERROR", +// // OutputTemplate{ +// // FormatFullName: "html.html", +// // OutputExtension: ".html", +// //}, +// }, +// "output.md": { +// input: "output.md", +// expected: "ERROR", +// // OutputTemplate{ +// // FormatFullName: "md.md", +// // OutputExtension: ".md", +// //}, +// }, +// // "output.txt": { +// // input: "output.txt", +// // expected: OutputTemplate{ +// // FormatFullName: "custom.txt", +// // OutputExtension: ".txt", +// // }, +// // }, +// // "output.dat": { +// // input: "output.dat", +// // expected: OutputTemplate{ +// // FormatFullName: "txt.dat", +// // OutputExtension: ".dat", +// // }, +// // }, +// "output.brief.html": { +// input: "output.brief.html", +// expected: "ERROR", +// // OutputTemplate{ +// // FormatFullName: "html.html", +// // OutputExtension: ".html", +// //}, +// }, +// "output.nunit3.xml": { +// input: "output.nunit3.xml", +// expected: "ERROR", +// // OutputTemplate{ +// // FormatFullName: "nunit3.xml", +// // OutputExtension: ".xml", +// //}, +// }, +// "output.foo.xml": { +// input: "output.foo.xml", +// expected: "ERROR", +// // OutputTemplate{ +// // FormatFullName: "nunit3.xml", +// // OutputExtension: ".xml", +// //}, +// }, +//} +// +//func setup() { +// filepaths.SteampipeDir = "~/.steampipe" +// source, err := filepath.Abs("templates") +// if err != nil { +// log.Fatal(err) +// } +// +// dest, err := filepath.Abs("~/.steampipe/check/templates") +// if err != nil { +// log.Fatal(err) +// } +// +// err = copy.Copy(source, dest) +// if err != nil { +// log.Fatal(err) +// } +//} +// +//func teardown() { +// os.RemoveAll("~/.steampipe/check/templates") +//} +// +//func TestResolveOutputTemplate(t *testing.T) { +// setup() +// resolver, _ := NewFormatResolver() +// for name, test := range resolveOutputTemplateTestCases { +// outputTemplate, err := resolver.resolveOutputTemplate(test.input) +// if err != nil { +// if test.expected != "ERROR" { +// t.Errorf("Test: '%s'' FAILED with unexpected error: %v", name, err) +// } +// continue +// } +// if test.expected == "ERROR" { +// t.Errorf("Test: '%s'' FAILED - expected error", name) +// continue +// } +// expectedFormat := test.expected.(OutputTemplate) +// if !FormatEqual(outputTemplate, &expectedFormat) { +// t.Errorf("Test: '%s'' FAILED : expected:\n%s\n\ngot:\n%s", name, expectedFormat.FormatName, outputTemplate) +// } +// } +// teardown() +//} +// +//func FormatEqual(l, r *OutputTemplate) bool { +// return (l.FormatFullName == r.FormatFullName) +//} diff --git a/pkg/control/controldisplay/formatter_text.go b/pkg/control/controldisplay/formatter_text.go index cff512d7e..4f1f1bfbd 100644 --- a/pkg/control/controldisplay/formatter_text.go +++ b/pkg/control/controldisplay/formatter_text.go @@ -14,7 +14,9 @@ import ( const MaxColumns = 200 -type TextFormatter struct{} +type TextFormatter struct { + FormatterBase +} func (tf *TextFormatter) Format(ctx context.Context, tree *controlexecute.ExecutionTree) (io.Reader, error) { renderer := NewTableRenderer(tree) diff --git a/pkg/control/controldisplay/get_exporters.go b/pkg/control/controldisplay/get_exporters.go new file mode 100644 index 000000000..1f52ca56b --- /dev/null +++ b/pkg/control/controldisplay/get_exporters.go @@ -0,0 +1,15 @@ +package controldisplay + +import ( + "github.com/turbot/steampipe/pkg/export" +) + +// GetExporters returns an array of ControlExporters corresponding to the available output formats +func GetExporters() ([]export.Exporter, error) { + formatResolver, err := NewFormatResolver() + if err != nil { + return nil, err + } + exporters := formatResolver.controlExporters() + return exporters, nil +} diff --git a/pkg/control/controldisplay/output_template.go b/pkg/control/controldisplay/output_template.go index 1ee8db43f..322e8225d 100644 --- a/pkg/control/controldisplay/output_template.go +++ b/pkg/control/controldisplay/output_template.go @@ -7,33 +7,26 @@ import ( ) type OutputTemplate struct { - TemplatePath string - FormatName string - OutputExtension string - FormatFullName string - DefaultTemplateForExtension bool + TemplatePath string + FormatName string + FileExtension string + FormatFullName string } -func NewOutputTemplate(directory string) *OutputTemplate { +func NewOutputTemplate(directoryPath string) *OutputTemplate { format := new(OutputTemplate) - format.TemplatePath = directory + format.TemplatePath = directoryPath - directory = filepath.Base(directory) + directoryName := filepath.Base(directoryPath) + // does the directory name include an extension? + ext := filepath.Ext(directoryName) + format.FormatFullName = directoryName + format.FormatName = strings.TrimSuffix(directoryName, ext) + format.FileExtension = fmt.Sprintf(".%s", directoryName) - // try splitting by a .(dot) - lastDotIndex := strings.LastIndex(directory, ".") - if lastDotIndex == -1 { - format.OutputExtension = fmt.Sprintf(".%s", directory) - format.FormatName = directory - format.DefaultTemplateForExtension = true - } else { - format.OutputExtension = filepath.Ext(directory) - format.FormatName = strings.TrimSuffix(directory, filepath.Ext(directory)) - } - format.FormatFullName = fmt.Sprintf("%s%s", format.FormatName, format.OutputExtension) return format } func (ft *OutputTemplate) String() string { - return fmt.Sprintf("( %s %s %s %s )", ft.TemplatePath, ft.FormatName, ft.OutputExtension, ft.FormatFullName) + return fmt.Sprintf("( %s %s %s %s )", ft.TemplatePath, ft.FormatName, ft.FileExtension, ft.FormatFullName) } diff --git a/pkg/control/controldisplay/template_render_context.go b/pkg/control/controldisplay/template_render_context.go new file mode 100644 index 000000000..9ab5ff148 --- /dev/null +++ b/pkg/control/controldisplay/template_render_context.go @@ -0,0 +1,18 @@ +package controldisplay + +import "github.com/turbot/steampipe/pkg/control/controlexecute" + +type TemplateRenderConfig struct { + RenderHeader bool +} + +type TemplateRenderConstants struct { + SteampipeVersion string + WorkingDir string +} + +type TemplateRenderContext struct { + Constants TemplateRenderConstants + Config TemplateRenderConfig + Data *controlexecute.ExecutionTree +} diff --git a/pkg/control/export_data.go b/pkg/control/export_data.go deleted file mode 100644 index 4888e7497..000000000 --- a/pkg/control/export_data.go +++ /dev/null @@ -1,22 +0,0 @@ -package control - -import ( - "sync" - - "github.com/turbot/steampipe/pkg/control/controldisplay" - "github.com/turbot/steampipe/pkg/control/controlexecute" -) - -type ExportData struct { - ExecutionTree *controlexecute.ExecutionTree - Targets []*controldisplay.CheckExportTarget - ErrorsLock *sync.Mutex - Errors []error - WaitGroup *sync.WaitGroup -} - -func (e *ExportData) AddErrors(err []error) { - e.ErrorsLock.Lock() - e.Errors = append(e.Errors, err...) - e.ErrorsLock.Unlock() -} diff --git a/pkg/export/exporter.go b/pkg/export/exporter.go index 26c8076e1..2d5586c32 100644 --- a/pkg/export/exporter.go +++ b/pkg/export/exporter.go @@ -1,12 +1,21 @@ package export +import "context" + // ExportSourceData is an interface implemented by all types which can be used as an input to an exporter type ExportSourceData interface { IsExportSourceData() } type Exporter interface { - Export(input ExportSourceData, destPath string) error + Export(ctx context.Context, input ExportSourceData, destPath string) error FileExtension() string Name() string + Alias() string +} + +type ExporterBase struct{} + +func (*ExporterBase) Alias() string { + return "" } diff --git a/pkg/export/helpers.go b/pkg/export/helpers.go index d2d41e707..c1e8aba79 100644 --- a/pkg/export/helpers.go +++ b/pkg/export/helpers.go @@ -4,17 +4,16 @@ import ( "fmt" "io" "os" - "strings" "time" ) -func generateDefaultExportFileName(exporter Exporter, executionName string) string { +func GenerateDefaultExportFileName(exporter Exporter, executionName string) string { now := time.Now() timeFormatted := fmt.Sprintf("%d%02d%02d-%02d%02d%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) return fmt.Sprintf("%s-%s%s", executionName, timeFormatted, exporter.FileExtension()) } -func writeExport(filePath string, exportData *strings.Reader) error { +func Write(filePath string, exportData io.Reader) error { // create the output file destination, err := os.Create(filePath) if err != nil { diff --git a/pkg/export/manager.go b/pkg/export/manager.go new file mode 100644 index 000000000..c267cf264 --- /dev/null +++ b/pkg/export/manager.go @@ -0,0 +1,150 @@ +package export + +import ( + "context" + "fmt" + "github.com/turbot/steampipe/pkg/error_helpers" + "golang.org/x/exp/maps" + "path" + "strings" +) + +type Manager struct { + registeredExporters map[string]Exporter + registeredExtensions map[string]Exporter +} + +func NewManager() *Manager { + return &Manager{ + registeredExporters: make(map[string]Exporter), + registeredExtensions: make(map[string]Exporter), + } +} + +func (m *Manager) Register(exporter Exporter) error { + name := exporter.Name() + if _, ok := m.registeredExporters[name]; ok { + return fmt.Errorf("failed to register exporter - duplicate name %s", name) + } + m.registeredExporters[exporter.Name()] = exporter + + // if the exporter has an alias, also register by alias + if alias := exporter.Alias(); alias != "" { + if _, ok := m.registeredExporters[alias]; ok { + return fmt.Errorf("failed to register exporter - duplicate name %s", name) + } + m.registeredExporters[alias] = exporter + } + + // now register extension + ext := exporter.FileExtension() + m.registerExporterByExtension(exporter, ext) + // if the extension has multiple segments, try to register for the short version as well + if shortExtension := path.Ext(ext); shortExtension != ext { + m.registerExporterByExtension(exporter, shortExtension) + } + return nil +} + +func (m *Manager) registerExporterByExtension(exporter Exporter, ext string) { + // do we already have an exporter registered for this extension? + if existing, ok := m.registeredExtensions[ext]; ok { + + // check if either the existing or new template is the default for extension + existingIsDefaultForExt := isDefaultExporterForExtension(existing) + newIsDefaultForExt := isDefaultExporterForExtension(exporter) + + // if NEITHER are default for the extension, there is a clash which cannot be resolved - + // we must remove the existing key + if !newIsDefaultForExt && !existingIsDefaultForExt { + delete(m.registeredExtensions, ext) + } + + // if existing is default and new isn't, nothing to do + if existingIsDefaultForExt { + return + } + + // to get here, new must be default exporter for extension + // (it is impossible for both to be default as that implies duplicate exporter names) + // fall through to... + } + + // register the extension + m.registeredExtensions[ext] = exporter +} + +// an exporter is the 'default for extension' if the exporter name is the same as the extension name +// i.e. json exporter would be the default for the `.json` extension +func isDefaultExporterForExtension(existing Exporter) bool { + return strings.TrimPrefix(existing.FileExtension(), ".") == existing.Name() +} + +func (m *Manager) resolveTargetsFromArgs(exportArgs []string, executionName string) ([]*Target, error) { + var targets = make(map[string]*Target) + var targetErrors []error + + for _, export := range exportArgs { + export = strings.TrimSpace(export) + if len(export) == 0 { + // if this is an empty string, ignore + continue + } + + t, err := m.getExportTarget(export, executionName) + if err != nil { + targetErrors = append(targetErrors, err) + continue + } + + // add to map if not already there + if _, ok := targets[t.filePath]; !ok { + targets[t.filePath] = t + } + } + + // convert target map into array + targetList := maps.Values(targets) + return targetList, error_helpers.CombineErrors(targetErrors...) +} + +func (m *Manager) getExportTarget(export, executionName string) (*Target, error) { + if e, ok := m.registeredExporters[export]; ok { + t := &Target{ + exporter: e, + filePath: GenerateDefaultExportFileName(e, executionName), + } + return t, nil + } + + // now try by extension + ext := path.Ext(export) + if e, ok := m.registeredExtensions[ext]; ok { + t := &Target{ + exporter: e, + filePath: export, + } + return t, nil + } + + return nil, fmt.Errorf("formatter satisfying '%s' not found", export) +} + +func (m *Manager) DoExport(ctx context.Context, targetName string, source ExportSourceData, exports []string) error { + if len(exports) == 0 { + return nil + } + + targets, err := m.resolveTargetsFromArgs(exports, targetName) + if err != nil { + return err + } + + var errors []error + for _, target := range targets { + if err := target.Export(ctx, source); err != nil { + errors = append(errors, err) + } + } + return error_helpers.CombineErrors(errors...) +} diff --git a/pkg/export/resolver.go b/pkg/export/resolver.go deleted file mode 100644 index dbf6e547c..000000000 --- a/pkg/export/resolver.go +++ /dev/null @@ -1,74 +0,0 @@ -package export - -import ( - "fmt" - "github.com/turbot/steampipe/pkg/error_helpers" - "golang.org/x/exp/maps" - "path" - "strings" -) - -type Resolver struct { - registeredExporters map[string]Exporter - registeredExtensions map[string]Exporter -} - -func NewResolver() *Resolver { - return &Resolver{ - registeredExporters: make(map[string]Exporter), - registeredExtensions: make(map[string]Exporter), - } -} - -func (r *Resolver) Register(exporter Exporter) { - r.registeredExporters[exporter.Name()] = exporter - r.registeredExporters[exporter.FileExtension()] = exporter -} - -func (r *Resolver) ResolveTargetsFromArgs(exportArgs []string, executionName string) ([]*Target, error) { - - var targets = make(map[string]*Target) - var targetErrors []error - - for _, export := range exportArgs { - export = strings.TrimSpace(export) - if len(export) == 0 { - // if this is an empty string, ignore - continue - } - - t, err := r.getExportTarget(export, executionName) - if err != nil { - targetErrors = append(targetErrors, err) - continue - } - - // add to map if not already there - if _, ok := targets[t.filePath]; !ok { - targets[t.filePath] = t - } - } - - // convert target map into array - targetList := maps.Values(targets) - return targetList, error_helpers.CombineErrors(targetErrors...) -} - -func (r *Resolver) getExportTarget(export, executionName string) (*Target, error) { - if e, ok := r.registeredExporters[export]; ok { - t := &Target{ - exporter: e, - filePath: generateDefaultExportFileName(e, executionName), - } - return t, nil - } - - if e, ok := r.registeredExtensions[path.Ext(export)]; ok { - t := &Target{ - exporter: e, - filePath: export, - } - return t, nil - } - return nil, fmt.Errorf("formatter satisfying '%s' not found", export) -} diff --git a/pkg/export/snapshot_exporter.go b/pkg/export/snapshot_exporter.go index 1248e36c4..817f76186 100644 --- a/pkg/export/snapshot_exporter.go +++ b/pkg/export/snapshot_exporter.go @@ -1,6 +1,7 @@ package export import ( + "context" "encoding/json" "fmt" "github.com/turbot/steampipe/pkg/constants" @@ -9,9 +10,10 @@ import ( ) type SnapshotExporter struct { + ExporterBase } -func (e *SnapshotExporter) Export(input ExportSourceData, filePath string) error { +func (e *SnapshotExporter) Export(_ context.Context, input ExportSourceData, filePath string) error { snapshot, ok := input.(*dashboardtypes.SteampipeSnapshot) if !ok { @@ -24,7 +26,7 @@ func (e *SnapshotExporter) Export(input ExportSourceData, filePath string) error res := strings.NewReader(fmt.Sprintf("%s\n", string(snapshotStr))) - return writeExport(filePath, res) + return Write(filePath, res) } func (e *SnapshotExporter) FileExtension() string { diff --git a/pkg/export/target.go b/pkg/export/target.go index 13190a4c4..e78e73438 100644 --- a/pkg/export/target.go +++ b/pkg/export/target.go @@ -1,10 +1,12 @@ package export +import "context" + type Target struct { exporter Exporter filePath string } -func (t *Target) Export(input ExportSourceData) error { - return t.exporter.Export(input, t.filePath) +func (t *Target) Export(ctx context.Context, input ExportSourceData) error { + return t.exporter.Export(ctx, input, t.filePath) } diff --git a/pkg/initialisation/init_data.go b/pkg/initialisation/init_data.go index 0bcb5b694..a67df91a6 100644 --- a/pkg/initialisation/init_data.go +++ b/pkg/initialisation/init_data.go @@ -29,7 +29,7 @@ type InitData struct { PreparedStatementSource *modconfig.ResourceMaps ShutdownTelemetry func() - ExportResolver *export.Resolver + ExportManager *export.Manager } func NewErrorInitData(err error) *InitData { @@ -40,17 +40,17 @@ func NewErrorInitData(err error) *InitData { func NewInitData(w *workspace.Workspace) *InitData { i := &InitData{ - Workspace: w, - Result: &db_common.InitResult{}, - ExportResolver: export.NewResolver(), + Workspace: w, + Result: &db_common.InitResult{}, + ExportManager: export.NewManager(), } return i } -func (i *InitData) RegisterExporters(exporters ...*export.SnapshotExporter) *InitData { +func (i *InitData) RegisterExporters(exporters ...export.Exporter) *InitData { for _, e := range exporters { - i.ExportResolver.Register(e) + i.ExportManager.Register(e) } return i diff --git a/pkg/query/init_data.go b/pkg/query/init_data.go index 6a4cb7a60..85f9e2bba 100644 --- a/pkg/query/init_data.go +++ b/pkg/query/init_data.go @@ -35,8 +35,8 @@ func NewInitData(ctx context.Context, w *workspace.Workspace, args []string) *In return i } -func queryExporters() []*export.SnapshotExporter { - return []*export.SnapshotExporter {&export.SnapshotExporter{}} +func queryExporters() []export.Exporter { + return []export.Exporter{&export.SnapshotExporter{}} } func (i *InitData) Cancel() { diff --git a/pkg/utils/json.go b/pkg/utils/json.go new file mode 100644 index 000000000..603b1c9a3 --- /dev/null +++ b/pkg/utils/json.go @@ -0,0 +1,22 @@ +package utils + +import ( + "bytes" + "encoding/json" + "io" +) + +func PrettifyJsonFromReader(dataToExport io.Reader) (io.Reader, error) { + b, err := io.ReadAll(dataToExport) + if err != nil { + return nil, err + } + var prettyJSON bytes.Buffer + + err = json.Indent(&prettyJSON, b, "", " ") + if err != nil { + return nil, err + } + dataToExport = &prettyJSON + return dataToExport, nil +}