package main import ( "fmt" "log" "net/url" "os" "strings" "github.com/influxdata/influx-cli/v2/clients/apply" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" "github.com/mattn/go-isatty" "github.com/urfave/cli" ) func newApplyCmd() cli.Command { var params struct { orgId string orgName string stackId string inPaths cli.StringSlice inUrls cli.StringSlice recursive bool encoding apply.TemplateEncoding noColor bool noTableBorders bool quiet bool force string secrets cli.StringSlice envVars cli.StringSlice filters cli.StringSlice } return cli.Command{ Name: "apply", Usage: "Apply a template to manage resources", Description: `The apply command applies InfluxDB template(s). Use the command to create new resources via a declarative template. The apply command can consume templates via file(s), url(s), stdin, or any combination of the 3. Each run of the apply command ensures that all templates applied are applied in unison as a transaction. If any unexpected errors are discovered then all side effects are rolled back. Examples: # Apply a template via a file influx apply -f $PATH_TO_TEMPLATE/template.json # Apply a stack that has associated templates. In this example the stack has a remote # template associated with it. influx apply --stack-id $STACK_ID # Apply a template associated with a stack. Stacks make template application idempotent. influx apply -f $PATH_TO_TEMPLATE/template.json --stack-id $STACK_ID # Apply multiple template files together (mix of yaml and json) influx apply \ -f $PATH_TO_TEMPLATE/template_1.json \ -f $PATH_TO_TEMPLATE/template_2.yml # Apply a template from a url influx apply -u https://raw.githubusercontent.com/influxdata/community-templates/master/docker/docker.yml # Apply a template from STDIN cat $TEMPLATE.json | influx apply --encoding json # Applying a directory of templates, takes everything from provided directory influx apply -f $PATH_TO_TEMPLATE_DIR # Applying a directory of templates, recursively descending into child directories influx apply -R -f $PATH_TO_TEMPLATE_DIR # Applying directories from many sources, file and URL influx apply -f $PATH_TO_TEMPLATE/template.yml -f $URL_TO_TEMPLATE # Applying a template with actions to skip resources applied. The # following example skips all buckets and the dashboard whose # metadata.name field matches the provided $DASHBOARD_TMPL_NAME. # format for filters: # --filter=kind=Bucket # --filter=resource=Label:$Label_TMPL_NAME influx apply \ -f $PATH_TO_TEMPLATE/template.yml \ --filter kind=Bucket \ --filter resource=Dashboard:$DASHBOARD_TMPL_NAME For information about finding and using InfluxDB templates, see https://docs.influxdata.com/influxdb/latest/reference/cli/influx/apply/. For more templates created by the community, see https://github.com/influxdata/community-templates. `, 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-id", Usage: "Stack ID to associate with template application", Destination: ¶ms.stackId, }, &cli.StringSliceFlag{ Name: "file, f", Usage: "Path to template file; Supports file paths or (deprecated) HTTP(S) URLs", TakesFile: true, Value: ¶ms.inPaths, }, &cli.StringSliceFlag{ Name: "template-url, u", Usage: "HTTP(S) URL to template file", Value: ¶ms.inUrls, }, &cli.BoolFlag{ Name: "recurse, R", Usage: "Process the directory used in -f, --file recursively. Useful when you want to manage related templates organized within the same directory.", Destination: ¶ms.recursive, }, &cli.GenericFlag{ Name: "encoding, e", Usage: "Encoding for the input stream. If a file is provided will gather encoding type from file extension. If extension provided will override.", Value: ¶ms.encoding, }, &cli.BoolFlag{ Name: "disable-color", Usage: "Disable color in output", Destination: ¶ms.noColor, }, &cli.BoolFlag{ Name: "disable-table-borders", Usage: "Disable table borders", Destination: ¶ms.noTableBorders, }, &cli.BoolFlag{ Name: "quiet, q", Usage: "Disable output printing", Destination: ¶ms.quiet, }, &cli.StringFlag{ Name: "force", Usage: "Set to 'true' to skip confirmation before applying changes. Set to 'conflict' to skip confirmation and overwrite existing resources", Destination: ¶ms.force, }, &cli.StringSliceFlag{ Name: "secret", Usage: "Secrets to provide alongside the template; format --secret SECRET_KEY=SECRET_VALUE --secret SECRET_KEY_2=SECRET_VALUE_2", Value: ¶ms.secrets, }, &cli.StringSliceFlag{ Name: "env-ref", Usage: "Environment references to provide alongside the template; format --env-ref REF_KEY=REF_VALUE --env-ref REF_KEY_2=REF_VALUE_2", Value: ¶ms.envVars, }, &cli.StringSliceFlag{ Name: "filter", Usage: "Resources to skip when applying the template. Filter out by `kind` or by `resource`", Value: ¶ms.filters, }, ), Before: middleware.WithBeforeFns(withCli(), withApi(true)), Action: func(ctx *cli.Context) error { parsedParams := apply.Params{ OrgId: params.orgId, OrgName: params.orgName, StackId: params.stackId, Recursive: params.recursive, Quiet: params.quiet, RenderTableBorders: !params.noTableBorders, RenderTableColors: !params.noColor, Secrets: make(map[string]string, len(params.secrets.Value())), EnvVars: make(map[string]string, len(params.envVars.Value())), Filters: make([]apply.ResourceFilter, len(params.filters.Value())), } // Collect all the sources the CLI needs to read templates from. var deprecationShown bool for _, in := range params.inPaths.Value() { // Heuristic to guess what's a URL and what's a local file. // TODO: Remove this once we stop supporting URLs in the --file arg. u, err := url.Parse(in) if err != nil { return fmt.Errorf("failed to parse input path %q: %w", in, err) } if strings.HasPrefix(u.Scheme, "http") { if !deprecationShown { log.Println("WARN: Passing URLs via -f/--file is deprecated, please use -u/--template-url instead") deprecationShown = true } parsedParams.Sources = append(parsedParams.Sources, apply.SourceFromURL(u, params.encoding)) } else { fileSources, err := apply.SourcesFromPath(in, params.recursive, params.encoding) if err != nil { return err } parsedParams.Sources = append(parsedParams.Sources, fileSources...) } } for _, in := range params.inUrls.Value() { u, err := url.Parse(in) if err != nil { return fmt.Errorf("failed to parse input URL %q: %w", in, err) } parsedParams.Sources = append(parsedParams.Sources, apply.SourceFromURL(u, params.encoding)) } if !isatty.IsTerminal(os.Stdin.Fd()) { parsedParams.Sources = append(parsedParams.Sources, apply.SourceFromReader(os.Stdin, params.encoding)) } // Parse env and secret values. for _, e := range params.envVars.Value() { pieces := strings.Split(e, "=") if len(pieces) != 2 { return fmt.Errorf("env-ref %q has invalid format, must be `REF_KEY=REF_VALUE`", e) } parsedParams.EnvVars[pieces[0]] = pieces[1] } for _, s := range params.secrets.Value() { pieces := strings.Split(s, "=") if len(pieces) != 2 { return fmt.Errorf("secret %q has invalid format, must be `SECRET_KEY=SECRET_VALUE`", s) } parsedParams.Secrets[pieces[0]] = pieces[1] } // Parse filters. for i, f := range params.filters.Value() { pieces := strings.Split(f, "=") if len(pieces) != 2 { return fmt.Errorf("filter %q has invalid format, expected `resource=KIND:NAME` or `kind=KIND`", f) } key, val := pieces[0], pieces[1] switch strings.ToLower(key) { case "kind": parsedParams.Filters[i] = apply.ResourceFilter{Kind: val} case "resource": valPieces := strings.Split(val, ":") if len(valPieces) != 2 { return fmt.Errorf("resource filter %q has invalid format, expected `resource=KIND:NAME", val) } kind, name := valPieces[0], valPieces[1] parsedParams.Filters[i] = apply.ResourceFilter{Kind: kind, Name: &name} default: return fmt.Errorf("invalid filter type %q, supported values are `resource` and `kind`", key) } } // Parse our strange way of passing 'force' switch params.force { case "conflict": parsedParams.Force = true parsedParams.OverwriteConflicts = true case "true": parsedParams.Force = true default: } api := getAPI(ctx) client := apply.Client{ CLI: getCLI(ctx), TemplatesApi: api.TemplatesApi, OrganizationsApi: api.OrganizationsApi, } return client.Apply(getContext(ctx), &parsedParams) }, } }