mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-23 08:00:51 -05:00
Refactor control export to use ExportManager. Closes #2515
This commit is contained in:
197
cmd/check.go
197
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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
31
cmd/query.go
31
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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
43
pkg/control/controldisplay/control_exporter.go
Normal file
43
pkg/control/controldisplay/control_exporter.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 <val>
|
||||
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 <val>
|
||||
// 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)
|
||||
//}
|
||||
|
||||
@@ -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)
|
||||
|
||||
15
pkg/control/controldisplay/get_exporters.go
Normal file
15
pkg/control/controldisplay/get_exporters.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
18
pkg/control/controldisplay/template_render_context.go
Normal file
18
pkg/control/controldisplay/template_render_context.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
150
pkg/export/manager.go
Normal file
150
pkg/export/manager.go
Normal file
@@ -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...)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
22
pkg/utils/json.go
Normal file
22
pkg/utils/json.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user