diff --git a/internal/flags/templates.go b/internal/flags/templates.go index 8598b9ad..f8553201 100644 --- a/internal/flags/templates.go +++ b/internal/flags/templates.go @@ -31,3 +31,15 @@ type TemplateGetOptions struct { func (o *TemplateGetOptions) Flags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.All, "all", o.All, "Include all workflows") } + +type TemplateLoadOptions struct { + Type string + Group string + Include []string +} + +func (o *TemplateLoadOptions) Flags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Type, "type", o.Type, "Type of template to load (valid values are textfsm, native)") + cmd.Flags().StringVar(&o.Group, "group", o.Group, "Group to load templates into (only valid with --type=textfsm)") + cmd.Flags().StringArrayVar(&o.Include, "include", o.Include, "Include files matching pattern") +} diff --git a/internal/flags/templates_test.go b/internal/flags/templates_test.go index 6214b67f..96e2e86d 100644 --- a/internal/flags/templates_test.go +++ b/internal/flags/templates_test.go @@ -13,3 +13,7 @@ func TestTemplateCreateOptions(t *testing.T) { func TestTemplateGetOptions(t *testing.T) { checkFlags(t, &TemplateGetOptions{}, []string{"all"}) } + +func TestTemplateLoadOptions(t *testing.T) { + checkFlags(t, &TemplateLoadOptions{}, []string{"type", "group", "include"}) +} diff --git a/internal/handlers/descriptors/templates.yaml b/internal/handlers/descriptors/templates.yaml index 934330b2..d4f77602 100644 --- a/internal/handlers/descriptors/templates.yaml +++ b/internal/handlers/descriptors/templates.yaml @@ -74,3 +74,15 @@ import: group: automation-studio description: | Import a template + +load: + use: templates + group: automation-studio + description: | + Load templates + +dump: + use: templates + group: automation-studio + description: | + Dump all templates diff --git a/internal/handlers/runner.go b/internal/handlers/runner.go index 18fcccb5..3fe2677b 100644 --- a/internal/handlers/runner.go +++ b/internal/handlers/runner.go @@ -78,6 +78,10 @@ func withOptions(f *AssetHandlerFlags) CommandRunnerOption { c.Options = f.Import case "export": c.Options = f.Export + case "load": + c.Options = f.Load + case "dump": + c.Options = f.Dump } } } diff --git a/internal/handlers/templates.go b/internal/handlers/templates.go index 61eaeff9..5a23b1f0 100644 --- a/internal/handlers/templates.go +++ b/internal/handlers/templates.go @@ -16,6 +16,7 @@ func NewTemplateHandler(r Runtime, desc Descriptors) AssetHandler { &AssetHandlerFlags{ Create: &flags.TemplateCreateOptions{}, Get: &flags.TemplateGetOptions{}, + Load: &flags.TemplateLoadOptions{}, }, ) } diff --git a/internal/runners/importer.go b/internal/runners/importer.go index 875e125a..a584d36a 100644 --- a/internal/runners/importer.go +++ b/internal/runners/importer.go @@ -68,6 +68,10 @@ func importGetPathFromRequest(in Request) (string, error) { path := in.Args[0] + if dir, err := utils.IsDir(path); err != nil || !dir { + return "", fmt.Errorf("path `%s` is not a directory", path) + } + if in.Common.(flags.Gitter).GetRepository() != "" { r, err := importNewRepositoryFromRequest(in) if err != nil { diff --git a/internal/runners/loader.go b/internal/runners/loader.go index 4e831457..37b00ee9 100644 --- a/internal/runners/loader.go +++ b/internal/runners/loader.go @@ -9,9 +9,15 @@ import ( "os" "path/filepath" + "github.com/itential/ipctl/internal/utils" "github.com/itential/ipctl/pkg/logger" ) +type LoadOptions struct { + Include []string + Exclude []string +} + // loadAssets receives the Request argument and loads the assets from disk. func loadAssets(in Request) (map[string]interface{}, error) { logger.Trace() @@ -39,6 +45,33 @@ func loadAssets(in Request) (map[string]interface{}, error) { return data, nil } +func loadStringAssets(in Request, options LoadOptions) (map[string]interface{}, error) { + logger.Trace() + + path, err := importGetPathFromRequest(in) + if err != nil { + return nil, err + } + + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + var data = make(map[string]interface{}) + + for _, ele := range files { + res, err := utils.ReadStringFromFile(filepath.Join(path, ele.Name())) + if err != nil { + return nil, err + } + data[ele.Name()] = res + } + + return data, nil + +} + // loadUnmarshalAsset unmarshals the interface in argument to ptr func loadUnmarshalAsset(in interface{}, ptr any) error { logger.Trace() diff --git a/internal/runners/templates.go b/internal/runners/templates.go index 83317de0..753bfb06 100644 --- a/internal/runners/templates.go +++ b/internal/runners/templates.go @@ -7,9 +7,11 @@ package runners import ( "errors" "fmt" + "path/filepath" "strings" "github.com/itential/ipctl/internal/flags" + "github.com/itential/ipctl/internal/terminal" "github.com/itential/ipctl/pkg/client" "github.com/itential/ipctl/pkg/config" "github.com/itential/ipctl/pkg/logger" @@ -28,9 +30,11 @@ func NewTemplateRunner(c client.Client, cfg *config.Config) *TemplateRunner { } } -////////////////////////////////////////////////////////////////////////////// -// Reader Interface -// +/* +******************************************************************************* +Reader interface +******************************************************************************* +*/ // Get implements the `get command-templates` command func (r *TemplateRunner) Get(in Request) (*Response, error) { @@ -86,9 +90,11 @@ func (r *TemplateRunner) Describe(in Request) (*Response, error) { }, nil } -////////////////////////////////////////////////////////////////////////////// -// Writer Interface -// +/* +******************************************************************************* +Writer interface +******************************************************************************* +*/ // Create implements the `create template ...` command func (r *TemplateRunner) Create(in Request) (*Response, error) { @@ -154,6 +160,7 @@ func (r *TemplateRunner) Clear(in Request) (*Response, error) { } for _, ele := range elements { + terminal.Display("Deleting template `%s` (%s)", ele.Name, ele.Id) if err := r.service.Delete(ele.Id); err != nil { logger.Debug("failed to delete template `%s` (%s)", ele.Name, ele.Id) return nil, err @@ -161,13 +168,15 @@ func (r *TemplateRunner) Clear(in Request) (*Response, error) { } return &Response{ - Text: fmt.Sprintf("Deleted %v template(s)", len(elements)), + Text: fmt.Sprintf("\nDeleted %v template(s)", len(elements)), }, nil } -////////////////////////////////////////////////////////////////////////////// -// Copier Interface -// +/* +******************************************************************************* +Copier interface +******************************************************************************* +*/ func (r *TemplateRunner) Copy(in Request) (*Response, error) { logger.Trace() @@ -233,9 +242,11 @@ func (r *TemplateRunner) CopyTo(profile string, in any, replace bool) (any, erro } -////////////////////////////////////////////////////////////////////////////// -// Importer Interface -// +/* +******************************************************************************* +Importer interface +******************************************************************************* +*/ func (r *TemplateRunner) Import(in Request) (*Response, error) { logger.Trace() @@ -257,9 +268,11 @@ func (r *TemplateRunner) Import(in Request) (*Response, error) { }, nil } -////////////////////////////////////////////////////////////////////////////// -// Exporter Interface -// +/* +******************************************************************************* +Exporter interface +******************************************************************************* +*/ func (r *TemplateRunner) Export(in Request) (*Response, error) { logger.Trace() @@ -287,16 +300,123 @@ func (r *TemplateRunner) Export(in Request) (*Response, error) { }, nil } -////////////////////////////////////////////////////////////////////////////// -// Private functions -// +/* +******************************************************************************* +Dumper interface +******************************************************************************* +*/ + +// Dump implements the `dump templates...` command +func (r *TemplateRunner) Dump(in Request) (*Response, error) { + logger.Trace() + + res, err := r.service.GetAll() + if err != nil { + return nil, err + } + + var assets = map[string]interface{}{} + + for _, ele := range res { + if !strings.HasPrefix(ele.Name, "@") { + key := fmt.Sprintf("%s.template.json", ele.Name) + assets[key] = ele + } + } + + if err := dumpAssets(in, assets); err != nil { + return nil, err + } + + return &Response{ + Text: fmt.Sprintf("Dumped %v template(s)", len(assets)), + }, nil +} + +/* +******************************************************************************* +Loader interface +******************************************************************************* +*/ + +// Load implements the `load template ...` command +func (r *TemplateRunner) Load(in Request) (*Response, error) { + logger.Trace() + + options := in.Options.(*flags.TemplateLoadOptions) + + var elements map[string]interface{} + var err error + + if options.Type == "textfsm" { + elements, err = loadStringAssets(in, LoadOptions{}) + } else { + elements, err = loadAssets(in) + if err != nil { + return nil, err + } + } + + var loaded int + var skipped int + + for fn, ele := range elements { + var template services.Template + var err error + + if options.Type == "textfsm" || options.Type == "jinja2" { + name := strings.TrimSuffix(fn, filepath.Ext(fn)) + template = services.NewTemplate(name, "Imported", "", options.Type) + template.Template = ele.(string) + } else { + err = loadUnmarshalAsset(ele, &template) + if err != nil { + terminal.Display("Failed to load template from `%s`, skipping", fn) + skipped++ + } + } + + if err == nil { + if err := r.importTemplate(template, false); err != nil { + if !strings.HasPrefix(err.Error(), "template with name") { + return nil, err + } + terminal.Display("Skipping `%s`, template `%s` already exists", fn, template.Name) + skipped++ + } else { + terminal.Display("Loaded template `%s` successfully from `%s`", template.Name, fn) + loaded++ + } + } + } + + output := fmt.Sprintf("\nSuccessfully loaded %v and skipped %v files from `%s`", loaded, skipped, in.Args[0]) + + return &Response{ + Text: output, + }, nil + +} + +/* +******************************************************************************* +Private functions +******************************************************************************* +*/ func (r TemplateRunner) importTemplate(in services.Template, replace bool) error { logger.Trace() + logger.Debug("attempting to import template `%s`", in.Name) p, err := r.service.GetByName(in.Name) - if err == nil { + if err != nil { + if err.Error() != "template not found" { + return err + } + } + if p != nil { if replace { + logger.Debug("template exists, deleting it") r.service.Delete(p.Id) } else { return errors.New(fmt.Sprintf("template with name `%s` already exists, use `--replace` to overwrite", p.Name)) @@ -308,5 +428,7 @@ func (r TemplateRunner) importTemplate(in services.Template, replace bool) error return err } + logger.Debug("successfully imported template `%s`", in.Name) + return nil } diff --git a/internal/utils/path.go b/internal/utils/path.go index 2e30bd7e..ac8476e6 100644 --- a/internal/utils/path.go +++ b/internal/utils/path.go @@ -189,3 +189,12 @@ func EnsurePathExists(p string) error { } return nil } + +// IsDir will check the path argument to see if it is a directory or not. +func IsDir(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + return false, err // Return false if the path doesn't exist or there's another error + } + return info.IsDir(), nil +}