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 }