diff --git a/clients/export/export.go b/clients/export/export.go index 56ab65a..e23c4ac 100644 --- a/clients/export/export.go +++ b/clients/export/export.go @@ -6,6 +6,7 @@ import ( "github.com/influxdata/influx-cli/v2/api" "github.com/influxdata/influx-cli/v2/clients" + "github.com/influxdata/influx-cli/v2/pkg/template" ) type Client struct { @@ -15,7 +16,7 @@ type Client struct { } type Params struct { - OutParams + template.OutParams StackId string IdsPerType map[string][]string @@ -52,14 +53,14 @@ func (c Client) Export(ctx context.Context, params *Params) error { if err != nil { return fmt.Errorf("failed to export template: %w", err) } - if err := params.OutParams.writeTemplate(tmpl); err != nil { + if err := params.OutParams.WriteTemplate(tmpl); err != nil { return fmt.Errorf("failed to write exported template: %w", err) } return nil } type AllParams struct { - OutParams + template.OutParams OrgId string OrgName string @@ -105,14 +106,14 @@ func (c Client) ExportAll(ctx context.Context, params *AllParams) error { if err != nil { return fmt.Errorf("failed to export template: %w", err) } - if err := params.OutParams.writeTemplate(tmpl); err != nil { + if err := params.OutParams.WriteTemplate(tmpl); err != nil { return fmt.Errorf("failed to write exported template: %w", err) } return nil } type StackParams struct { - OutParams + template.OutParams StackId string } @@ -127,7 +128,7 @@ func (c Client) ExportStack(ctx context.Context, params *StackParams) error { if err != nil { return fmt.Errorf("failed to export stack %q: %w", params.StackId, err) } - if err := params.OutParams.writeTemplate(tmpl); err != nil { + if err := params.OutParams.WriteTemplate(tmpl); err != nil { return fmt.Errorf("failed to write exported template: %w", err) } diff --git a/clients/export/out.go b/clients/export/out.go deleted file mode 100644 index 9680484..0000000 --- a/clients/export/out.go +++ /dev/null @@ -1,41 +0,0 @@ -package export - -import ( - "encoding/json" - "fmt" - "io" - - "github.com/influxdata/influx-cli/v2/api" - "gopkg.in/yaml.v3" -) - -type OutEncoding int - -const ( - YamlEncoding OutEncoding = iota - JsonEncoding -) - -type OutParams struct { - Out io.Writer - Encoding OutEncoding -} - -func (o OutParams) writeTemplate(template []api.TemplateEntry) error { - switch o.Encoding { - case JsonEncoding: - enc := json.NewEncoder(o.Out) - enc.SetIndent("", "\t") - return enc.Encode(template) - case YamlEncoding: - enc := yaml.NewEncoder(o.Out) - for _, entry := range template { - if err := enc.Encode(entry); err != nil { - return err - } - } - default: - return fmt.Errorf("encoding %q is not recognized", o.Encoding) - } - return nil -} diff --git a/clients/stacks/stacks.go b/clients/stacks/stacks.go new file mode 100644 index 0000000..446070f --- /dev/null +++ b/clients/stacks/stacks.go @@ -0,0 +1,265 @@ +package stacks + +import ( + "context" + "fmt" + "sort" + + "github.com/influxdata/influx-cli/v2/api" + "github.com/influxdata/influx-cli/v2/clients" + "github.com/influxdata/influx-cli/v2/pkg/template" +) + +type Client struct { + clients.CLI + api.StacksApi + api.OrganizationsApi + api.TemplatesApi +} + +type ListParams struct { + OrgId string + OrgName string + + StackIds []string + StackNames []string +} + +func (c Client) List(ctx context.Context, params *ListParams) error { + if params.OrgId == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { + return clients.ErrMustSpecifyOrg + } + + orgId := params.OrgId + if orgId == "" { + orgName := params.OrgName + if orgName == "" { + orgName = c.ActiveConfig.Org + } + res, err := c.GetOrgs(ctx).Org(orgName).Execute() + if err != nil { + return fmt.Errorf("failed to lookup org with name %q: %w", orgName, err) + } + if len(res.GetOrgs()) == 0 { + return fmt.Errorf("no organization with name %q: %w", orgName, err) + } + orgId = res.GetOrgs()[0].GetId() + } + + res, err := c.ListStacks(ctx).OrgID(orgId).Name(params.StackNames).StackID(params.StackIds).Execute() + if err != nil { + return fmt.Errorf("failed to list stacks: %w", err) + } + + return c.printStacks(stackPrintOptions{stacks: &res}) +} + +type InitParams struct { + OrgId string + OrgName string + + Name string + Description string + URLs []string +} + +func (c Client) Init(ctx context.Context, params *InitParams) error { + if params.OrgId == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { + return clients.ErrMustSpecifyOrg + } + + orgId := params.OrgId + if orgId == "" { + orgName := params.OrgName + if orgName == "" { + orgName = c.ActiveConfig.Org + } + res, err := c.GetOrgs(ctx).Org(orgName).Execute() + if err != nil { + return fmt.Errorf("failed to lookup org with name %q: %w", orgName, err) + } + if len(res.GetOrgs()) == 0 { + return fmt.Errorf("no organization with name %q: %w", orgName, err) + } + orgId = res.GetOrgs()[0].GetId() + } + + req := api.StackPostRequest{ + OrgID: orgId, + Name: params.Name, + Urls: params.URLs, + } + if params.Description != "" { + req.Description = ¶ms.Description + } + + stack, err := c.CreateStack(ctx).StackPostRequest(req).Execute() + if err != nil { + return fmt.Errorf("failed to create stack %q: %w", params.Name, err) + } + + return c.printStacks(stackPrintOptions{stack: &stack}) +} + +type RemoveParams struct { + OrgId string + OrgName string + + Ids []string + Force bool +} + +func (c Client) Remove(ctx context.Context, params *RemoveParams) error { + if params.OrgId == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { + return clients.ErrMustSpecifyOrg + } + + orgId := params.OrgId + if orgId == "" { + orgName := params.OrgName + if orgName == "" { + orgName = c.ActiveConfig.Org + } + res, err := c.GetOrgs(ctx).Org(orgName).Execute() + if err != nil { + return fmt.Errorf("failed to lookup org with name %q: %w", orgName, err) + } + if len(res.GetOrgs()) == 0 { + return fmt.Errorf("no organization with name %q: %w", orgName, err) + } + orgId = res.GetOrgs()[0].GetId() + } + + stacks, err := c.ListStacks(ctx).OrgID(orgId).StackID(params.Ids).Execute() + if err != nil { + return fmt.Errorf("failed to look up stacks: %w", err) + } + + for _, stack := range stacks.Stacks { + if err := c.printStacks(stackPrintOptions{stack: &stack}); err != nil { + return err + } + if !params.Force && !c.StdIO.GetConfirm(fmt.Sprintf("Confirm removal of the stack[%s] and all associated resources", stack.Id)) { + continue + } + if err := c.DeleteStack(ctx, stack.Id).OrgID(orgId).Execute(); err != nil { + return fmt.Errorf("failed to delete stack %q: %w", stack.Id, err) + } + } + + return nil +} + +type AddedResource struct { + Kind string + Id string +} + +type UpdateParams struct { + Id string + + Name *string + Description *string + URLs []string + AddedResources []AddedResource + + template.OutParams +} + +func (c Client) Update(ctx context.Context, params *UpdateParams) error { + req := api.StackPatchRequest{ + Name: params.Name, + Description: params.Description, + TemplateURLs: params.URLs, + AdditionalResources: make([]api.StackPatchRequestResource, len(params.AddedResources)), + } + for i, r := range params.AddedResources { + req.AdditionalResources[i] = api.StackPatchRequestResource{ + ResourceID: r.Id, + Kind: r.Kind, + } + } + + stack, err := c.UpdateStack(ctx, params.Id).StackPatchRequest(req).Execute() + if err != nil { + return fmt.Errorf("failed to udpate stack %q: %w", params.Id, err) + } + if err := c.printStacks(stackPrintOptions{stack: &stack}); err != nil { + return err + } + + // Can skip exporting the updated template if no resources were added. + if len(params.AddedResources) == 0 { + return nil + } + + if !c.StdIO.GetConfirm(`Your stack now differs from your template. Applying an outdated template will revert these updates. +Export a new template with these updates to prevent accidental changes?`) { + return nil + } + + exportReq := api.TemplateExport{StackID: &stack.Id} + tmpl, err := c.ExportTemplate(ctx).TemplateExport(exportReq).Execute() + if err != nil { + return fmt.Errorf("failed to export stack %q: %w", stack.Id, err) + } + if err := params.OutParams.WriteTemplate(tmpl); err != nil { + return fmt.Errorf("failed to write exported template: %w", err) + } + return nil +} + +type stackPrintOptions struct { + stack *api.Stack + stacks *api.Stacks +} + +func (c Client) printStacks(options stackPrintOptions) error { + if c.PrintAsJSON { + var v interface{} + if options.stack != nil { + v = options.stack + } else { + v = options.stacks + } + return c.PrintJSON(v) + } + + headers := []string{"ID", "OrgID", "Active", "Name", "Description", "Num Resources", "Sources", "URLs", "Created At", "Updated At"} + var stacks []api.Stack + if options.stacks != nil { + stacks = options.stacks.Stacks + } + if options.stack != nil { + stacks = append(stacks, *options.stack) + } + + var rows []map[string]interface{} + for _, s := range stacks { + var latestEvent api.StackEvent + if len(s.Events) > 0 { + sort.Slice(s.Events, func(i, j int) bool { + return s.Events[i].UpdatedAt.Before(s.Events[j].UpdatedAt) + }) + latestEvent = s.Events[len(s.Events)-1] + } + var desc string + if latestEvent.Description != nil { + desc = *latestEvent.Description + } + row := map[string]interface{}{ + "ID": s.Id, + "OrgID": s.OrgID, + "Active": latestEvent.EventType != "uninstall", + "Name": latestEvent.Name, + "Description": desc, + "Num Resources": len(latestEvent.Resources), + "Sources": latestEvent.Sources, + "URLs": latestEvent.Urls, + "Created At": s.CreatedAt, + "Updated At": latestEvent.UpdatedAt, + } + rows = append(rows, row) + } + return c.PrintTable(headers, rows...) +} diff --git a/cmd/influx/export.go b/cmd/influx/export.go index 3fc9ca7..42dce7f 100644 --- a/cmd/influx/export.go +++ b/cmd/influx/export.go @@ -4,11 +4,11 @@ import ( "fmt" "io" "os" - "path/filepath" "strings" "github.com/influxdata/influx-cli/v2/clients/export" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" + "github.com/influxdata/influx-cli/v2/pkg/template" "github.com/mattn/go-isatty" "github.com/urfave/cli" ) @@ -205,7 +205,8 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/`, }, } - outParams, closer, err := parseOutParams(params.out) + cli := getCLI(ctx) + outParams, closer, err := template.ParseOutParams(params.out, cli.StdIO) if closer != nil { defer closer() } @@ -241,7 +242,7 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/`, } client := export.Client{ - CLI: getCLI(ctx), + CLI: cli, TemplatesApi: getAPI(ctx).TemplatesApi, } return client.Export(getContext(ctx), &parsedParams) @@ -338,7 +339,8 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/all/ } } - outParams, closer, err := parseOutParams(params.out) + cli := getCLI(ctx) + outParams, closer, err := template.ParseOutParams(params.out, cli.StdIO) if closer != nil { defer closer() } @@ -349,7 +351,7 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/all/ apiClient := getAPI(ctx) client := export.Client{ - CLI: getCLI(ctx), + CLI: cli, TemplatesApi: apiClient.TemplatesApi, OrganizationsApi: apiClient.OrganizationsApi, } @@ -396,7 +398,8 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/stack/ StackId: ctx.Args().Get(0), } - outParams, closer, err := parseOutParams(params.out) + cli := getCLI(ctx) + outParams, closer, err := template.ParseOutParams(params.out, cli.StdIO) if closer != nil { defer closer() } @@ -407,7 +410,7 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/stack/ apiClient := getAPI(ctx) client := export.Client{ - CLI: getCLI(ctx), + CLI: cli, TemplatesApi: apiClient.TemplatesApi, OrganizationsApi: apiClient.OrganizationsApi, } @@ -422,23 +425,3 @@ func splitNonEmpty(s string) []string { } return strings.Split(s, ",") } - -func parseOutParams(outPath string) (export.OutParams, func(), error) { - if outPath == "" { - return export.OutParams{Out: os.Stdout, Encoding: export.YamlEncoding}, nil, nil - } - - f, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return export.OutParams{}, nil, fmt.Errorf("failed to open output path %q: %w", outPath, err) - } - params := export.OutParams{Out: f} - switch filepath.Ext(outPath) { - case ".json": - params.Encoding = export.JsonEncoding - default: - params.Encoding = export.YamlEncoding - } - - return params, func() { _ = f.Close() }, nil -} diff --git a/cmd/influx/main.go b/cmd/influx/main.go index 4aba758..de10b03 100644 --- a/cmd/influx/main.go +++ b/cmd/influx/main.go @@ -51,6 +51,7 @@ var app = cli.App{ newV1SubCommand(), newAuthCommand(), newApplyCmd(), + newStacksCmd(), }, Before: withContext(), } diff --git a/cmd/influx/stacks.go b/cmd/influx/stacks.go new file mode 100644 index 0000000..a02c47d --- /dev/null +++ b/cmd/influx/stacks.go @@ -0,0 +1,300 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/influxdata/influx-cli/v2/clients/stacks" + "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" + "github.com/influxdata/influx-cli/v2/pkg/template" + "github.com/urfave/cli" +) + +func newStacksCmd() cli.Command { + var params stacks.ListParams + return cli.Command{ + Name: "stacks", + Usage: "List stack(s) and associated templates. Subcommands manage stacks.", + Description: `List stack(s) and associated templates. Subcommands manage stacks. + +Examples: + # list all known stacks + influx stacks + + # list stacks filtered by stack name + # output here are stacks that have match at least 1 name provided + influx stacks --stack-name=$STACK_NAME_1 --stack-name=$STACK_NAME_2 + + # list stacks filtered by stack id + # output here are stacks that have match at least 1 ids provided + influx stacks --stack-id=$STACK_ID_1 --stack-id=$STACK_ID_2 + + # list stacks filtered by stack id or stack name + # output here are stacks that have match the id provided or + # matches of the name provided + influx stacks --stack-id=$STACK_ID --stack-name=$STACK_NAME + +For information about Stacks and how they integrate with InfluxDB templates, see +https://docs.influxdata.com/influxdb/latest/reference/cli/influx/stacks/`, + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.StringFlag{ + Name: "org-id", + Usage: "The ID of the organization", + EnvVar: "INFLUX_ORG_ID", + Destination: ¶ms.OrgId, + }, + &cli.StringFlag{ + Name: "org, o", + Usage: "The name of the organization", + EnvVar: "INFLUX_ORG", + Destination: ¶ms.OrgName, + }, + &cli.StringSliceFlag{ + Name: "stack-id", + Usage: "Stack ID to filter by", + }, + &cli.StringSliceFlag{ + Name: "stack-name", + Usage: "Stack name to filter by", + }, + ), + Subcommands: []cli.Command{ + newStacksInitCmd(), + newStacksRemoveCmd(), + newStacksUpdateCmd(), + }, + Action: func(ctx *cli.Context) error { + params.StackIds = ctx.StringSlice("stack-id") + params.StackNames = ctx.StringSlice("stack-name") + api := getAPI(ctx) + client := stacks.Client{ + CLI: getCLI(ctx), + StacksApi: api.StacksApi, + OrganizationsApi: api.OrganizationsApi, + } + return client.List(getContext(ctx), ¶ms) + }, + } +} + +func newStacksInitCmd() cli.Command { + var params stacks.InitParams + return cli.Command{ + Name: "init", + Usage: "Initialize a stack", + Description: `The stack init command creates a new stack to associated templates with. A +stack is used to make applying templates idempotent. When you apply a template +and associate it with a stack, the stack can manage the created/updated resources +from the template back to the platform. This enables a multitude of useful features. +Any associated template urls will be applied when applying templates via a stack. + +Examples: + # Initialize a stack with a name and description + influx stacks init -n $STACK_NAME -d $STACK_DESCRIPTION + + # Initialize a stack with a name and urls to associate with stack. + influx stacks init -n $STACK_NAME -u $PATH_TO_TEMPLATE + +For information about how stacks work with InfluxDB templates, see +https://docs.influxdata.com/influxdb/latest/reference/cli/influx/stacks/ +and +https://docs.influxdata.com/influxdb/latest/reference/cli/influx/stacks/init/`, + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.StringFlag{ + Name: "org-id", + Usage: "The ID of the organization", + EnvVar: "INFLUX_ORG_ID", + Destination: ¶ms.OrgId, + }, + &cli.StringFlag{ + Name: "org, o", + Usage: "The name of the organization", + EnvVar: "INFLUX_ORG", + Destination: ¶ms.OrgName, + }, + &cli.StringFlag{ + Name: "stack-name, n", + Usage: "Name given to created stack", + Destination: ¶ms.Name, + }, + &cli.StringFlag{ + Name: "stack-description, d", + Usage: "Description given to created stack", + Destination: ¶ms.Description, + }, + &cli.StringSliceFlag{ + Name: "template-url, u", + Usage: "Template urls to associate with new stack", + }, + ), + Action: func(ctx *cli.Context) error { + params.URLs = ctx.StringSlice("template-url") + api := getAPI(ctx) + client := stacks.Client{ + CLI: getCLI(ctx), + StacksApi: api.StacksApi, + OrganizationsApi: api.OrganizationsApi, + } + return client.Init(getContext(ctx), ¶ms) + }, + } +} + +func newStacksRemoveCmd() cli.Command { + var params stacks.RemoveParams + return cli.Command{ + Name: "rm", + Aliases: []string{"remove", "uninstall"}, + Usage: "Remove a stack(s) and all associated resources", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.StringFlag{ + Name: "org-id", + Usage: "The ID of the organization", + EnvVar: "INFLUX_ORG_ID", + Destination: ¶ms.OrgId, + }, + &cli.StringFlag{ + Name: "org, o", + Usage: "The name of the organization", + EnvVar: "INFLUX_ORG", + Destination: ¶ms.OrgName, + }, + &cli.StringSliceFlag{ + Name: "stack-id", + Usage: "Stack IDs to be removed", + Required: true, + }, + &cli.BoolFlag{ + Name: "force", + Usage: "Remove stack without confirmation prompt", + Destination: ¶ms.Force, + }, + ), + Action: func(ctx *cli.Context) error { + params.Ids = ctx.StringSlice("stack-id") + api := getAPI(ctx) + client := stacks.Client{ + CLI: getCLI(ctx), + StacksApi: api.StacksApi, + OrganizationsApi: api.OrganizationsApi, + } + return client.Remove(getContext(ctx), ¶ms) + }, + } +} + +func newStacksUpdateCmd() cli.Command { + var params stacks.UpdateParams + return cli.Command{ + Name: "update", + Usage: "Update a stack", + Description: `The stack update command updates a stack. + +Examples: + # Update a stack with a name and description + influx stacks update -i $STACK_ID -n $STACK_NAME -d $STACK_DESCRIPTION + + # Update a stack with a name and urls to associate with stack. + influx stacks update --stack-id $STACK_ID --stack-name $STACK_NAME --template-url $PATH_TO_TEMPLATE + + # Update stack with new resources to manage + influx stacks update \ + --stack-id $STACK_ID \ + --addResource=Bucket=$BUCKET_ID \ + --addResource=Dashboard=$DASH_ID + + # Update stack with new resources to manage and export stack + # as a template + influx stacks update \ + --stack-id $STACK_ID \ + --addResource=Bucket=$BUCKET_ID \ + --export-file /path/to/file.yml + +For information about how stacks work with InfluxDB templates, see +https://docs.influxdata.com/influxdb/latest/reference/cli/influx/stacks/ +and +https://docs.influxdata.com/influxdb/latest/reference/cli/influx/stacks/update/ +`, + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.StringFlag{ + Name: "stack-id, i", + Usage: "ID of stack", + Destination: ¶ms.Id, + Required: true, + }, + cli.StringFlag{ + Name: "stack-name, n", + Usage: "New name for the stack", + }, + cli.StringFlag{ + Name: "stack-description, d", + Usage: "New description for the stack", + }, + cli.StringSliceFlag{ + Name: "template-url, u", + Usage: "New template URLs to associate with the stack", + }, + cli.StringSliceFlag{ + Name: "addResource", + Usage: "Additional resources to associate with the stack", + }, + cli.StringFlag{ + Name: "export-file, f", + Usage: "Destination for exported template", + TakesFile: true, + }, + ), + Action: func(ctx *cli.Context) error { + if ctx.IsSet("stack-name") { + name := ctx.String("stack-name") + params.Name = &name + } + if ctx.IsSet("stack-description") { + desc := ctx.String("stack-description") + params.Description = &desc + } + if ctx.IsSet("template-url") { + urls := ctx.StringSlice("template-url") + params.URLs = urls + } + + rawResources := ctx.StringSlice("addResource") + for _, res := range rawResources { + pieces := strings.Split(res, "=") + if len(pieces) != 2 { + return fmt.Errorf("invalid resource specification %q, must have format `KIND=ID`", res) + } + params.AddedResources = append(params.AddedResources, stacks.AddedResource{ + Kind: pieces[0], + Id: pieces[1], + }) + } + + cli := getCLI(ctx) + outParams, closer, err := template.ParseOutParams(ctx.String("export-file"), cli.StdIO) + if closer != nil { + defer closer() + } + if err != nil { + return err + } + params.OutParams = outParams + + api := getAPI(ctx) + client := stacks.Client{ + CLI: cli, + StacksApi: api.StacksApi, + TemplatesApi: api.TemplatesApi, + } + return client.Update(getContext(ctx), ¶ms) + }, + } +} diff --git a/pkg/template/out.go b/pkg/template/out.go new file mode 100644 index 0000000..fe60b45 --- /dev/null +++ b/pkg/template/out.go @@ -0,0 +1,63 @@ +package template + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/influxdata/influx-cli/v2/api" + "gopkg.in/yaml.v3" +) + +type OutEncoding int + +const ( + YamlEncoding OutEncoding = iota + JsonEncoding +) + +type OutParams struct { + Out io.Writer + Encoding OutEncoding +} + +func ParseOutParams(path string, fallback io.Writer) (OutParams, func(), error) { + if path == "" { + return OutParams{Out: fallback, Encoding: YamlEncoding}, nil, nil + } + + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return OutParams{}, nil, fmt.Errorf("failed to open output path %q: %w", path, err) + } + params := OutParams{Out: f} + switch filepath.Ext(path) { + case ".json": + params.Encoding = JsonEncoding + default: + params.Encoding = YamlEncoding + } + + return params, func() { _ = f.Close() }, nil +} + +func (o OutParams) WriteTemplate(template []api.TemplateEntry) error { + switch o.Encoding { + case JsonEncoding: + enc := json.NewEncoder(o.Out) + enc.SetIndent("", "\t") + return enc.Encode(template) + case YamlEncoding: + enc := yaml.NewEncoder(o.Out) + for _, entry := range template { + if err := enc.Encode(entry); err != nil { + return err + } + } + default: + return fmt.Errorf("encoding %q is not recognized", o.Encoding) + } + return nil +} diff --git a/pkg/template/out_test.go b/pkg/template/out_test.go new file mode 100644 index 0000000..62d7622 --- /dev/null +++ b/pkg/template/out_test.go @@ -0,0 +1,124 @@ +package template_test + +import ( + "bytes" + "encoding/json" + "io" + "os" + "path/filepath" + "testing" + + "github.com/influxdata/influx-cli/v2/api" + "github.com/influxdata/influx-cli/v2/pkg/template" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +var tmpls = []api.TemplateEntry{ + { + ApiVersion: "api1", + Kind: "Foo", + Metadata: api.TemplateEntryMetadata{ + Name: "foo", + }, + Spec: map[string]interface{}{ + "hello": "world", + "1 + 1 =": "2", + }, + }, + { + ApiVersion: "api1", + Kind: "Bar", + Metadata: api.TemplateEntryMetadata{ + Name: "bar", + }, + Spec: map[string]interface{}{ + "success?": "true", + }, + }, +} + +func TestOutParams(t *testing.T) { + t.Parallel() + + t.Run("json to file", func(t *testing.T) { + t.Parallel() + + tmp, err := os.MkdirTemp("", "") + require.NoError(t, err) + defer os.RemoveAll(tmp) + + out := filepath.Join(tmp, "test.json") + params, closer, err := template.ParseOutParams(out, nil) + require.NoError(t, err) + require.NotNil(t, closer) + defer closer() + + require.NoError(t, params.WriteTemplate(tmpls)) + contents, err := os.ReadFile(out) + require.NoError(t, err) + + var written []api.TemplateEntry + dec := json.NewDecoder(bytes.NewReader(contents)) + require.NoError(t, dec.Decode(&written)) + + require.Equal(t, tmpls, written) + }) + + t.Run("yaml to file", func(t *testing.T) { + t.Parallel() + + tmp, err := os.MkdirTemp("", "") + require.NoError(t, err) + defer os.RemoveAll(tmp) + + out := filepath.Join(tmp, "test.yaml") + params, closer, err := template.ParseOutParams(out, nil) + require.NoError(t, err) + require.NotNil(t, closer) + defer closer() + + require.NoError(t, params.WriteTemplate(tmpls)) + contents, err := os.ReadFile(out) + require.NoError(t, err) + + var written []api.TemplateEntry + dec := yaml.NewDecoder(bytes.NewReader(contents)) + for { + var e api.TemplateEntry + err := dec.Decode(&e) + if err == io.EOF { + break + } + require.NoError(t, err) + written = append(written, e) + } + + require.Equal(t, tmpls, written) + }) + + t.Run("yaml to buffer", func(t *testing.T) { + t.Parallel() + + out := bytes.Buffer{} + params, closer, err := template.ParseOutParams("", &out) + require.NoError(t, err) + require.Nil(t, closer) + + require.NoError(t, params.WriteTemplate(tmpls)) + + var written []api.TemplateEntry + dec := yaml.NewDecoder(&out) + for { + var e api.TemplateEntry + err := dec.Decode(&e) + if err == io.EOF { + break + } + require.NoError(t, err) + written = append(written, e) + } + + require.Equal(t, tmpls, written) + }) +}