feat: port apply command from influxdb (#160)

This commit is contained in:
Daniel Moran
2021-06-30 14:47:23 -04:00
committed by GitHub
parent d7b2983dd4
commit a3f36a9821
8 changed files with 1037 additions and 8 deletions

729
clients/apply/apply.go Normal file
View File

@ -0,0 +1,729 @@
package apply
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/influxdata/influx-cli/v2/api"
"github.com/influxdata/influx-cli/v2/clients"
"github.com/influxdata/influx-cli/v2/pkg/influxid"
"github.com/influxdata/influx-cli/v2/pkg/template"
)
type Client struct {
clients.CLI
api.TemplatesApi
api.OrganizationsApi
}
type Params struct {
OrgId string
OrgName string
StackId string
Sources []TemplateSource
Recursive bool
Secrets map[string]string
EnvVars map[string]string
Filters []ResourceFilter
Force bool
OverwriteConflicts bool
Quiet bool
RenderTableColors bool
RenderTableBorders bool
}
type ResourceFilter struct {
Kind string
Name *string
}
func (c Client) Apply(ctx context.Context, params *Params) 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
}
orgs, err := c.GetOrgs(ctx).Org(orgName).Execute()
if err != nil {
return fmt.Errorf("failed to get ID for org %q: %w", orgName, err)
}
if len(orgs.GetOrgs()) == 0 {
return fmt.Errorf("no orgs found with name %q: %w", orgName, err)
}
orgID = orgs.GetOrgs()[0].GetId()
}
templates, err := readTemplates(ctx, params.Sources)
if err != nil {
return err
}
req := api.TemplateApply{
DryRun: true,
OrgID: orgID,
Templates: templates,
EnvRefs: params.EnvVars,
Secrets: params.Secrets,
Actions: make([]api.TemplateApplyAction, len(params.Filters)),
}
if params.StackId != "" {
req.StackID = &params.StackId
}
for i, f := range params.Filters {
req.Actions[i].Action = api.TEMPLATEAPPLYACTIONKIND_SKIP_KIND
if f.Name != nil {
req.Actions[i].Action = api.TEMPLATEAPPLYACTIONKIND_SKIP_RESOURCE
}
req.Actions[i].Properties = api.TemplateApplyActionProperties{
Kind: f.Kind,
ResourceTemplateName: f.Name,
}
}
// Initial dry-run to make the server summarize the template & report any missing env/secret values.
res, err := c.ApplyTemplate(ctx).TemplateApply(req).Execute()
if err != nil {
return fmt.Errorf("failed to check template impact: %w", err)
}
if c.StdIO.InputIsInteractive() && (len(res.Summary.MissingEnvRefs) > 0 || len(res.Summary.MissingSecrets) > 0) {
for _, e := range res.Summary.MissingEnvRefs {
val, err := c.StdIO.GetStringInput(fmt.Sprintf("Please provide environment reference value for key %s", e), "")
if err != nil {
return err
}
req.EnvRefs[e] = val
}
for _, s := range res.Summary.MissingSecrets {
val, err := c.StdIO.GetSecret(fmt.Sprintf("Please provide secret value for key %s (optional, press enter to skip)", s), 0)
if err != nil {
return err
}
if val != "" {
req.Secrets[s] = val
}
}
// 2nd dry-run to see the diff after resolving all env/secret values.
res, err = c.ApplyTemplate(ctx).TemplateApply(req).Execute()
if err != nil {
return fmt.Errorf("failed to check template impact: %w", err)
}
}
if !params.Quiet {
if err := c.printDiff(res.Diff, params); err != nil {
return err
}
}
if c.StdIO.InputIsInteractive() && !params.Force {
if confirmed := c.StdIO.GetConfirm("Confirm application of the above resources"); !confirmed {
return errors.New("aborted application of template")
}
}
if !params.OverwriteConflicts && hasConflicts(res.Diff) {
return errors.New("template has conflicts with existing resources and cannot safely apply")
}
// Flip the dry-run flag and apply the template.
req.DryRun = false
res, err = c.ApplyTemplate(ctx).TemplateApply(req).Execute()
if err != nil {
return fmt.Errorf("failed to apply template: %w", err)
}
params.StackId = res.StackID
if !params.Quiet {
if err := c.printSummary(res.Summary, params); err != nil {
return err
}
}
return nil
}
func readTemplates(ctx context.Context, sources []TemplateSource) ([]api.TemplateApplyTemplate, error) {
templates := make([]api.TemplateApplyTemplate, 0, len(sources))
for _, source := range sources {
tmpl, err := source.Read(ctx)
if err != nil {
return nil, err
}
// We always send the templates as JSON.
tmpl.ContentType = "json"
templates = append(templates, tmpl)
}
return templates, nil
}
func (c Client) printDiff(diff api.TemplateSummaryDiff, params *Params) error {
if c.PrintAsJSON {
return c.PrintJSON(diff)
}
newDiffPrinter := func(title string, headers []string) *template.DiffPrinter {
return template.NewDiffPrinter(c.StdIO, params.RenderTableColors, params.RenderTableBorders).
Title(title).
SetHeaders(append([]string{"Metadata Name", "ID", "Resource Name"}, headers...)...)
}
if labels := diff.Labels; len(labels) > 0 {
printer := newDiffPrinter("Labels", []string{"Color", "Description"})
buildRow := func(metaName string, id string, lf api.TemplateSummaryDiffLabelFields) []string {
var desc string
if lf.Description != nil {
desc = *lf.Description
}
return []string{metaName, id, lf.Name, lf.Color, desc}
}
for _, l := range labels {
var oldRow, newRow []string
hexId := influxid.ID(l.Id).String()
if l.Old != nil {
oldRow = buildRow(l.TemplateMetaName, hexId, *l.Old)
}
if l.New != nil {
newRow = buildRow(l.TemplateMetaName, hexId, *l.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if bkts := diff.Buckets; len(bkts) > 0 {
printer := newDiffPrinter("Buckets", []string{"Retention Period", "Description", "Schema Type", "Num Measurements"})
buildRow := func(metaName string, id string, bf api.TemplateSummaryDiffBucketFields) []string {
var desc string
if bf.Description != nil {
desc = *bf.Description
}
var retention time.Duration
if len(bf.RetentionRules) > 0 {
retention = time.Duration(bf.RetentionRules[0].EverySeconds) * time.Second
}
schemaType := api.SCHEMATYPE_IMPLICIT
if bf.SchemaType != nil {
schemaType = *bf.SchemaType
}
return []string{metaName, id, bf.Name, retention.String(), desc, schemaType.String(), strconv.Itoa(len(bf.MeasurementSchemas))}
}
for _, b := range bkts {
var oldRow, newRow []string
hexId := influxid.ID(b.Id).String()
if b.Old != nil {
oldRow = buildRow(b.TemplateMetaName, hexId, *b.Old)
}
if b.New != nil {
newRow = buildRow(b.TemplateMetaName, hexId, *b.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if checks := diff.Checks; len(checks) > 0 {
printer := newDiffPrinter("Checks", []string{"Description"})
buildRow := func(metaName string, id string, cf api.TemplateSummaryDiffCheckFields) []string {
var desc string
if cf.Description != nil {
desc = *cf.Description
}
return []string{metaName, id, cf.Name, desc}
}
for _, c := range checks {
var oldRow, newRow []string
hexId := influxid.ID(c.Id).String()
if c.Old != nil {
oldRow = buildRow(c.TemplateMetaName, hexId, *c.Old)
}
if c.New != nil {
newRow = buildRow(c.TemplateMetaName, hexId, *c.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if dashboards := diff.Dashboards; len(dashboards) > 0 {
printer := newDiffPrinter("Dashboards", []string{"Description", "Num Charts"})
buildRow := func(metaName string, id string, df api.TemplateSummaryDiffDashboardFields) []string {
var desc string
if df.Description != nil {
desc = *df.Description
}
return []string{metaName, id, df.Name, desc, strconv.Itoa(len(df.Charts))}
}
for _, d := range dashboards {
var oldRow, newRow []string
hexId := influxid.ID(d.Id).String()
if d.Old != nil {
oldRow = buildRow(d.TemplateMetaName, hexId, *d.Old)
}
if d.New != nil {
newRow = buildRow(d.TemplateMetaName, hexId, *d.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if endpoints := diff.NotificationEndpoints; len(endpoints) > 0 {
printer := newDiffPrinter("Notification Endpoints", nil)
buildRow := func(metaName string, id string, nef api.TemplateSummaryDiffNotificationEndpointFields) []string {
return []string{metaName, id, nef.Name}
}
for _, e := range endpoints {
var oldRow, newRow []string
hexId := influxid.ID(e.Id).String()
if e.Old != nil {
oldRow = buildRow(e.TemplateMetaName, hexId, *e.Old)
}
if e.New != nil {
newRow = buildRow(e.TemplateMetaName, hexId, *e.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if rules := diff.NotificationRules; len(rules) > 0 {
printer := newDiffPrinter("Notification Rules", []string{"Description", "Every", "Offset", "Endpoint Name", "Endpoint ID", "Endpoint Type"})
buildRow := func(metaName string, id string, nrf api.TemplateSummaryDiffNotificationRuleFields) []string {
var desc string
if nrf.Description != nil {
desc = *nrf.Description
}
eid := influxid.ID(nrf.EndpointID).String()
return []string{metaName, id, nrf.Name, desc, nrf.Every, nrf.Offset, nrf.EndpointName, eid, nrf.EndpointType}
}
for _, r := range rules {
var oldRow, newRow []string
hexId := influxid.ID(r.Id).String()
if r.Old != nil {
oldRow = buildRow(r.TemplateMetaName, hexId, *r.Old)
}
if r.New != nil {
newRow = buildRow(r.TemplateMetaName, hexId, *r.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if teles := diff.TelegrafConfigs; len(teles) > 0 {
printer := newDiffPrinter("Telegraf Configurations", []string{"Description"})
buildRow := func(metaName string, id string, tc api.TemplateSummaryTelegrafConfig) []string {
var desc string
if tc.Description != nil {
desc = *tc.Description
}
return []string{metaName, id, tc.Name, desc}
}
for _, t := range teles {
var oldRow, newRow []string
hexId := influxid.ID(t.Id).String()
if t.Old != nil {
oldRow = buildRow(t.TemplateMetaName, hexId, *t.Old)
}
if t.New != nil {
newRow = buildRow(t.TemplateMetaName, hexId, *t.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if tasks := diff.Tasks; len(tasks) > 0 {
printer := newDiffPrinter("Tasks", []string{"Description", "Cycle"})
buildRow := func(metaName string, id string, tf api.TemplateSummaryDiffTaskFields) []string {
var desc string
if tf.Description != nil {
desc = *tf.Description
}
var timing string
if tf.Cron != nil {
timing = *tf.Cron
} else {
offset := time.Duration(0).String()
if tf.Offset != nil {
offset = *tf.Offset
}
// If `cron` isn't set, `every` must be set
timing = fmt.Sprintf("every: %s offset: %s", *tf.Every, offset)
}
return []string{metaName, id, tf.Name, desc, timing}
}
for _, t := range tasks {
var oldRow, newRow []string
hexId := influxid.ID(t.Id).String()
if t.Old != nil {
oldRow = buildRow(t.TemplateMetaName, hexId, *t.Old)
}
if t.New != nil {
newRow = buildRow(t.TemplateMetaName, hexId, *t.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if vars := diff.Variables; len(vars) > 0 {
printer := newDiffPrinter("Variables", []string{"Description", "Arg Type", "Arg Values"})
buildRow := func(metaName string, id string, vf api.TemplateSummaryDiffVariableFields) []string {
var desc, argType string
if vf.Description != nil {
desc = *vf.Description
}
if vf.Args != nil {
argType = vf.Args.Type
}
return []string{metaName, id, vf.Name, desc, argType, sprintVarArgs(vf.Args)}
}
for _, v := range vars {
var oldRow, newRow []string
hexId := influxid.ID(v.Id).String()
if v.Old != nil {
oldRow = buildRow(v.TemplateMetaName, hexId, *v.Old)
}
if v.New != nil {
newRow = buildRow(v.TemplateMetaName, hexId, *v.New)
}
printer.AppendDiff(oldRow, newRow)
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if mappings := diff.LabelMappings; len(mappings) > 0 {
printer := template.NewDiffPrinter(c.StdIO, params.RenderTableColors, params.RenderTableBorders).
Title("Label Associations").
SetHeaders("Resource Type", "Resource Meta Name", "Resource Name", "Resource ID", "Label Package Name", "Label Name", "Label ID")
for _, m := range mappings {
resId := influxid.ID(m.ResourceID).String()
labelId := influxid.ID(m.LabelID).String()
row := []string{m.ResourceType, m.ResourceName, resId, m.LabelTemplateMetaName, m.LabelName, labelId}
switch m.StateStatus {
case "new":
printer.AppendDiff(nil, row)
case "remove":
printer.AppendDiff(row, nil)
default:
printer.AppendDiff(row, row)
}
}
printer.Render()
}
return nil
}
func sprintVarArgs(args *api.TemplateSummaryVariableArgs) string {
if args == nil {
return "<nil>"
}
switch args.Type {
case "map":
b, err := json.Marshal(args.Values)
if err != nil {
return "{}"
}
return string(b)
case "constant":
values, ok := args.Values.([]interface{})
if !ok {
return "[]"
}
var out []string
for _, v := range values {
out = append(out, fmt.Sprintf("%q", v))
}
return fmt.Sprintf("[%s]", strings.Join(out, " "))
case "query":
values, ok := args.Values.(map[string]interface{})
if !ok {
return ""
}
qVal, ok := values["query"]
if !ok {
return ""
}
lVal, ok := values["language"]
if !ok {
return ""
}
return fmt.Sprintf("language=%q query=%q", lVal, qVal)
default:
}
return "unknown variable argument"
}
func hasConflicts(diff api.TemplateSummaryDiff) bool {
for _, l := range diff.Labels {
if l.StateStatus != "new" && l.Old != nil && l.New != nil && !reflect.DeepEqual(l.Old, l.New) {
return true
}
}
for _, b := range diff.Buckets {
if b.StateStatus != "new" && b.Old != nil && b.New != nil && !reflect.DeepEqual(b.Old, b.New) {
return true
}
}
for _, c := range diff.Checks {
if c.StateStatus != "new" && c.Old != nil && c.New != nil && !reflect.DeepEqual(c.Old, c.New) {
return true
}
}
for _, d := range diff.Dashboards {
if d.StateStatus != "new" && d.Old != nil && d.New != nil && !reflect.DeepEqual(d.Old, d.New) {
return true
}
}
for _, e := range diff.NotificationEndpoints {
if e.StateStatus != "new" && e.Old != nil && e.New != nil && !reflect.DeepEqual(e.Old, e.New) {
return true
}
}
for _, r := range diff.NotificationRules {
if r.StateStatus != "new" && r.Old != nil && r.New != nil && !reflect.DeepEqual(r.Old, r.New) {
return true
}
}
for _, t := range diff.TelegrafConfigs {
if t.StateStatus != "new" && t.Old != nil && t.New != nil && !reflect.DeepEqual(t.Old, t.New) {
return true
}
}
for _, t := range diff.Tasks {
if t.StateStatus != "new" && t.Old != nil && t.New != nil && !reflect.DeepEqual(t.Old, t.New) {
return true
}
}
for _, v := range diff.Variables {
if v.StateStatus != "new" && v.Old != nil && v.New != nil && !reflect.DeepEqual(v.Old, v.New) {
return true
}
}
return false
}
func (c Client) printSummary(summary api.TemplateSummaryResources, params *Params) error {
if c.PrintAsJSON {
return c.PrintJSON(struct {
StackID string `json:"stackID"`
Summary api.TemplateSummaryResources
}{
StackID: params.StackId,
Summary: summary,
})
}
newPrinter := func(title string, headers []string) *template.TablePrinter {
return template.NewTablePrinter(c.StdIO, params.RenderTableColors, params.RenderTableBorders).
Title(title).
SetHeaders(append([]string{"Package Name", "ID", "Resource Name"}, headers...)...)
}
if labels := summary.Labels; len(labels) > 0 {
printer := newPrinter("LABELS", []string{"Description", "Color"})
for _, l := range labels {
id := influxid.ID(l.Id).String()
var desc string
if l.Properties.Description != nil {
desc = *l.Properties.Description
}
printer.Append([]string{l.TemplateMetaName, id, l.Name, desc, l.Properties.Color})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if buckets := summary.Buckets; len(buckets) > 0 {
printer := newPrinter("BUCKETS", []string{"Retention", "Description", "Schema Type"})
for _, b := range buckets {
id := influxid.ID(b.Id).String()
var desc string
if b.Description != nil {
desc = *b.Description
}
rp := "inf"
if b.RetentionPeriod != 0 {
rp = time.Duration(b.RetentionPeriod).String()
}
schemaType := api.SCHEMATYPE_IMPLICIT
if b.SchemaType != nil {
schemaType = *b.SchemaType
}
printer.Append([]string{b.TemplateMetaName, id, b.Name, rp, desc, schemaType.String()})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if checks := summary.Checks; len(checks) > 0 {
printer := newPrinter("CHECKS", []string{"Description"})
for _, c := range checks {
id := influxid.ID(c.Id).String()
var desc string
if c.Description != nil {
desc = *c.Description
}
printer.Append([]string{c.TemplateMetaName, id, c.Name, desc})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if dashboards := summary.Dashboards; len(dashboards) > 0 {
printer := newPrinter("DASHBOARDS", []string{"Description"})
for _, d := range dashboards {
id := influxid.ID(d.Id).String()
var desc string
if d.Description != nil {
desc = *d.Description
}
printer.Append([]string{d.TemplateMetaName, id, d.Name, desc})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if endpoints := summary.NotificationEndpoints; len(endpoints) > 0 {
printer := newPrinter("NOTIFICATION ENDPOINTS", []string{"Description", "Status"})
for _, e := range endpoints {
id := influxid.ID(e.Id).String()
var desc string
if e.Description != nil {
desc = *e.Description
}
printer.Append([]string{e.TemplateMetaName, id, e.Name, desc, e.Status})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if rules := summary.NotificationRules; len(rules) > 0 {
printer := newPrinter("NOTIFICATION RULES", []string{"Description", "Every", "Offset", "Endpoint Name", "Endpoint ID", "Endpoint Type"})
for _, r := range rules {
id := influxid.ID(r.Id).String()
eid := influxid.ID(r.EndpointID).String()
var desc string
if r.Description != nil {
desc = *r.Description
}
printer.Append([]string{r.TemplateMetaName, id, r.Name, desc, r.Every, r.Offset, r.EndpointTemplateMetaName, eid, r.EndpointType})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if tasks := summary.Tasks; len(tasks) > 0 {
printer := newPrinter("TASKS", []string{"Description", "Cycle"})
for _, t := range tasks {
id := influxid.ID(t.Id).String()
var desc string
if t.Description != nil {
desc = *t.Description
}
var timing string
if t.Cron != nil {
timing = *t.Cron
} else {
offset := time.Duration(0).String()
if t.Offset != nil {
offset = *t.Offset
}
// If `cron` isn't set, `every` must be set
timing = fmt.Sprintf("every: %s offset: %s", *t.Every, offset)
}
printer.Append([]string{t.TemplateMetaName, id, t.Name, desc, timing})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if teles := summary.TelegrafConfigs; len(teles) > 0 {
printer := newPrinter("TELEGRAF CONFIGS", []string{"Description"})
for _, t := range teles {
var desc string
if t.TelegrafConfig.Description != nil {
desc = *t.TelegrafConfig.Description
}
printer.Append([]string{t.TemplateMetaName, t.TelegrafConfig.Id, t.TelegrafConfig.Name, desc})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if vars := summary.Variables; len(vars) > 0 {
printer := newPrinter("VARIABLES", []string{"Description", "Arg Type", "Arg Values"})
for _, v := range vars {
id := influxid.ID(v.Id).String()
var desc string
if v.Description != nil {
desc = *v.Description
}
var argType string
if v.Arguments != nil {
argType = v.Arguments.Type
}
printer.Append([]string{v.TemplateMetaName, id, v.Name, desc, argType, sprintVarArgs(v.Arguments)})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if mappings := summary.LabelMappings; len(mappings) > 0 {
printer := template.NewTablePrinter(c.StdIO, params.RenderTableColors, params.RenderTableBorders).
Title("LABEL ASSOCIATIONS").
SetHeaders("Resource Type", "Resource Name", "Resource ID", "Label Name", "Label ID")
for _, m := range mappings {
rid := influxid.ID(m.ResourceID).String()
lid := influxid.ID(m.LabelID).String()
printer.Append([]string{m.ResourceType, m.ResourceName, rid, m.LabelName, lid})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if secrets := summary.MissingSecrets; len(secrets) > 0 {
printer := template.NewTablePrinter(c.StdIO, params.RenderTableColors, params.RenderTableBorders).
Title("MISSING SECRETS").
SetHeaders("Secret Key")
for _, sk := range secrets {
printer.Append([]string{sk})
}
printer.Render()
_, _ = c.StdIO.Write([]byte("\n"))
}
if params.StackId != "" {
_, _ = c.StdIO.Write([]byte(fmt.Sprintf("Stack ID: %s\n", params.StackId)))
}
return nil
}

View File

@ -180,7 +180,7 @@ func SourceFromReader(r io.Reader, encoding TemplateEncoding) TemplateSource {
}
}
func (s TemplateSource) Read(ctx context.Context) ([]api.TemplateEntry, error) {
func (s TemplateSource) Read(ctx context.Context) (api.TemplateApplyTemplate, error) {
var entries []api.TemplateEntry
if err := func() error {
in, err := s.Open(ctx)
@ -210,8 +210,11 @@ func (s TemplateSource) Read(ctx context.Context) ([]api.TemplateEntry, error) {
}
return err
}(); err != nil {
return nil, fmt.Errorf("failed to read template(s) from %q: %w", s.Name, err)
return api.TemplateApplyTemplate{}, fmt.Errorf("failed to read template(s) from %q: %w", s.Name, err)
}
return entries, nil
return api.TemplateApplyTemplate{
Sources: []string{s.Name},
Contents: entries,
}, nil
}

View File

@ -424,10 +424,13 @@ spec:
t.Parallel()
source := apply.SourceFromReader(strings.NewReader(tc.data), tc.encoding)
tmpls, err := source.Read(context.Background())
tmpl, err := source.Read(context.Background())
require.NoError(t, err)
require.Equal(t, parsed, tmpls)
expected := api.TemplateApplyTemplate{
Sources: []string{source.Name},
Contents: parsed,
}
require.Equal(t, expected, tmpl)
})
}
}

273
cmd/influx/apply.go Normal file
View File

@ -0,0 +1,273 @@
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: &params.orgId,
},
&cli.StringFlag{
Name: "org, o",
Usage: "The name of the organization",
EnvVar: "INFLUX_ORG",
Destination: &params.orgName,
},
&cli.StringFlag{
Name: "stack-id",
Usage: "Stack ID to associate with template application",
Destination: &params.stackId,
},
&cli.StringSliceFlag{
Name: "file, f",
Usage: "Path to template file; Supports file paths or (deprecated) HTTP(S) URLs",
TakesFile: true,
Value: &params.inPaths,
},
&cli.StringSliceFlag{
Name: "template-url, u",
Usage: "HTTP(S) URL to template file",
Value: &params.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: &params.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: &params.encoding,
},
&cli.BoolFlag{
Name: "disable-color",
Usage: "Disable color in output",
Destination: &params.noColor,
},
&cli.BoolFlag{
Name: "disable-table-borders",
Usage: "Disable table borders",
Destination: &params.noTableBorders,
},
&cli.BoolFlag{
Name: "quiet, q",
Usage: "Disable output printing",
Destination: &params.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: &params.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: &params.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: &params.envVars,
},
&cli.StringSliceFlag{
Name: "filter",
Usage: "Resources to skip when applying the template. Filter out by `kind` or by `resource`",
Value: &params.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)
},
}
}

View File

@ -50,6 +50,7 @@ var app = cli.App{
newSecretCommand(),
newV1SubCommand(),
newAuthCommand(),
newApplyCmd(),
},
Before: withContext(),
}

View File

@ -120,6 +120,20 @@ func (mr *MockStdIOMockRecorder) GetStringInput(arg0, arg1 interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStringInput", reflect.TypeOf((*MockStdIO)(nil).GetStringInput), arg0, arg1)
}
// InputIsInteractive mocks base method.
func (m *MockStdIO) InputIsInteractive() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InputIsInteractive")
ret0, _ := ret[0].(bool)
return ret0
}
// InputIsInteractive indicates an expected call of InputIsInteractive.
func (mr *MockStdIOMockRecorder) InputIsInteractive() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InputIsInteractive", reflect.TypeOf((*MockStdIO)(nil).InputIsInteractive))
}
// Write mocks base method.
func (m *MockStdIO) Write(arg0 []byte) (int, error) {
m.ctrl.T.Helper()

View File

@ -7,6 +7,7 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/mattn/go-isatty"
)
// terminalStdio interacts with the user via an interactive terminal.
@ -55,6 +56,10 @@ func (t *terminalStdio) Error(message string) error {
return r.Error(&cfg, errors.New(message))
}
func (t *terminalStdio) InputIsInteractive() bool {
return isatty.IsTerminal(t.Stdin.Fd()) || isatty.IsCygwinTerminal(t.Stdin.Fd())
}
// GetStringInput prompts the user for arbitrary input.
func (t *terminalStdio) GetStringInput(prompt, defaultValue string) (input string, err error) {
question := survey.Input{

View File

@ -9,6 +9,7 @@ type StdIO interface {
WriteErr(p []byte) (n int, err error)
Banner(message string) error
Error(message string) error
InputIsInteractive() bool
GetStringInput(prompt, defaultValue string) (string, error)
GetSecret(prompt string, minLen int) (string, error)
GetPassword(prompt string) (string, error)