influxdb/cmd/influx/template.go

2144 lines
60 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/fatih/color"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/cmd/influx/internal"
internal2 "github.com/influxdata/influxdb/v2/cmd/internal"
ierror "github.com/influxdata/influxdb/v2/kit/errors"
"github.com/influxdata/influxdb/v2/pkger"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
input "github.com/tcnksm/go-input"
)
type templateSVCsFn func() (pkger.SVC, influxdb.OrganizationService, error)
func cmdApply(f *globalFlags, opts genericCLIOpts) *cobra.Command {
return newCmdPkgerBuilder(newPkgerSVC, f, opts).cmdApply()
}
func cmdExport(f *globalFlags, opts genericCLIOpts) *cobra.Command {
return newCmdPkgerBuilder(newPkgerSVC, f, opts).cmdExport()
}
func cmdStack(f *globalFlags, opts genericCLIOpts) *cobra.Command {
return newCmdPkgerBuilder(newPkgerSVC, f, opts).cmdStacks()
}
func cmdTemplate(f *globalFlags, opts genericCLIOpts) *cobra.Command {
return newCmdPkgerBuilder(newPkgerSVC, f, opts).cmdTemplate()
}
type cmdTemplateBuilder struct {
genericCLIOpts
*globalFlags
svcFn templateSVCsFn
encoding string
file string
files []string
filters []string
description string
disableColor bool
disableTableBorders bool
hideHeaders bool
json bool
force bool
name string
names []string
org organization
quiet bool
recurse bool
stackID string
stackIDs []string
urls []string
applyOpts struct {
envRefs []string
force string
secrets []string
}
exportOpts struct {
resourceType string
buckets string
checks string
dashboards string
endpoints string
labels string
rules string
tasks string
telegrafs string
variables string
bucketNames string
checkNames string
dashboardNames string
endpointNames string
labelNames string
ruleNames string
taskNames string
telegrafNames string
variableNames string
}
updateStackOpts struct {
addResources []string
}
}
func newCmdPkgerBuilder(svcFn templateSVCsFn, f *globalFlags, opts genericCLIOpts) *cmdTemplateBuilder {
return &cmdTemplateBuilder{
genericCLIOpts: opts,
globalFlags: f,
svcFn: svcFn,
}
}
func (b *cmdTemplateBuilder) cmdApply() *cobra.Command {
cmd := b.newCmd("apply", b.applyRunEFn)
cmd.Short = "Apply a template to manage resources"
cmd.Long = `
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 -f 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://v2.docs.influxdata.com/v2.0/reference/cli/influx/apply/.
For more templates created by the community, see
https://github.com/influxdata/community-templates.
`
b.org.register(b.viper, cmd, false)
b.registerTemplateFileFlags(cmd)
b.registerTemplatePrintOpts(cmd)
cmd.Flags().BoolVarP(&b.quiet, "quiet", "q", false, "Disable output printing")
cmd.Flags().StringVar(&b.applyOpts.force, "force", "", `TTY input, if template will have destructive changes, proceed if set "true"`)
cmd.Flags().StringVar(&b.stackID, "stack-id", "", "Stack ID to associate template application")
b.applyOpts.secrets = []string{}
cmd.Flags().StringSliceVar(&b.applyOpts.secrets, "secret", nil, "Secrets to provide alongside the template; format should --secret=SECRET_KEY=SECRET_VALUE --secret=SECRET_KEY_2=SECRET_VALUE_2")
cmd.Flags().StringSliceVar(&b.applyOpts.envRefs, "env-ref", nil, "Environment references to provide alongside the template; format should --env-ref=REF_KEY=REF_VALUE --env-ref=REF_KEY_2=REF_VALUE_2")
cmd.Flags().StringSliceVar(&b.filters, "filter", nil, "Resources to skip when applying the template. Filter out by kind or by resource")
return cmd
}
func (b *cmdTemplateBuilder) applyRunEFn(cmd *cobra.Command, args []string) error {
if err := b.org.validOrgFlags(&flags); err != nil {
return err
}
color.NoColor = b.disableColor
svc, orgSVC, err := b.svcFn()
if err != nil {
return err
}
influxOrgID, err := b.org.getID(orgSVC)
if err != nil {
return err
}
template, isTTY, err := b.readTemplate()
if err != nil {
return err
}
providedEnvRefs := mapKeys(template.Summary().MissingEnvs, b.applyOpts.envRefs)
if !isTTY {
for _, envRef := range missingValKeys(providedEnvRefs) {
prompt := "Please provide environment reference value for key " + envRef
providedEnvRefs[envRef] = b.getInput(prompt, "")
}
}
var stackID influxdb.ID
if b.stackID != "" {
if err := stackID.DecodeFromString(b.stackID); err != nil {
return err
}
}
opts := []pkger.ApplyOptFn{
pkger.ApplyWithTemplate(template),
pkger.ApplyWithEnvRefs(toMapInterface(providedEnvRefs)),
pkger.ApplyWithStackID(stackID),
}
actionOpts, err := parseTemplateActions(b.filters)
if err != nil {
return err
}
opts = append(opts, actionOpts...)
dryRunImpact, err := svc.DryRun(context.Background(), influxOrgID, 0, opts...)
if err != nil {
return err
}
providedSecrets := mapKeys(dryRunImpact.Summary.MissingSecrets, b.applyOpts.secrets)
if !isTTY {
const skipDefault = "$$skip-this-key$$"
for _, secretKey := range missingValKeys(providedSecrets) {
prompt := "Please provide secret value for key " + secretKey + " (optional, press enter to skip)"
secretVal := b.getInput(prompt, skipDefault)
if secretVal != "" && secretVal != skipDefault {
providedSecrets[secretKey] = secretVal
}
}
}
if err := b.printTemplateDiff(dryRunImpact.Diff); err != nil {
return err
}
isForced, _ := strconv.ParseBool(b.applyOpts.force)
if !isTTY && !isForced && b.applyOpts.force != "conflict" {
confirm := b.getInput("Confirm application of the above resources (y/n)", "n")
if strings.ToLower(confirm) != "y" {
fmt.Fprintln(b.w, "aborted application of template")
return nil
}
}
if b.applyOpts.force != "conflict" && isTTY && dryRunImpact.Diff.HasConflicts() {
return errors.New("template has conflicts with existing resources and cannot safely apply")
}
opts = append(opts, pkger.ApplyWithSecrets(providedSecrets))
impact, err := svc.Apply(context.Background(), influxOrgID, 0, opts...)
if err != nil {
return err
}
b.printTemplateSummary(impact.StackID, impact.Summary)
return nil
}
func parseTemplateActions(args []string) ([]pkger.ApplyOptFn, error) {
var opts []pkger.ApplyOptFn
for _, rawAct := range args {
pair := strings.SplitN(rawAct, "=", 2)
if len(pair) < 2 {
continue
}
key, val := pair[0], pair[1]
switch strings.ToLower(key) {
case "kind":
opts = append(opts, pkger.ApplyWithKindSkip(pkger.ActionSkipKind{
Kind: pkger.Kind(val),
}))
case "resource":
pp := strings.SplitN(val, ":", 2)
if len(pair) != 2 {
return nil, fmt.Errorf(`invalid skipResource action provided: %q;
Expected format --action=skipResource=Label:$LABEL_ID`, rawAct)
}
kind, metaName := pp[0], pp[1]
opts = append(opts, pkger.ApplyWithResourceSkip(pkger.ActionSkipResource{
Kind: pkger.Kind(kind),
MetaName: metaName,
}))
default:
return nil, fmt.Errorf(`invalid action provided: %q;
Expected format --action=skipResource=Label:$LABEL_ID
or
Expected format --action=skipKind=Bucket`, rawAct)
}
}
return opts, nil
}
func (b *cmdTemplateBuilder) cmdExport() *cobra.Command {
cmd := b.newCmd("export", b.exportRunEFn)
cmd.Short = "Export existing resources as a template"
cmd.Long = `
The export command provides a mechanism to export existing resources to a
template. Each template resource kind is supported via flags.
Examples:
# export buckets by ID
influx export --buckets=$ID1,$ID2,$ID3
# export buckets, labels, and dashboards by ID
influx export \
--buckets=$BID1,$BID2,$BID3 \
--labels=$LID1,$LID2,$LID3 \
--dashboards=$DID1,$DID2,$DID3
# export all resources for a stack
influx export --stack-id $STACK_ID
# export a stack with resources not associated with the stack
influx export --stack-id $STACK_ID --buckets $BUCKET_ID
All of the resources are supported via the examples provided above. Provide the
resource flag and then provide the IDs.
For information about exporting InfluxDB templates, see
https://v2.docs.influxdata.com/v2.0/reference/cli/influx/export/
`
cmd.AddCommand(
b.cmdExportAll(),
b.cmdExportStack(),
)
cmd.Flags().StringVarP(&b.file, "file", "f", "", "Output file for created template; defaults to std out if no file provided; the extension of provided file (.yml/.json) will dictate encoding")
cmd.Flags().StringVar(&b.stackID, "stack-id", "", "ID for stack to include in export")
cmd.Flags().StringVar(&b.exportOpts.resourceType, "resource-type", "", "Resource type provided will be associated with all IDs via stdin.")
cmd.Flags().StringVar(&b.exportOpts.buckets, "buckets", "", "List of bucket ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.checks, "checks", "", "List of check ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.dashboards, "dashboards", "", "List of dashboard ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.endpoints, "endpoints", "", "List of notification endpoint ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.labels, "labels", "", "List of label ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.rules, "rules", "", "List of notification rule ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.tasks, "tasks", "", "List of task ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.telegrafs, "telegraf-configs", "", "List of telegraf config ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.variables, "variables", "", "List of variable ids comma separated")
cmd.Flags().StringVar(&b.exportOpts.bucketNames, "bucket-names", "", "List of bucket names comma separated")
cmd.Flags().StringVar(&b.exportOpts.checkNames, "check-names", "", "List of check names comma separated")
cmd.Flags().StringVar(&b.exportOpts.dashboardNames, "dashboard-names", "", "List of dashboard names comma separated")
cmd.Flags().StringVar(&b.exportOpts.endpointNames, "endpoint-names", "", "List of notification endpoint names comma separated")
cmd.Flags().StringVar(&b.exportOpts.labelNames, "label-names", "", "List of label names comma separated")
cmd.Flags().StringVar(&b.exportOpts.ruleNames, "rule-names", "", "List of notification rule names comma separated")
cmd.Flags().StringVar(&b.exportOpts.taskNames, "task-names", "", "List of task names comma separated")
cmd.Flags().StringVar(&b.exportOpts.telegrafNames, "telegraf-config-names", "", "List of telegraf config names comma separated")
cmd.Flags().StringVar(&b.exportOpts.variableNames, "variable-names", "", "List of variable names comma separated")
return cmd
}
func (b *cmdTemplateBuilder) exportRunEFn(cmd *cobra.Command, args []string) error {
templateSVC, _, err := b.svcFn()
if err != nil {
return err
}
resTypes := []struct {
kind pkger.Kind
idStrs []string
names []string
}{
{kind: pkger.KindBucket, idStrs: strings.Split(b.exportOpts.buckets, ","), names: strings.Split(b.exportOpts.bucketNames, ",")},
{kind: pkger.KindCheck, idStrs: strings.Split(b.exportOpts.checks, ","), names: strings.Split(b.exportOpts.checkNames, ",")},
{kind: pkger.KindDashboard, idStrs: strings.Split(b.exportOpts.dashboards, ","), names: strings.Split(b.exportOpts.dashboardNames, ",")},
{kind: pkger.KindLabel, idStrs: strings.Split(b.exportOpts.labels, ","), names: strings.Split(b.exportOpts.labelNames, ",")},
{kind: pkger.KindNotificationEndpoint, idStrs: strings.Split(b.exportOpts.endpoints, ","), names: strings.Split(b.exportOpts.endpointNames, ",")},
{kind: pkger.KindNotificationRule, idStrs: strings.Split(b.exportOpts.rules, ","), names: strings.Split(b.exportOpts.ruleNames, ",")},
{kind: pkger.KindTask, idStrs: strings.Split(b.exportOpts.tasks, ","), names: strings.Split(b.exportOpts.taskNames, ",")},
{kind: pkger.KindTelegraf, idStrs: strings.Split(b.exportOpts.telegrafs, ","), names: strings.Split(b.exportOpts.telegrafNames, ",")},
{kind: pkger.KindVariable, idStrs: strings.Split(b.exportOpts.variables, ","), names: strings.Split(b.exportOpts.variableNames, ",")},
}
var opts []pkger.ExportOptFn
for _, rt := range resTypes {
newOpt, err := newResourcesToClone(rt.kind, rt.idStrs, rt.names)
if err != nil {
return ierror.Wrap(err, rt.kind.String())
}
opts = append(opts, newOpt)
}
if b.stackID != "" {
stackID, err := influxdb.IDFromString(b.stackID)
if err != nil {
return ierror.Wrap(err, "invalid stack ID provided")
}
opts = append(opts, pkger.ExportWithStackID(*stackID))
}
if b.exportOpts.resourceType == "" {
return b.exportTemplate(cmd.OutOrStdout(), templateSVC, b.file, opts...)
}
resKind := templateKindFold(b.exportOpts.resourceType)
if err := resKind.OK(); err != nil {
return errors.New("resource type is invalid; got: " + b.exportOpts.resourceType)
}
if stdin, err := inStdIn(b.in); err == nil {
stdinInpt, _ := b.readLines(stdin)
if len(stdinInpt) > 0 {
args = stdinInpt
}
}
resTypeOpt, err := newResourcesToClone(resKind, args, []string{})
if err != nil {
return err
}
opts = append(opts, resTypeOpt)
return b.exportTemplate(cmd.OutOrStdout(), templateSVC, b.file, opts...)
}
func (b *cmdTemplateBuilder) cmdExportAll() *cobra.Command {
cmd := b.newCmd("all", b.exportAllRunEFn)
cmd.Short = "Export all existing resources for an organization as a template"
cmd.Long = `
The export all command will export all resources for an organization. The
command also provides a mechanism to filter by label name or resource kind.
Examples:
# Export all resources for an organization
influx export all --org $ORG_NAME
# Export all bucket resources
influx export all --org $ORG_NAME --filter=kind=Bucket
# Export all resources associated with label Foo
influx export all --org $ORG_NAME --filter=labelName=Foo
# Export all bucket resources and filter by label Foo
influx export all --org $ORG_NAME \
--filter=kind=Bucket \
--filter=labelName=Foo
# Export all bucket or dashboard resources and filter by label Foo.
# note: like filters are unioned and filter types are intersections.
# This example will export a resource if it is a dashboard or
# bucket and has an associated label of Foo.
influx export all --org $ORG_NAME \
--filter=kind=Bucket \
--filter=kind=Dashboard \
--filter=labelName=Foo
For information about exporting InfluxDB templates, see
https://v2.docs.influxdata.com/v2.0/reference/cli/influx/export
and
https://v2.docs.influxdata.com/v2.0/reference/cli/influx/export/all
`
cmd.Flags().StringVarP(&b.file, "file", "f", "", "output file for created template; defaults to std out if no file provided; the extension of provided file (.yml/.json) will dictate encoding")
cmd.Flags().StringArrayVar(&b.filters, "filter", nil, "Filter exported resources by labelName or resourceKind (format: --filter=labelName=example)")
b.org.register(b.viper, cmd, false)
return cmd
}
func (b *cmdTemplateBuilder) exportAllRunEFn(cmd *cobra.Command, args []string) error {
templateSVC, orgSVC, err := b.svcFn()
if err != nil {
return err
}
orgID, err := b.org.getID(orgSVC)
if err != nil {
return err
}
var (
labelNames []string
resourceKinds []pkger.Kind
)
for _, filter := range b.filters {
pair := strings.SplitN(filter, "=", 2)
if len(pair) < 2 {
continue
}
switch key, val := pair[0], pair[1]; key {
case "labelName":
labelNames = append(labelNames, val)
case "kind", "resourceKind":
k := pkger.Kind(val)
if err := k.OK(); err != nil {
return err
}
resourceKinds = append(resourceKinds, k)
default:
return fmt.Errorf("invalid filter provided %q; filter must be 1 in [labelName, resourceKind]", filter)
}
}
orgOpt := pkger.ExportWithAllOrgResources(pkger.ExportByOrgIDOpt{
OrgID: orgID,
LabelNames: labelNames,
ResourceKinds: resourceKinds,
})
return b.exportTemplate(cmd.OutOrStdout(), templateSVC, b.file, orgOpt)
}
func (b *cmdTemplateBuilder) cmdExportStack() *cobra.Command {
cmd := b.newCmd("stack $STACK_ID", b.exportStackRunEFn)
cmd.Short = "Export all existing resources for associated with a stack as a template"
cmd.Long = `
The export stack command exports the resources associated with a stack as
they currently exist in the platform. All the same metadata.name fields will be
reused.
Example:
# Export by a stack
influx export stack $STACK_ID
For information about exporting InfluxDB templates, see
https://v2.docs.influxdata.com/v2.0/reference/cli/influx/export
and
https://v2.docs.influxdata.com/v2.0/reference/cli/influx/export/stack/
`
cmd.Args = cobra.ExactValidArgs(1)
cmd.Flags().StringVarP(&b.file, "file", "f", "", "output file for created template; defaults to std out if no file provided; the extension of provided file (.yml/.json) will dictate encoding")
b.org.register(b.viper, cmd, false)
return cmd
}
func (b *cmdTemplateBuilder) exportStackRunEFn(cmd *cobra.Command, args []string) error {
templateSVC, _, err := b.svcFn()
if err != nil {
return err
}
stackID, err := influxdb.IDFromString(args[0])
if err != nil {
return err
}
template, err := templateSVC.Export(context.Background(), pkger.ExportWithStackID(*stackID))
if err != nil {
return err
}
return b.writeTemplate(b.w, b.file, template)
}
func (b *cmdTemplateBuilder) cmdTemplate() *cobra.Command {
runE := func(cmd *cobra.Command, args []string) error {
template, _, err := b.readTemplate()
if err != nil {
return err
}
return b.printTemplateSummary(0, template.Summary())
}
cmd := b.genericCLIOpts.newCmd("template", runE, false)
b.registerTemplateFileFlags(cmd)
b.registerTemplatePrintOpts(cmd)
cmd.Short = "Summarize the provided template"
cmd.AddCommand(b.cmdTemplateValidate())
return cmd
}
func (b *cmdTemplateBuilder) cmdTemplateValidate() *cobra.Command {
runE := func(cmd *cobra.Command, args []string) error {
template, _, err := b.readTemplate()
if err != nil {
return err
}
return template.Validate()
}
cmd := b.genericCLIOpts.newCmd("validate", runE, false)
cmd.Short = "Validate the provided template"
b.registerTemplateFileFlags(cmd)
return cmd
}
func (b *cmdTemplateBuilder) cmdStacks() *cobra.Command {
cmd := b.newCmd("stacks [flags]", b.stackListRunEFn)
cmd.Flags().StringArrayVar(&b.stackIDs, "stack-id", nil, "Stack ID to filter by")
cmd.Flags().StringArrayVar(&b.names, "stack-name", nil, "Stack name to filter by")
registerPrintOptions(b.viper, cmd, &b.hideHeaders, &b.json)
b.org.register(b.viper, cmd, false)
cmd.Short = "List stack(s) and associated templates. Subcommands manage stacks."
cmd.Long = `
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://v2.docs.influxdata.com/v2.0/reference/cli/influx/stacks
`
cmd.AddCommand(
b.cmdStackInit(),
b.cmdStackRemove(),
b.cmdStackUpdate(),
)
return cmd
}
func (b *cmdTemplateBuilder) cmdStackInit() *cobra.Command {
cmd := b.newCmd("init", b.stackInitRunEFn)
cmd.Short = "Initialize a stack"
cmd.Long = `
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://v2.docs.influxdata.com/v2.0/reference/cli/influx/stacks/
and
https://v2.docs.influxdata.com/v2.0/reference/cli/influx/stacks/init/
`
cmd.Flags().StringVarP(&b.name, "stack-name", "n", "", "Name given to created stack")
cmd.Flags().StringVarP(&b.description, "stack-description", "d", "", "Description given to created stack")
cmd.Flags().StringArrayVarP(&b.urls, "template-url", "u", nil, "Template urls to associate with new stack")
registerPrintOptions(b.viper, cmd, &b.hideHeaders, &b.json)
b.org.register(b.viper, cmd, false)
return cmd
}
func (b *cmdTemplateBuilder) stackInitRunEFn(cmd *cobra.Command, args []string) error {
templateSVC, orgSVC, err := b.svcFn()
if err != nil {
return err
}
orgID, err := b.org.getID(orgSVC)
if err != nil {
return err
}
const fakeUserID = 0 // is 0 because user is pulled from token...
stack, err := templateSVC.InitStack(context.Background(), fakeUserID, pkger.StackCreate{
OrgID: orgID,
Name: b.name,
Description: b.description,
TemplateURLs: b.urls,
})
if err != nil {
return err
}
return b.writeStack(stack)
}
func (b *cmdTemplateBuilder) stackListRunEFn(cmd *cobra.Command, args []string) error {
templateSVC, orgSVC, err := b.svcFn()
if err != nil {
return err
}
orgID, err := b.org.getID(orgSVC)
if err != nil {
return err
}
var stackIDs []influxdb.ID
for _, rawID := range b.stackIDs {
id, err := influxdb.IDFromString(rawID)
if err != nil {
return err
}
stackIDs = append(stackIDs, *id)
}
stacks, err := templateSVC.ListStacks(context.Background(), orgID, pkger.ListFilter{
StackIDs: stackIDs,
Names: b.names,
})
if err != nil {
return err
}
if b.json {
return b.writeJSON(stacks)
}
tabW := b.newTabWriter()
defer tabW.Flush()
tabW.HideHeaders(b.hideHeaders)
writeStackRows(tabW, stacks...)
return nil
}
func (b *cmdTemplateBuilder) cmdStackRemove() *cobra.Command {
cmd := b.newCmd("rm [--stack-id=ID1 --stack-id=ID2]", b.stackRemoveRunEFn)
cmd.Short = "Remove a stack(s) and all associated resources"
cmd.Aliases = []string{"remove", "uninstall"}
cmd.Flags().StringArrayVar(&b.stackIDs, "stack-id", nil, "Stack IDs to be removed")
cmd.Flags().BoolVar(&b.force, "force", false, "Remove stack without confirmation prompt")
cmd.MarkFlagRequired("stack-id")
registerPrintOptions(b.viper, cmd, &b.hideHeaders, &b.json)
b.org.register(b.viper, cmd, false)
return cmd
}
func (b *cmdTemplateBuilder) stackRemoveRunEFn(cmd *cobra.Command, args []string) error {
templateSVC, orgSVC, err := b.svcFn()
if err != nil {
return err
}
orgID, err := b.org.getID(orgSVC)
if err != nil {
return err
}
var stackIDs []influxdb.ID
for _, rawID := range b.stackIDs {
id, err := influxdb.IDFromString(rawID)
if err != nil {
return err
}
stackIDs = append(stackIDs, *id)
}
stacks, err := templateSVC.ListStacks(context.Background(), orgID, pkger.ListFilter{
StackIDs: stackIDs,
})
if err != nil {
return err
}
printStack := func(stack pkger.Stack) error {
if b.json {
return b.writeJSON(stack)
}
tabW := b.newTabWriter()
defer func() {
tabW.Flush()
// add a breather line between confirm and printout
fmt.Fprintln(b.w)
}()
tabW.HideHeaders(b.hideHeaders)
writeStackRows(tabW, stack)
return nil
}
for _, stack := range stacks {
if err := printStack(stack); err != nil {
return err
}
if !b.force {
msg := fmt.Sprintf("Confirm removal of the stack[%s] and all associated resources (y/n)", stack.ID)
confirm := b.getInput(msg, "n")
if confirm != "y" {
continue
}
}
err := templateSVC.DeleteStack(context.Background(), struct{ OrgID, UserID, StackID influxdb.ID }{
OrgID: orgID,
UserID: 0,
StackID: stack.ID,
})
if err != nil {
return err
}
}
return nil
}
func (b *cmdTemplateBuilder) cmdStackUpdate() *cobra.Command {
cmd := b.newCmd("update", b.stackUpdateRunEFn)
cmd.Short = "Update a stack"
cmd.Long = `
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://v2.docs.influxdata.com/v2.0/reference/cli/influx/stacks
and
https://v2.docs.influxdata.com/v2.0/reference/cli/influx/stacks/update/
`
cmd.Flags().StringVarP(&b.stackID, "stack-id", "i", "", "ID of stack")
cmd.MarkFlagRequired("stack-id")
cmd.Flags().StringVarP(&b.name, "stack-name", "n", "", "Name for stack")
cmd.Flags().StringVarP(&b.description, "stack-description", "d", "", "Description for stack")
cmd.Flags().StringArrayVarP(&b.urls, "template-url", "u", nil, "Template urls to associate with stack")
cmd.Flags().StringArrayVar(&b.updateStackOpts.addResources, "addResource", nil, "Additional resources to associate with stack")
cmd.Flags().StringVarP(&b.file, "export-file", "f", "", "Destination for exported template")
registerPrintOptions(b.viper, cmd, &b.hideHeaders, &b.json)
return cmd
}
func (b *cmdTemplateBuilder) stackUpdateRunEFn(cmd *cobra.Command, args []string) error {
templateSVC, _, err := b.svcFn()
if err != nil {
return err
}
stackID, err := influxdb.IDFromString(b.stackID)
if err != nil {
return ierror.Wrap(err, "required stack id is invalid")
}
update := pkger.StackUpdate{
ID: *stackID,
Name: &b.name,
Description: &b.description,
TemplateURLs: b.urls,
}
failedString := []string{}
for _, res := range b.updateStackOpts.addResources {
parts := strings.SplitN(res, "=", 2)
if len(parts) < 2 {
failedString = append(failedString, res)
continue
}
kind, idRaw := templateKindFold(parts[0]), parts[1]
if err := kind.OK(); err != nil {
return errors.New("resource type is invalid; got: " + b.exportOpts.resourceType)
}
id, err := influxdb.IDFromString(idRaw)
if err != nil {
return ierror.Wrap(err, fmt.Sprintf("%s resource id %q is invalid", kind, idRaw))
}
update.AdditionalResources = append(update.AdditionalResources, pkger.StackAdditionalResource{
APIVersion: pkger.APIVersion,
ID: *id,
Kind: kind,
})
}
if len(failedString) > 0 {
return errors.New("invalid 'resourceType=resourceID' key value pair[s]: " + strings.Join(failedString, ";"))
}
stack, err := templateSVC.UpdateStack(context.Background(), update)
if err != nil {
return err
}
if err := b.writeStack(stack); err != nil {
return err
}
if len(update.AdditionalResources) == 0 {
return nil
}
if b.file == "" {
const msg = `
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? (y/n)`
for range make([]struct{}, 3) {
if in := b.getInput(msg, "n"); in == "y" {
break
} else if in == "n" {
return nil
}
}
}
return b.exportTemplate(cmd.OutOrStdout(), templateSVC, b.file, pkger.ExportWithStackID(*stackID))
}
func (b *cmdTemplateBuilder) writeStack(stack pkger.Stack) error {
if b.json {
return b.writeJSON(stack)
}
tabW := b.newTabWriter()
defer tabW.Flush()
tabW.HideHeaders(b.hideHeaders)
writeStackRows(tabW, stack)
return nil
}
func (b *cmdTemplateBuilder) newCmd(use string, runE func(*cobra.Command, []string) error) *cobra.Command {
cmd := b.genericCLIOpts.newCmd(use, runE, true)
b.globalFlags.registerFlags(b.viper, cmd)
return cmd
}
func (b *cmdTemplateBuilder) registerTemplatePrintOpts(cmd *cobra.Command) {
cmd.Flags().BoolVar(&b.disableColor, "disable-color", false, "Disable color in output")
cmd.Flags().BoolVar(&b.disableTableBorders, "disable-table-borders", false, "Disable table borders")
registerPrintOptions(b.viper, cmd, nil, &b.json)
}
func (b *cmdTemplateBuilder) registerTemplateFileFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceVarP(&b.files, "file", "f", nil, "Path to template file; Supports HTTP(S) URLs or file paths.")
cmd.MarkFlagFilename("file", "yaml", "yml", "json", "jsonnet")
cmd.Flags().BoolVarP(&b.recurse, "recurse", "R", false, "Process the directory used in -f, --file recursively. Useful when you want to manage related templates organized within the same directory.")
cmd.Flags().StringSliceVarP(&b.urls, "template-url", "u", nil, "URL to template file")
cmd.Flags().MarkDeprecated("template-url", "use the --file flag; example: influx apply --file $URL_TO_TEMPLATE")
cmd.Flags().StringVarP(&b.encoding, "encoding", "e", "", "Encoding for the input stream. If a file is provided will gather encoding type from file extension. If extension provided will override.")
cmd.MarkFlagFilename("encoding", "yaml", "yml", "json", "jsonnet")
}
func (b *cmdTemplateBuilder) exportTemplate(w io.Writer, templateSVC pkger.SVC, outPath string, opts ...pkger.ExportOptFn) error {
template, err := templateSVC.Export(context.Background(), opts...)
if err != nil {
return err
}
return b.writeTemplate(w, outPath, template)
}
func (b *cmdTemplateBuilder) writeTemplate(w io.Writer, outPath string, template *pkger.Template) error {
buf, err := createTemplateBuf(template, outPath)
if err != nil {
return err
}
if outPath == "" {
_, err := io.Copy(w, buf)
return err
}
return ioutil.WriteFile(outPath, buf.Bytes(), os.ModePerm)
}
func (b *cmdTemplateBuilder) readRawTemplatesFromFiles(filePaths []string, recurse bool) ([]*pkger.Template, error) {
mFiles := make(map[string]struct{})
for _, f := range filePaths {
files, err := readFilesFromPath(f, recurse)
if err != nil {
return nil, err
}
for _, ff := range files {
mFiles[ff] = struct{}{}
}
}
var rawTemplates []*pkger.Template
for f := range mFiles {
template, err := pkger.Parse(b.convertFileEncoding(f), pkger.FromFile(f), pkger.ValidSkipParseError())
if err != nil {
return nil, err
}
rawTemplates = append(rawTemplates, template)
}
return rawTemplates, nil
}
func (b *cmdTemplateBuilder) readRawTemplatesFromURLs(urls []string) ([]*pkger.Template, error) {
mURLs := make(map[string]struct{})
for _, f := range urls {
mURLs[f] = struct{}{}
}
var rawTemplates []*pkger.Template
for u := range mURLs {
template, err := pkger.Parse(b.convertURLEncoding(u), pkger.FromHTTPRequest(u), pkger.ValidSkipParseError())
if err != nil {
return nil, err
}
rawTemplates = append(rawTemplates, template)
}
return rawTemplates, nil
}
func (b *cmdTemplateBuilder) readTemplate() (*pkger.Template, bool, error) {
var remotes, files []string
for _, rawURL := range append(b.files, b.urls...) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, false, ierror.Wrap(err, fmt.Sprintf("failed to parse url[%s]", rawURL))
}
if strings.HasPrefix(u.Scheme, "http") {
remotes = append(remotes, u.String())
} else {
files = append(files, u.String())
}
}
templates, err := b.readRawTemplatesFromFiles(files, b.recurse)
if err != nil {
return nil, false, err
}
urlTemplates, err := b.readRawTemplatesFromURLs(remotes)
if err != nil {
return nil, false, err
}
templates = append(templates, urlTemplates...)
// the pkger.ValidSkipParseError option allows our server to be the one to validate the
// the template is accurate. If a user has an older version of the CLI and cloud gets updated
// with new validation rules,they'll get immediate access to that change without having to
// rol their CLI build.
if _, err := inStdIn(b.in); err != nil {
template, err := pkger.Combine(templates, pkger.ValidSkipParseError())
return template, false, err
}
stdinTemplate, err := pkger.Parse(b.convertEncoding(), pkger.FromReader(b.in), pkger.ValidSkipParseError())
if err != nil {
return nil, true, err
}
template, err := pkger.Combine(append(templates, stdinTemplate), pkger.ValidSkipParseError())
return template, true, err
}
func (b *cmdTemplateBuilder) readLines(r io.Reader) ([]string, error) {
bb, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
var stdinInput []string
for _, bs := range bytes.Split(bb, []byte("\n")) {
trimmed := bytes.TrimSpace(bs)
if len(trimmed) == 0 {
continue
}
stdinInput = append(stdinInput, string(trimmed))
}
return stdinInput, nil
}
func (b *cmdTemplateBuilder) getInput(msg, defaultVal string) string {
ui := &input.UI{
Writer: b.w,
Reader: b.in,
}
return internal2.GetInput(ui, msg, defaultVal)
}
func (b *cmdTemplateBuilder) convertURLEncoding(url string) pkger.Encoding {
urlBase := path.Ext(url)
switch {
case strings.HasPrefix(urlBase, ".jsonnet"):
return pkger.EncodingJsonnet
case strings.HasPrefix(urlBase, ".json"):
return pkger.EncodingJSON
case strings.HasPrefix(urlBase, ".yml") || strings.HasPrefix(urlBase, ".yaml"):
return pkger.EncodingYAML
}
return b.convertEncoding()
}
func (b *cmdTemplateBuilder) convertFileEncoding(file string) pkger.Encoding {
ext := filepath.Ext(file)
switch {
case strings.HasPrefix(ext, ".jsonnet"):
return pkger.EncodingJsonnet
case strings.HasPrefix(ext, ".json"):
return pkger.EncodingJSON
case strings.HasPrefix(ext, ".yml") || strings.HasPrefix(ext, ".yaml"):
return pkger.EncodingYAML
}
return b.convertEncoding()
}
func (b *cmdTemplateBuilder) convertEncoding() pkger.Encoding {
switch {
case b.encoding == "json":
return pkger.EncodingJSON
case b.encoding == "yml" || b.encoding == "yaml":
return pkger.EncodingYAML
case b.encoding == "jsonnet":
return pkger.EncodingJsonnet
default:
return pkger.EncodingSource
}
}
func newResourcesToClone(kind pkger.Kind, idStrs, names []string) (pkger.ExportOptFn, error) {
ids, err := toInfluxIDs(idStrs)
if err != nil {
return nil, err
}
var resources []pkger.ResourceToClone
for _, id := range ids {
resources = append(resources, pkger.ResourceToClone{
Kind: kind,
ID: id,
})
}
for _, name := range names {
if len(name) == 0 {
continue
}
resources = append(resources, pkger.ResourceToClone{
Kind: kind,
Name: name,
})
}
return pkger.ExportWithExistingResources(resources...), nil
}
func toInfluxIDs(args []string) ([]influxdb.ID, error) {
var (
ids []influxdb.ID
errs []string
)
for _, arg := range args {
normedArg := strings.TrimSpace(strings.ToLower(arg))
if len(normedArg) == 0 {
continue
}
id, err := influxdb.IDFromString(normedArg)
if err != nil {
errs = append(errs, "arg must provide a valid 16 length ID; got: "+arg)
continue
}
ids = append(ids, *id)
}
if len(errs) > 0 {
return nil, errors.New(strings.Join(errs, "\n\t"))
}
return ids, nil
}
func createTemplateBuf(template *pkger.Template, outPath string) (*bytes.Buffer, error) {
var encoding pkger.Encoding
switch ext := filepath.Ext(outPath); ext {
case ".json":
encoding = pkger.EncodingJSON
default:
encoding = pkger.EncodingYAML
}
b, err := template.Encode(encoding)
if err != nil {
return nil, err
}
return bytes.NewBuffer(b), nil
}
func newPkgerSVC() (pkger.SVC, influxdb.OrganizationService, error) {
httpClient, err := newHTTPClient()
if err != nil {
return nil, nil, err
}
orgSvc := &tenant.OrgClientService{
Client: httpClient,
}
return &pkger.HTTPRemoteService{Client: httpClient}, orgSvc, nil
}
func (b *cmdTemplateBuilder) printTemplateDiff(diff pkger.Diff) error {
if b.quiet {
return nil
}
if b.json {
return b.writeJSON(diff)
}
diffPrinterGen := func(title string, headers []string) *diffPrinter {
commonHeaders := []string{"Metadata Name", "ID", "Resource Name"}
printer := newDiffPrinter(b.w, !b.disableColor, !b.disableTableBorders)
printer.
Title(title).
SetHeaders(append(commonHeaders, headers...)...)
return printer
}
if labels := diff.Labels; len(labels) > 0 {
printer := diffPrinterGen("Labels", []string{"Color", "Description"})
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffLabelValues) []string {
return []string{metaName, id.String(), v.Name, v.Color, v.Description}
}
for _, l := range labels {
var oldRow []string
if l.Old != nil {
oldRow = appendValues(l.ID, l.MetaName, *l.Old)
}
newRow := appendValues(l.ID, l.MetaName, l.New)
switch {
case pkger.IsNew(l.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(l.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if bkts := diff.Buckets; len(bkts) > 0 {
printer := diffPrinterGen("Buckets", []string{"Retention Period", "Description"})
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffBucketValues) []string {
return []string{metaName, id.String(), v.Name, v.RetentionRules.RP().String(), v.Description}
}
for _, b := range bkts {
var oldRow []string
if b.Old != nil {
oldRow = appendValues(b.ID, b.MetaName, *b.Old)
}
newRow := appendValues(b.ID, b.MetaName, b.New)
switch {
case pkger.IsNew(b.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(b.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if checks := diff.Checks; len(checks) > 0 {
printer := diffPrinterGen("Checks", []string{"Description"})
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffCheckValues) []string {
out := []string{metaName, id.String()}
if v.Check == nil {
return append(out, "", "")
}
return append(out, v.Check.GetName(), v.Check.GetDescription())
}
for _, c := range checks {
var oldRow []string
if c.Old != nil {
oldRow = appendValues(c.ID, c.MetaName, *c.Old)
}
newRow := appendValues(c.ID, c.MetaName, c.New)
switch {
case pkger.IsNew(c.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(c.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if dashes := diff.Dashboards; len(dashes) > 0 {
printer := diffPrinterGen("Dashboards", []string{"Description", "Num Charts"})
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffDashboardValues) []string {
return []string{metaName, id.String(), v.Name, v.Desc, strconv.Itoa(len(v.Charts))}
}
for _, d := range dashes {
var oldRow []string
if d.Old != nil {
oldRow = appendValues(d.ID, d.MetaName, *d.Old)
}
newRow := appendValues(d.ID, d.MetaName, d.New)
switch {
case pkger.IsNew(d.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(d.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if endpoints := diff.NotificationEndpoints; len(endpoints) > 0 {
printer := diffPrinterGen("Notification Endpoints", nil)
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffNotificationEndpointValues) []string {
out := []string{metaName, id.String()}
if v.NotificationEndpoint == nil {
return append(out, "")
}
return append(out, v.NotificationEndpoint.GetName())
}
for _, e := range endpoints {
var oldRow []string
if e.Old != nil {
oldRow = appendValues(e.ID, e.MetaName, *e.Old)
}
newRow := appendValues(e.ID, e.MetaName, e.New)
switch {
case pkger.IsNew(e.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(e.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if rules := diff.NotificationRules; len(rules) > 0 {
printer := diffPrinterGen("Notification Rules", []string{
"Description",
"Every",
"Offset",
"Endpoint Name",
"Endpoint ID",
"Endpoint Type",
})
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffNotificationRuleValues) []string {
return []string{
metaName,
id.String(),
v.Name,
v.Every,
v.Offset,
v.EndpointName,
v.EndpointID.String(),
v.EndpointType,
}
}
for _, e := range rules {
var oldRow []string
if e.Old != nil {
oldRow = appendValues(e.ID, e.MetaName, *e.Old)
}
newRow := appendValues(e.ID, e.MetaName, e.New)
switch {
case pkger.IsNew(e.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(e.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if teles := diff.Telegrafs; len(teles) > 0 {
printer := diffPrinterGen("Telegraf Configurations", []string{"Description"})
appendValues := func(id pkger.SafeID, metaName string, v influxdb.TelegrafConfig) []string {
return []string{metaName, id.String(), v.Name, v.Description}
}
for _, e := range teles {
var oldRow []string
if e.Old != nil {
oldRow = appendValues(e.ID, e.MetaName, *e.Old)
}
newRow := appendValues(e.ID, e.MetaName, e.New)
switch {
case pkger.IsNew(e.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(e.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if tasks := diff.Tasks; len(tasks) > 0 {
printer := diffPrinterGen("Tasks", []string{"Description", "Cycle"})
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffTaskValues) []string {
timing := v.Cron
if v.Cron == "" {
if v.Offset == "" {
v.Offset = time.Duration(0).String()
}
timing = fmt.Sprintf("every: %s offset: %s", v.Every, v.Offset)
}
return []string{metaName, id.String(), v.Name, v.Description, timing}
}
for _, e := range tasks {
var oldRow []string
if e.Old != nil {
oldRow = appendValues(e.ID, e.MetaName, *e.Old)
}
newRow := appendValues(e.ID, e.MetaName, e.New)
switch {
case pkger.IsNew(e.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(e.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if vars := diff.Variables; len(vars) > 0 {
printer := diffPrinterGen("Variables", []string{"Description", "Arg Type", "Arg Values"})
appendValues := func(id pkger.SafeID, metaName string, v pkger.DiffVariableValues) []string {
var argType string
if v.Args != nil {
argType = v.Args.Type
}
return []string{metaName, id.String(), v.Name, v.Description, argType, printVarArgs(v.Args)}
}
for _, v := range vars {
var oldRow []string
if v.Old != nil {
oldRow = appendValues(v.ID, v.MetaName, *v.Old)
}
newRow := appendValues(v.ID, v.MetaName, v.New)
switch {
case pkger.IsNew(v.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(v.StateStatus):
printer.AppendDiff(oldRow, nil)
default:
printer.AppendDiff(oldRow, newRow)
}
}
printer.Render()
}
if len(diff.LabelMappings) > 0 {
printer := newDiffPrinter(b.w, !b.disableColor, !b.disableTableBorders)
printer.
Title("Label Associations").
SetHeaders(
"Resource Type",
"Resource Meta Name", "Resource Name", "Resource ID",
"Label Package Name", "Label Name", "Label ID",
)
for _, m := range diff.LabelMappings {
newRow := []string{
string(m.ResType),
m.ResMetaName, m.ResName, m.ResID.String(),
m.LabelMetaName, m.LabelName, m.LabelID.String(),
}
switch {
case pkger.IsNew(m.StateStatus):
printer.AppendDiff(nil, newRow)
case pkger.IsRemoval(m.StateStatus):
printer.AppendDiff(newRow, nil)
default:
printer.AppendDiff(newRow, newRow)
}
}
printer.Render()
}
return nil
}
func (b *cmdTemplateBuilder) printTemplateSummary(stackID influxdb.ID, sum pkger.Summary) error {
if b.quiet {
return nil
}
if b.json {
return b.writeJSON(struct {
StackID string `json:"stackID"`
Summary pkger.Summary
}{
StackID: stackID.String(),
Summary: sum,
})
}
commonHeaders := []string{"Package Name", "ID", "Resource Name"}
tablePrintFn := b.tablePrinterGen()
if labels := sum.Labels; len(labels) > 0 {
headers := append(commonHeaders, "Description", "Color")
tablePrintFn("LABELS", headers, len(labels), func(i int) []string {
l := labels[i]
return []string{
l.MetaName,
l.ID.String(),
l.Name,
l.Properties.Description,
l.Properties.Color,
}
})
}
if buckets := sum.Buckets; len(buckets) > 0 {
headers := append(commonHeaders, "Retention", "Description")
tablePrintFn("BUCKETS", headers, len(buckets), func(i int) []string {
bucket := buckets[i]
return []string{
bucket.MetaName,
bucket.ID.String(),
bucket.Name,
formatDuration(bucket.RetentionPeriod),
bucket.Description,
}
})
}
if checks := sum.Checks; len(checks) > 0 {
headers := append(commonHeaders, "Description")
tablePrintFn("CHECKS", headers, len(checks), func(i int) []string {
c := checks[i].Check
return []string{
checks[i].MetaName,
c.GetID().String(),
c.GetName(),
c.GetDescription(),
}
})
}
if dashes := sum.Dashboards; len(dashes) > 0 {
headers := append(commonHeaders, "Description")
tablePrintFn("DASHBOARDS", headers, len(dashes), func(i int) []string {
d := dashes[i]
return []string{d.MetaName, d.ID.String(), d.Name, d.Description}
})
}
if endpoints := sum.NotificationEndpoints; len(endpoints) > 0 {
headers := append(commonHeaders, "Description", "Status")
tablePrintFn("NOTIFICATION ENDPOINTS", headers, len(endpoints), func(i int) []string {
v := endpoints[i]
return []string{
v.MetaName,
v.NotificationEndpoint.GetID().String(),
v.NotificationEndpoint.GetName(),
v.NotificationEndpoint.GetDescription(),
string(v.NotificationEndpoint.GetStatus()),
}
})
}
if rules := sum.NotificationRules; len(rules) > 0 {
headers := append(commonHeaders, "Description", "Every", "Offset", "Endpoint Name", "Endpoint ID", "Endpoint Type")
tablePrintFn("NOTIFICATION RULES", headers, len(rules), func(i int) []string {
v := rules[i]
return []string{
v.MetaName,
v.ID.String(),
v.Name,
v.Description,
v.Every,
v.Offset,
v.EndpointMetaName,
v.EndpointID.String(),
v.EndpointType,
}
})
}
if tasks := sum.Tasks; len(tasks) > 0 {
headers := append(commonHeaders, "Description", "Cycle")
tablePrintFn("TASKS", headers, len(tasks), func(i int) []string {
t := tasks[i]
timing := fmt.Sprintf("every: %s offset: %s", t.Every, t.Offset)
if t.Cron != "" {
timing = t.Cron
}
return []string{
t.MetaName,
t.ID.String(),
t.Name,
t.Description,
timing,
}
})
}
if teles := sum.TelegrafConfigs; len(teles) > 0 {
headers := append(commonHeaders, "Description")
tablePrintFn("TELEGRAF CONFIGS", headers, len(teles), func(i int) []string {
t := teles[i]
return []string{
t.MetaName,
t.TelegrafConfig.ID.String(),
t.TelegrafConfig.Name,
t.TelegrafConfig.Description,
}
})
}
if vars := sum.Variables; len(vars) > 0 {
headers := append(commonHeaders, "Description", "Arg Type", "Arg Values")
tablePrintFn("VARIABLES", headers, len(vars), func(i int) []string {
v := vars[i]
args := v.Arguments
return []string{
v.MetaName,
v.ID.String(),
v.Name,
v.Description,
args.Type,
printVarArgs(args),
}
})
}
if mappings := sum.LabelMappings; len(mappings) > 0 {
headers := []string{"Resource Type", "Resource Name", "Resource ID", "Label Name", "Label ID"}
tablePrintFn("LABEL ASSOCIATIONS", headers, len(mappings), func(i int) []string {
m := mappings[i]
return []string{
string(m.ResourceType),
m.ResourceName,
m.ResourceID.String(),
m.LabelName,
m.LabelID.String(),
}
})
}
if secrets := sum.MissingSecrets; len(secrets) > 0 {
headers := []string{"Secret Key"}
tablePrintFn("MISSING SECRETS", headers, len(secrets), func(i int) []string {
return []string{secrets[i]}
})
}
if stackID != 0 {
fmt.Fprintln(b.w, "Stack ID: "+stackID.String())
}
return nil
}
func (b *cmdTemplateBuilder) tablePrinterGen() func(table string, headers []string, count int, rowFn func(i int) []string) {
return func(table string, headers []string, count int, rowFn func(i int) []string) {
tablePrinter(b.w, table, headers, count, !b.disableColor, !b.disableTableBorders, rowFn)
}
}
func writeStackRows(tabW *internal.TabWriter, stacks ...pkger.Stack) {
tabW.WriteHeaders("ID", "OrgID", "Active", "Name", "Description", "Num Resources", "Sources", "URLs", "Created At", "Updated At")
for _, stack := range stacks {
latest := stack.LatestEvent()
tabW.Write(map[string]interface{}{
"ID": stack.ID,
"OrgID": stack.OrgID,
"Active": latest.EventType != pkger.StackEventUninstalled,
"Name": latest.Name,
"Description": latest.Description,
"Num Resources": len(latest.Resources),
"Sources": latest.Sources,
"URLs": latest.TemplateURLs,
"Created At": stack.CreatedAt,
"Updated At": latest.UpdatedAt,
})
}
}
type diffPrinter struct {
w io.Writer
writer *tablewriter.Table
colorAdd tablewriter.Colors
colorFooter tablewriter.Colors
colorHeaders tablewriter.Colors
colorRemove tablewriter.Colors
title string
appendCalls int
headerLen int
}
func newDiffPrinter(w io.Writer, hasColor, hasBorder bool) *diffPrinter {
wr := tablewriter.NewWriter(w)
wr.SetBorder(hasBorder)
wr.SetRowLine(hasBorder)
var (
colorAdd = tablewriter.Colors{}
colorFooter = tablewriter.Colors{}
colorHeader = tablewriter.Colors{}
colorRemove = tablewriter.Colors{}
)
if hasColor {
colorAdd = tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Bold}
colorFooter = tablewriter.Color(tablewriter.FgHiBlueColor, tablewriter.Bold)
colorHeader = tablewriter.Colors{tablewriter.FgCyanColor, tablewriter.Bold}
colorRemove = tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold}
}
return &diffPrinter{
w: w,
writer: wr,
colorAdd: colorAdd,
colorFooter: colorFooter,
colorHeaders: colorHeader,
colorRemove: colorRemove,
}
}
func (d *diffPrinter) Render() {
if d.appendCalls == 0 {
return
}
// set the title and the add/remove legend
title := color.New(color.FgYellow, color.Bold).Sprint(strings.ToUpper(d.title))
add := color.New(color.FgHiGreen, color.Bold).Sprint("+add")
remove := color.New(color.FgRed, color.Bold).Sprint("-remove")
fmt.Fprintf(d.w, "%s %s | %s | unchanged\n", title, add, remove)
d.setFooter()
d.writer.Render()
fmt.Fprintln(d.w)
}
func (d *diffPrinter) Title(title string) *diffPrinter {
d.title = title
return d
}
func (d *diffPrinter) SetHeaders(headers ...string) *diffPrinter {
headers = d.prepend(headers, "+/-")
d.headerLen = len(headers)
d.writer.SetHeader(headers)
headerColors := make([]tablewriter.Colors, d.headerLen)
for i := range headerColors {
headerColors[i] = d.colorHeaders
}
d.writer.SetHeaderColor(headerColors...)
return d
}
func (d *diffPrinter) setFooter() *diffPrinter {
footers := make([]string, d.headerLen)
if d.headerLen > 1 {
footers[len(footers)-2] = "TOTAL"
footers[len(footers)-1] = strconv.Itoa(d.appendCalls)
} else {
footers[0] = "TOTAL: " + strconv.Itoa(d.appendCalls)
}
d.writer.SetFooter(footers)
colors := make([]tablewriter.Colors, d.headerLen)
if d.headerLen > 1 {
colors[len(colors)-2] = d.colorFooter
colors[len(colors)-1] = d.colorFooter
} else {
colors[0] = d.colorFooter
}
d.writer.SetFooterColor(colors...)
return d
}
func (d *diffPrinter) Append(slc []string) {
d.writer.Append(d.prepend(slc, ""))
}
func (d *diffPrinter) AppendDiff(remove, add []string) {
defer func() { d.appendCalls++ }()
if d.appendCalls > 0 {
d.appendBufferLine()
}
lenAdd, lenRemove := len(add), len(remove)
preppedAdd, preppedRemove := d.prepend(add, "+"), d.prepend(remove, "-")
if lenRemove > 0 && lenAdd == 0 {
d.writer.Rich(preppedRemove, d.redRow(len(preppedRemove)))
return
}
if lenAdd > 0 && lenRemove == 0 {
d.writer.Rich(preppedAdd, d.greenRow(len(preppedAdd)))
return
}
var (
addColors = make([]tablewriter.Colors, len(preppedAdd))
removeColors = make([]tablewriter.Colors, len(preppedRemove))
hasDiff bool
)
for i := 0; i < lenRemove; i++ {
if add[i] != remove[i] {
hasDiff = true
// offset to skip prepended +/- column
addColors[i+1], removeColors[i+1] = d.colorAdd, d.colorRemove
}
}
if !hasDiff {
d.writer.Append(d.prepend(add, ""))
return
}
addColors[0], removeColors[0] = d.colorAdd, d.colorRemove
d.writer.Rich(d.prepend(remove, "-"), removeColors)
d.writer.Rich(d.prepend(add, "+"), addColors)
}
func (d *diffPrinter) appendBufferLine() {
d.writer.Append([]string{})
}
func (d *diffPrinter) redRow(i int) []tablewriter.Colors {
return colorRow(d.colorRemove, i)
}
func (d *diffPrinter) greenRow(i int) []tablewriter.Colors {
return colorRow(d.colorAdd, i)
}
func (d *diffPrinter) prepend(slc []string, val string) []string {
return append([]string{val}, slc...)
}
func colorRow(color tablewriter.Colors, i int) []tablewriter.Colors {
colors := make([]tablewriter.Colors, i)
for i := range colors {
colors[i] = color
}
return colors
}
func tablePrinter(wr io.Writer, table string, headers []string, count int, hasColor, hasTableBorders bool, rowFn func(i int) []string) {
color.New(color.FgYellow, color.Bold).Fprintln(wr, strings.ToUpper(table))
w := tablewriter.NewWriter(wr)
w.SetBorder(hasTableBorders)
w.SetRowLine(hasTableBorders)
var alignments []int
for range headers {
alignments = append(alignments, tablewriter.ALIGN_CENTER)
}
descrCol := find("description", headers)
if descrCol != -1 {
w.SetColMinWidth(descrCol, 30)
alignments[descrCol] = tablewriter.ALIGN_LEFT
}
w.SetHeader(headers)
w.SetColumnAlignment(alignments)
for i := range make([]struct{}, count) {
w.Append(rowFn(i))
}
footers := make([]string, len(headers))
if len(headers) > 1 {
footers[len(footers)-2] = "TOTAL"
footers[len(footers)-1] = strconv.Itoa(count)
} else {
footers[0] = "TOTAL: " + strconv.Itoa(count)
}
w.SetFooter(footers)
if hasColor {
headerColor := tablewriter.Color(tablewriter.FgHiCyanColor, tablewriter.Bold)
footerColor := tablewriter.Color(tablewriter.FgHiBlueColor, tablewriter.Bold)
var colors []tablewriter.Colors
for i := 0; i < len(headers); i++ {
colors = append(colors, headerColor)
}
w.SetHeaderColor(colors...)
if len(headers) > 1 {
colors[len(colors)-2] = footerColor
colors[len(colors)-1] = footerColor
} else {
colors[0] = footerColor
}
w.SetFooterColor(colors...)
}
w.Render()
fmt.Fprintln(wr)
}
func printVarArgs(a *influxdb.VariableArguments) string {
if a == nil {
return "<nil>"
}
if a.Type == "map" {
b, err := json.Marshal(a.Values)
if err != nil {
return "{}"
}
return string(b)
}
if a.Type == "constant" {
vals, ok := a.Values.(influxdb.VariableConstantValues)
if !ok {
return "[]"
}
var out []string
for _, s := range vals {
out = append(out, fmt.Sprintf("%q", s))
}
return fmt.Sprintf("[%s]", strings.Join(out, " "))
}
if a.Type == "query" {
qVal, ok := a.Values.(influxdb.VariableQueryValues)
if !ok {
return ""
}
return fmt.Sprintf("language=%q query=%q", qVal.Language, qVal.Query)
}
return "unknown variable argument"
}
func formatDuration(d time.Duration) string {
if d == 0 {
return "inf"
}
return d.String()
}
func readFilesFromPath(filePath string, recurse bool) ([]string, error) {
info, err := os.Stat(filePath)
if err != nil {
return nil, err
}
if !info.IsDir() {
return []string{filePath}, nil
}
dirFiles, err := ioutil.ReadDir(filePath)
if err != nil {
return nil, err
}
mFiles := make(map[string]struct{})
assign := func(ss ...string) {
for _, s := range ss {
mFiles[s] = struct{}{}
}
}
for _, f := range dirFiles {
fileP := filepath.Join(filePath, f.Name())
if f.IsDir() {
if recurse {
rFiles, err := readFilesFromPath(fileP, recurse)
if err != nil {
return nil, err
}
assign(rFiles...)
}
continue
}
assign(fileP)
}
var files []string
for f := range mFiles {
files = append(files, f)
}
return files, nil
}
func mapKeys(provided, kvPairs []string) map[string]string {
out := make(map[string]string)
for _, k := range provided {
out[k] = ""
}
for _, pair := range kvPairs {
pieces := strings.SplitN(pair, "=", 2)
if len(pieces) < 2 {
continue
}
k, v := pieces[0], pieces[1]
if _, ok := out[k]; !ok {
continue
}
out[k] = v
}
return out
}
func toMapInterface(m map[string]string) map[string]interface{} {
out := make(map[string]interface{})
for k, v := range m {
out[k] = v
}
return out
}
func missingValKeys(m map[string]string) []string {
out := make([]string, 0, len(m))
for k, v := range m {
if v != "" {
continue
}
out = append(out, k)
}
sort.Slice(out, func(i, j int) bool {
return out[i] < out[j]
})
return out
}
func templateKindFold(resType string) pkger.Kind {
resKind := pkger.KindUnknown
for _, k := range pkger.Kinds() {
if strings.EqualFold(string(k), resType) {
resKind = k
break
}
}
return resKind
}
func find(needle string, haystack []string) int {
for i, h := range haystack {
if strings.ToLower(h) == needle {
return i
}
}
return -1
}
func inStdIn(in io.Reader) (*os.File, error) {
stdin, _ := in.(*os.File)
if stdin != os.Stdin {
return nil, errors.New("input not stdIn")
}
info, err := stdin.Stat()
if err != nil {
return nil, err
}
if (info.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
return nil, errors.New("input not stdIn")
}
return stdin, nil
}