
* fix: template apply uses better diff checking Previously, we did a DeepEqual of all the returned data about each changed entity, but due to our template overrides that is not actually all the information available for each entity. So we marked trivial things as 'conflicts' (e.g. telegraf config ID's 'changing' from the empty string to the real, current value) while not catching important conflicts like flux script changes in checks and tasks. Changes to make things more straightforward: * Change the --force behaviour to be more similar to `apt install`, where even in non-interactive mode `--force yes` is required to bypass the prompt to apply. * Before, there were two stages of diff checking - once to print diffs, and once after the 'Yes/No' prompt. If any conflicts were detected in the second check, the user got an inscrutable error message that did not highlight what the difference was or how to force it to apply. `--force conflict` was required to avoid this error. Instead, we now have simplified to `--force yes` to bypass the 'Yes/No' prompt, and we never do a second stage of diff checking. * Because we do not currently properly account for more complicated diffs (e.g. flux tasks), we now assume in the diff printing that every object has changes, except for Labels, Buckets, Variables, and Label Mappings. This could be improved in the future. * fix: when statestatus is 'remove', mark as removed
268 lines
9.3 KiB
Go
268 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/influxdata/influx-cli/v2/clients"
|
|
"github.com/influxdata/influx-cli/v2/clients/apply"
|
|
"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"
|
|
)
|
|
|
|
func newApplyCmd() cli.Command {
|
|
var params struct {
|
|
orgParams clients.OrgParams
|
|
stackId string
|
|
inPaths cli.StringSlice
|
|
inUrls cli.StringSlice
|
|
recursive bool
|
|
encoding template.Encoding
|
|
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(
|
|
append(commonFlags(), getOrgFlags(¶ms.orgParams)...),
|
|
&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 'yes' to skip confirmation before applying changes (recommended for non-interactive scripts).",
|
|
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), middleware.NoArgs),
|
|
Action: func(ctx *cli.Context) error {
|
|
parsedParams := apply.Params{
|
|
OrgParams: params.orgParams,
|
|
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, template.SourceFromURL(u, params.encoding))
|
|
} else {
|
|
fileSources, err := template.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, template.SourceFromURL(u, params.encoding))
|
|
}
|
|
if !isatty.IsTerminal(os.Stdin.Fd()) {
|
|
parsedParams.Sources = append(parsedParams.Sources, template.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 "":
|
|
// no force
|
|
case "conflict":
|
|
log.Println("WARN: Passing '--force conflict' is deprecated, assuming '--force yes'")
|
|
parsedParams.Force = true
|
|
case "true":
|
|
log.Println("WARN: Passing '--force true' is deprecated, assuming '--force yes'")
|
|
parsedParams.Force = true
|
|
case "yes":
|
|
parsedParams.Force = true
|
|
default:
|
|
return fmt.Errorf("invalid argument '--force %s', should use '--force yes'", params.force)
|
|
}
|
|
|
|
api := getAPI(ctx)
|
|
client := apply.Client{
|
|
CLI: getCLI(ctx),
|
|
TemplatesApi: api.TemplatesApi,
|
|
OrganizationsApi: api.OrganizationsApi,
|
|
}
|
|
|
|
return client.Apply(getContext(ctx), &parsedParams)
|
|
},
|
|
}
|
|
}
|