github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/export/manager.go (about)

     1  package export
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path"
     7  	"strings"
     8  
     9  	"github.com/turbot/steampipe-plugin-sdk/v5/sperr"
    10  	"github.com/turbot/steampipe/pkg/error_helpers"
    11  	"github.com/turbot/steampipe/pkg/statushooks"
    12  	"github.com/turbot/steampipe/pkg/utils"
    13  	"golang.org/x/exp/maps"
    14  	"golang.org/x/exp/slices"
    15  )
    16  
    17  type Manager struct {
    18  	registeredExporters  map[string]Exporter
    19  	registeredExtensions map[string]Exporter
    20  }
    21  
    22  func NewManager() *Manager {
    23  	return &Manager{
    24  		registeredExporters:  make(map[string]Exporter),
    25  		registeredExtensions: make(map[string]Exporter),
    26  	}
    27  }
    28  
    29  func (m *Manager) Register(exporter Exporter) error {
    30  	name := exporter.Name()
    31  	if _, ok := m.registeredExporters[name]; ok {
    32  		return fmt.Errorf("failed to register exporter - duplicate name %s", name)
    33  	}
    34  	m.registeredExporters[exporter.Name()] = exporter
    35  
    36  	// if the exporter has an alias, also register by alias
    37  	if alias := exporter.Alias(); alias != "" {
    38  		if _, ok := m.registeredExporters[alias]; ok {
    39  			return fmt.Errorf("failed to register exporter - duplicate name %s", name)
    40  		}
    41  		m.registeredExporters[alias] = exporter
    42  	}
    43  
    44  	// now register extension
    45  	ext := exporter.FileExtension()
    46  	m.registerExporterByExtension(exporter, ext)
    47  	// if the extension has multiple segments, try to register for the short version as well
    48  	if shortExtension := path.Ext(ext); shortExtension != ext {
    49  		m.registerExporterByExtension(exporter, shortExtension)
    50  	}
    51  	return nil
    52  }
    53  
    54  func (m *Manager) registerExporterByExtension(exporter Exporter, ext string) {
    55  	// do we already have an exporter registered for this extension?
    56  	if existing, ok := m.registeredExtensions[ext]; ok {
    57  
    58  		// check if either the existing or new template is the default for extension
    59  		existingIsDefaultForExt := isDefaultExporterForExtension(existing)
    60  		newIsDefaultForExt := isDefaultExporterForExtension(exporter)
    61  
    62  		// if  NEITHER are default for the extension, there is a clash which cannot be resolved -
    63  		// we must remove the existing key
    64  		if !newIsDefaultForExt && !existingIsDefaultForExt {
    65  			delete(m.registeredExtensions, ext)
    66  		}
    67  
    68  		// if existing is default and new isn't, nothing to do
    69  		if existingIsDefaultForExt {
    70  			return
    71  		}
    72  
    73  		// to get here, new must be default exporter for extension
    74  		// (it is impossible for both to be default as that implies duplicate exporter names)
    75  		// fall through to...
    76  	}
    77  
    78  	// register the extension
    79  	m.registeredExtensions[ext] = exporter
    80  }
    81  
    82  // an exporter is the 'default for extension' if the exporter name is the same as the extension name
    83  // i.e. json exporter would be the default for the `.json` extension
    84  func isDefaultExporterForExtension(existing Exporter) bool {
    85  	return strings.TrimPrefix(existing.FileExtension(), ".") == existing.Name()
    86  }
    87  
    88  func (m *Manager) resolveTargetsFromArgs(exportArgs []string, executionName string) ([]*Target, error) {
    89  	var targets = make(map[string]*Target)
    90  	var targetErrors []error
    91  
    92  	for _, export := range exportArgs {
    93  		export = strings.TrimSpace(export)
    94  		if len(export) == 0 {
    95  			// if this is an empty string, ignore
    96  			continue
    97  		}
    98  
    99  		t, err := m.getExportTarget(export, executionName)
   100  		if err != nil {
   101  			targetErrors = append(targetErrors, err)
   102  			continue
   103  		}
   104  
   105  		// add to map if not already there
   106  		if _, ok := targets[t.filePath]; !ok {
   107  			targets[t.filePath] = t
   108  		}
   109  	}
   110  
   111  	// convert target map into array
   112  	targetList := maps.Values(targets)
   113  	return targetList, error_helpers.CombineErrors(targetErrors...)
   114  }
   115  
   116  func (m *Manager) getExportTarget(export, executionName string) (*Target, error) {
   117  	if e, ok := m.registeredExporters[export]; ok {
   118  		t := &Target{
   119  			exporter: e,
   120  			filePath: GenerateDefaultExportFileName(executionName, e.FileExtension()),
   121  		}
   122  		return t, nil
   123  	}
   124  
   125  	// now try by extension
   126  	ext := path.Ext(export)
   127  	if e, ok := m.registeredExtensions[ext]; ok {
   128  		t := &Target{
   129  			exporter:      e,
   130  			filePath:      export,
   131  			isNamedTarget: true,
   132  		}
   133  		return t, nil
   134  	}
   135  
   136  	return nil, fmt.Errorf("formatter satisfying '%s' not found", export)
   137  }
   138  
   139  func (m *Manager) DoExport(ctx context.Context, targetName string, source ExportSourceData, exports []string) ([]string, error) {
   140  	var errors []error
   141  	var msg string
   142  	var expLocation []string
   143  
   144  	if len(exports) == 0 {
   145  		return nil, nil
   146  	}
   147  
   148  	targets, err := m.resolveTargetsFromArgs(exports, targetName)
   149  
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	for idx, target := range targets {
   155  		statushooks.SetStatus(ctx, fmt.Sprintf("Exporting %d of %d", idx+1, len(targets)))
   156  		if msg, err = target.Export(ctx, source); err != nil {
   157  			errors = append(errors, err)
   158  		} else {
   159  			expLocation = append(expLocation, msg)
   160  		}
   161  	}
   162  	return expLocation, error_helpers.CombineErrors(errors...)
   163  }
   164  
   165  // HasNamedExport returns true if any of the export arguments has a filename (--export=file.json) instead of the format name (--export=json)
   166  // panics if a target is not valid
   167  func (m *Manager) HasNamedExport(exports []string) bool {
   168  	for _, export := range exports {
   169  		target, err := m.getExportTarget(export, "dummy_exec_name")
   170  		error_helpers.FailOnError(err)
   171  		if target.isNamedTarget {
   172  			return true
   173  		}
   174  	}
   175  	return false
   176  }
   177  
   178  func (m *Manager) ValidateExportFormat(exports []string) error {
   179  	var invalidFormats []string
   180  	var targets []*Target
   181  	for _, export := range exports {
   182  		target, err := m.getExportTarget(export, "dummy_exec_name")
   183  		if err != nil {
   184  			invalidFormats = append(invalidFormats, export)
   185  		}
   186  		targets = append(targets, target)
   187  	}
   188  	if invalidCount := len(invalidFormats); invalidCount > 0 {
   189  		return fmt.Errorf("invalid export %s: '%s'", utils.Pluralize("format", invalidCount), strings.Join(invalidFormats, "','"))
   190  	}
   191  	// verify all are either named or unnamed but not both
   192  	hasNamed := slices.ContainsFunc(targets, func(t *Target) bool { return t.isNamedTarget })
   193  	hasUnnamed := slices.ContainsFunc(targets, func(t *Target) bool { return !t.isNamedTarget })
   194  
   195  	if hasNamed && hasUnnamed {
   196  		return sperr.New("combination of named and unnamed exports is not supported")
   197  	}
   198  
   199  	return nil
   200  }