influxdb/cmd/influx/pkg.go

1117 lines
29 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/fatih/color"
"github.com/influxdata/influxdb"
ihttp "github.com/influxdata/influxdb/http"
ierror "github.com/influxdata/influxdb/kit/errors"
"github.com/influxdata/influxdb/pkger"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
input "github.com/tcnksm/go-input"
)
type pkgSVCsFn func() (pkger.SVC, influxdb.OrganizationService, error)
func cmdPkg(f *globalFlags, opts genericCLIOpts) *cobra.Command {
return newCmdPkgBuilder(newPkgerSVC, opts).cmd()
}
type cmdPkgBuilder struct {
genericCLIOpts
svcFn pkgSVCsFn
encoding string
file string
files []string
filters []string
disableColor bool
disableTableBorders bool
org organization
quiet bool
recurse bool
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
}
}
func newCmdPkgBuilder(svcFn pkgSVCsFn, opts genericCLIOpts) *cmdPkgBuilder {
return &cmdPkgBuilder{
genericCLIOpts: opts,
svcFn: svcFn,
}
}
func (b *cmdPkgBuilder) cmd() *cobra.Command {
cmd := b.cmdPkgApply()
cmd.AddCommand(
b.cmdPkgExport(),
b.cmdPkgSummary(),
b.cmdPkgValidate(),
)
return cmd
}
func (b *cmdPkgBuilder) cmdPkgApply() *cobra.Command {
cmd := b.newCmd("pkg", b.pkgApplyRunEFn)
cmd.Short = "Apply a pkg to create resources"
b.org.register(cmd, false)
b.registerPkgFileFlags(cmd)
cmd.Flags().BoolVarP(&b.quiet, "quiet", "q", false, "Disable output printing")
cmd.Flags().StringVar(&b.applyOpts.force, "force", "", `TTY input, if package will have destructive changes, proceed if set "true"`)
cmd.Flags().BoolVarP(&b.disableColor, "disable-color", "c", false, "Disable color in output")
cmd.Flags().BoolVar(&b.disableTableBorders, "disable-table-borders", false, "Disable table borders")
b.applyOpts.secrets = []string{}
cmd.Flags().StringSliceVar(&b.applyOpts.secrets, "secret", nil, "Secrets to provide alongside the package; 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 package; format should --env-ref=REF_KEY=REF_VALUE --env-ref=REF_KEY_2=REF_VALUE_2")
return cmd
}
func (b *cmdPkgBuilder) pkgApplyRunEFn(cmd *cobra.Command, args []string) error {
if err := b.org.validOrgFlags(); err != nil {
return err
}
color.NoColor = b.disableColor
svc, orgSVC, err := b.svcFn()
if err != nil {
return err
}
if err := b.org.validOrgFlags(); err != nil {
return err
}
influxOrgID, err := b.org.getID(orgSVC)
if err != nil {
return err
}
pkg, isTTY, err := b.readPkg()
if err != nil {
return err
}
providedEnvRefs := mapKeys(pkg.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, "")
}
}
drySum, diff, err := svc.DryRun(context.Background(), influxOrgID, 0, pkg, pkger.ApplyWithEnvRefs(providedEnvRefs))
if err != nil {
return err
}
providedSecrets := mapKeys(drySum.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 !b.quiet {
b.printPkgDiff(diff)
}
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 package")
return nil
}
}
if b.applyOpts.force != "conflict" && isTTY && diff.HasConflicts() {
return errors.New("package has conflicts with existing resources and cannot safely apply")
}
summary, err := svc.Apply(context.Background(), influxOrgID, 0, pkg, pkger.ApplyWithEnvRefs(providedEnvRefs), pkger.ApplyWithSecrets(providedSecrets))
if err != nil {
return err
}
if !b.quiet {
b.printPkgSummary(summary)
}
return nil
}
func (b *cmdPkgBuilder) cmdPkgExport() *cobra.Command {
cmd := b.newCmd("export", b.pkgExportRunEFn)
cmd.Short = "Export existing resources as a package"
cmd.AddCommand(b.cmdPkgExportAll())
cmd.Flags().StringVarP(&b.file, "file", "f", "", "output file for created pkg; defaults to std out if no file provided; the extension of provided file (.yml/.json) will dictate encoding")
cmd.Flags().StringVar(&b.exportOpts.resourceType, "resource-type", "", "The 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")
return cmd
}
func (b *cmdPkgBuilder) pkgExportRunEFn(cmd *cobra.Command, args []string) error {
pkgSVC, _, err := b.svcFn()
if err != nil {
return err
}
opts := []pkger.CreatePkgSetFn{}
resTypes := []struct {
kind pkger.Kind
idStrs []string
}{
{kind: pkger.KindBucket, idStrs: strings.Split(b.exportOpts.buckets, ",")},
{kind: pkger.KindCheck, idStrs: strings.Split(b.exportOpts.checks, ",")},
{kind: pkger.KindDashboard, idStrs: strings.Split(b.exportOpts.dashboards, ",")},
{kind: pkger.KindLabel, idStrs: strings.Split(b.exportOpts.labels, ",")},
{kind: pkger.KindNotificationEndpoint, idStrs: strings.Split(b.exportOpts.endpoints, ",")},
{kind: pkger.KindNotificationRule, idStrs: strings.Split(b.exportOpts.rules, ",")},
{kind: pkger.KindTask, idStrs: strings.Split(b.exportOpts.tasks, ",")},
{kind: pkger.KindTelegraf, idStrs: strings.Split(b.exportOpts.telegrafs, ",")},
{kind: pkger.KindVariable, idStrs: strings.Split(b.exportOpts.variables, ",")},
}
for _, rt := range resTypes {
newOpt, err := newResourcesToClone(rt.kind, rt.idStrs)
if err != nil {
return ierror.Wrap(err, rt.kind.String())
}
opts = append(opts, newOpt)
}
if b.exportOpts.resourceType == "" {
return b.writePkg(cmd.OutOrStdout(), pkgSVC, b.file, opts...)
}
kind := pkger.Kind(b.exportOpts.resourceType)
if err := kind.OK(); err != nil {
return errors.New("resource type must be one of bucket|dashboard|label|variable; got: " + b.exportOpts.resourceType)
}
if stdin, err := b.inStdIn(); err == nil {
stdinInpt, _ := b.readLines(stdin)
if len(stdinInpt) > 0 {
args = stdinInpt
}
}
resTypeOpt, err := newResourcesToClone(kind, args)
if err != nil {
return err
}
return b.writePkg(cmd.OutOrStdout(), pkgSVC, b.file, append(opts, resTypeOpt)...)
}
func (b *cmdPkgBuilder) cmdPkgExportAll() *cobra.Command {
cmd := b.newCmd("all", b.pkgExportAllRunEFn)
cmd.Short = "Export all existing resources for an organization as a package"
cmd.Flags().StringVarP(&b.file, "file", "f", "", "output file for created pkg; 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(cmd, false)
return cmd
}
func (b *cmdPkgBuilder) pkgExportAllRunEFn(cmd *cobra.Command, args []string) error {
pkgSVC, 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 "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.CreateWithAllOrgResources(pkger.CreateByOrgIDOpt{
OrgID: orgID,
LabelNames: labelNames,
ResourceKinds: resourceKinds,
})
return b.writePkg(cmd.OutOrStdout(), pkgSVC, b.file, orgOpt)
}
func (b *cmdPkgBuilder) cmdPkgSummary() *cobra.Command {
runE := func(cmd *cobra.Command, args []string) error {
pkg, _, err := b.readPkg()
if err != nil {
return err
}
b.printPkgSummary(pkg.Summary())
return nil
}
cmd := b.newCmd("summary", runE)
cmd.Short = "Summarize the provided package"
b.registerPkgFileFlags(cmd)
cmd.Flags().BoolVarP(&b.disableColor, "disable-color", "c", false, "Disable color in output")
cmd.Flags().BoolVar(&b.disableTableBorders, "disable-table-borders", false, "Disable table borders")
return cmd
}
func (b *cmdPkgBuilder) cmdPkgValidate() *cobra.Command {
runE := func(cmd *cobra.Command, args []string) error {
pkg, _, err := b.readPkg()
if err != nil {
return err
}
return pkg.Validate()
}
cmd := b.newCmd("validate", runE)
cmd.Short = "Validate the provided package"
b.registerPkgFileFlags(cmd)
return cmd
}
func (b *cmdPkgBuilder) registerPkgFileFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceVarP(&b.files, "file", "f", nil, "Path to package file")
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 manifests organized within the same directory.")
cmd.Flags().StringSliceVarP(&b.urls, "url", "u", nil, "URL to a package file")
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 *cmdPkgBuilder) writePkg(w io.Writer, pkgSVC pkger.SVC, outPath string, opts ...pkger.CreatePkgSetFn) error {
pkg, err := pkgSVC.CreatePkg(context.Background(), opts...)
if err != nil {
return err
}
buf, err := createPkgBuf(pkg, 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 *cmdPkgBuilder) readRawPkgsFromFiles(filePaths []string, recurse bool) ([]*pkger.Pkg, 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 rawPkgs []*pkger.Pkg
for f := range mFiles {
pkg, err := pkger.Parse(b.convertFileEncoding(f), pkger.FromFile(f), pkger.ValidSkipParseError())
if err != nil {
return nil, err
}
rawPkgs = append(rawPkgs, pkg)
}
return rawPkgs, nil
}
func (b *cmdPkgBuilder) readRawPkgsFromURLs(urls []string) ([]*pkger.Pkg, error) {
mURLs := make(map[string]struct{})
for _, f := range urls {
mURLs[f] = struct{}{}
}
var rawPkgs []*pkger.Pkg
for u := range mURLs {
pkg, err := pkger.Parse(b.convertURLEncoding(u), pkger.FromHTTPRequest(u), pkger.ValidSkipParseError())
if err != nil {
return nil, err
}
rawPkgs = append(rawPkgs, pkg)
}
return rawPkgs, nil
}
func (b *cmdPkgBuilder) readPkg() (*pkger.Pkg, bool, error) {
pkgs, err := b.readRawPkgsFromFiles(b.files, b.recurse)
if err != nil {
return nil, false, err
}
urlPkgs, err := b.readRawPkgsFromURLs(b.urls)
if err != nil {
return nil, false, err
}
pkgs = append(pkgs, urlPkgs...)
if _, err := b.inStdIn(); err != nil {
pkg, err := pkger.Combine(pkgs...)
return pkg, false, err
}
stdinPkg, err := pkger.Parse(b.convertEncoding(), pkger.FromReader(b.in), pkger.ValidSkipParseError())
if err != nil {
return nil, true, err
}
pkg, err := pkger.Combine(append(pkgs, stdinPkg)...)
return pkg, true, err
}
func (b *cmdPkgBuilder) inStdIn() (*os.File, error) {
stdin, _ := b.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
}
func (b *cmdPkgBuilder) 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 *cmdPkgBuilder) getInput(msg, defaultVal string) string {
ui := &input.UI{
Writer: b.w,
Reader: b.in,
}
return getInput(ui, msg, defaultVal)
}
func (b *cmdPkgBuilder) 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 *cmdPkgBuilder) 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 *cmdPkgBuilder) 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 []string) (pkger.CreatePkgSetFn, 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,
})
}
return pkger.CreateWithExistingResources(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 normedArg == "" {
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 createPkgBuf(pkg *pkger.Pkg, 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 := pkg.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 := &ihttp.OrganizationService{
Client: httpClient,
}
return &pkger.HTTPRemoteService{Client: httpClient}, orgSvc, nil
}
func (b *cmdPkgBuilder) printPkgDiff(diff pkger.Diff) {
red := color.New(color.FgRed).SprintFunc()
green := color.New(color.FgHiGreen, color.Bold).SprintFunc()
diffLn := func(isNew bool, old, new interface{}) string {
if isNew {
return green(new)
}
if reflect.DeepEqual(old, new) {
return fmt.Sprint(new)
}
return fmt.Sprintf("%s\n%s", red(old), green(new))
}
boolDiff := func(b bool) string {
bb := strconv.FormatBool(b)
if b {
return green(bb)
}
return bb
}
tablePrintFn := b.tablePrinterGen()
if labels := diff.Labels; len(labels) > 0 {
headers := []string{"New", "ID", "Name", "Color", "Description"}
tablePrintFn("LABELS", headers, len(labels), func(i int) []string {
l := labels[i]
var old pkger.DiffLabelValues
if l.Old != nil {
old = *l.Old
}
return []string{
boolDiff(l.IsNew()),
l.ID.String(),
l.Name,
diffLn(l.IsNew(), old.Color, l.New.Color),
diffLn(l.IsNew(), old.Description, l.New.Description),
}
})
}
if bkts := diff.Buckets; len(bkts) > 0 {
headers := []string{"New", "ID", "Name", "Retention Period", "Description"}
tablePrintFn("BUCKETS", headers, len(bkts), func(i int) []string {
b := bkts[i]
var old pkger.DiffBucketValues
if b.Old != nil {
old = *b.Old
}
return []string{
boolDiff(b.IsNew()),
b.ID.String(),
b.Name,
diffLn(b.IsNew(), old.RetentionRules.RP().String(), b.New.RetentionRules.RP().String()),
diffLn(b.IsNew(), old.Description, b.New.Description),
}
})
}
if checks := diff.Checks; len(checks) > 0 {
headers := []string{"New", "ID", "Name", "Description"}
tablePrintFn("CHECKS", headers, len(checks), func(i int) []string {
c := checks[i]
var oldDesc string
if c.Old != nil {
oldDesc = c.Old.GetDescription()
}
return []string{
boolDiff(c.IsNew()),
c.ID.String(),
c.Name,
diffLn(c.IsNew(), oldDesc, c.New.GetDescription()),
}
})
}
if dashes := diff.Dashboards; len(dashes) > 0 {
headers := []string{"New", "Name", "Description", "Num Charts"}
tablePrintFn("DASHBOARDS", headers, len(dashes), func(i int) []string {
d := dashes[i]
return []string{
boolDiff(true),
d.Name,
green(d.Desc),
green(strconv.Itoa(len(d.Charts))),
}
})
}
if endpoints := diff.NotificationEndpoints; len(endpoints) > 0 {
headers := []string{"New", "ID", "Name"}
tablePrintFn("NOTIFICATION ENDPOINTS", headers, len(endpoints), func(i int) []string {
v := endpoints[i]
return []string{
boolDiff(v.IsNew()),
v.ID.String(),
v.Name,
}
})
}
if rules := diff.NotificationRules; len(rules) > 0 {
headers := []string{"New", "Name", "Description", "Every", "Offset", "Endpoint Name", "Endpoint ID", "Endpoint Type"}
tablePrintFn("NOTIFICATION RULES", headers, len(rules), func(i int) []string {
v := rules[i]
return []string{
green(true),
v.Name,
v.Description,
v.Every,
v.Offset,
v.EndpointName,
v.EndpointID.String(),
v.EndpointType,
}
})
}
if teles := diff.Telegrafs; len(teles) > 0 {
headers := []string{"New", "Name", "Description"}
tablePrintFn("TELEGRAF CONFIGS", headers, len(teles), func(i int) []string {
t := teles[i]
return []string{
boolDiff(true),
t.Name,
green(t.Description),
}
})
}
if tasks := diff.Tasks; len(tasks) > 0 {
headers := []string{"New", "Name", "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{
boolDiff(true),
t.Name,
green(t.Description),
green(timing),
}
})
}
if vars := diff.Variables; len(vars) > 0 {
headers := []string{"New", "ID", "Name", "Description", "Arg Type", "Arg Values"}
tablePrintFn("VARIABLES", headers, len(vars), func(i int) []string {
v := vars[i]
var old pkger.DiffVariableValues
if v.Old != nil {
old = *v.Old
}
var oldArgType string
if old.Args != nil {
oldArgType = old.Args.Type
}
var newArgType string
if v.New.Args != nil {
newArgType = v.New.Args.Type
}
return []string{
boolDiff(v.IsNew()),
v.ID.String(),
v.Name,
diffLn(v.IsNew(), old.Description, v.New.Description),
diffLn(v.IsNew(), oldArgType, newArgType),
diffLn(v.IsNew(), printVarArgs(old.Args), printVarArgs(v.New.Args)),
}
})
}
if len(diff.LabelMappings) > 0 {
headers := []string{"New", "Resource Type", "Resource Name", "Resource ID", "Label Name", "Label ID"}
tablePrintFn("LABEL MAPPINGS", headers, len(diff.LabelMappings), func(i int) []string {
m := diff.LabelMappings[i]
return []string{
boolDiff(m.IsNew),
string(m.ResType),
m.ResName,
m.ResID.String(),
m.LabelName,
m.LabelID.String(),
}
})
}
}
func (b *cmdPkgBuilder) printPkgSummary(sum pkger.Summary) {
tablePrintFn := b.tablePrinterGen()
if labels := sum.Labels; len(labels) > 0 {
headers := []string{"ID", "Name", "Description", "Color"}
tablePrintFn("LABELS", headers, len(labels), func(i int) []string {
l := labels[i]
return []string{
l.ID.String(),
l.Name,
l.Properties.Description,
l.Properties.Color,
}
})
}
if buckets := sum.Buckets; len(buckets) > 0 {
headers := []string{"ID", "Name", "Retention", "Description"}
tablePrintFn("BUCKETS", headers, len(buckets), func(i int) []string {
bucket := buckets[i]
return []string{
bucket.ID.String(),
bucket.Name,
formatDuration(bucket.RetentionPeriod),
bucket.Description,
}
})
}
if checks := sum.Checks; len(checks) > 0 {
headers := []string{"ID", "Name", "Description"}
tablePrintFn("CHECKS", headers, len(checks), func(i int) []string {
c := checks[i].Check
return []string{
c.GetID().String(),
c.GetName(),
c.GetDescription(),
}
})
}
if dashes := sum.Dashboards; len(dashes) > 0 {
headers := []string{"ID", "Name", "Description"}
tablePrintFn("DASHBOARDS", headers, len(dashes), func(i int) []string {
d := dashes[i]
return []string{d.ID.String(), d.Name, d.Description}
})
}
if endpoints := sum.NotificationEndpoints; len(endpoints) > 0 {
headers := []string{"ID", "Name", "Description", "Status"}
tablePrintFn("NOTIFICATION ENDPOINTS", headers, len(endpoints), func(i int) []string {
v := endpoints[i]
return []string{
v.NotificationEndpoint.GetID().String(),
v.NotificationEndpoint.GetName(),
v.NotificationEndpoint.GetDescription(),
string(v.NotificationEndpoint.GetStatus()),
}
})
}
if rules := sum.NotificationRules; len(rules) > 0 {
headers := []string{"ID", "Name", "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.ID.String(),
v.Name,
v.Description,
v.Every,
v.Offset,
v.EndpointName,
v.EndpointID.String(),
v.EndpointType,
}
})
}
if tasks := sum.Tasks; len(tasks) > 0 {
headers := []string{"ID", "Name", "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.ID.String(),
t.Name,
t.Description,
timing,
}
})
}
if teles := sum.TelegrafConfigs; len(teles) > 0 {
headers := []string{"ID", "Name", "Description"}
tablePrintFn("TELEGRAF CONFIGS", headers, len(teles), func(i int) []string {
t := teles[i]
return []string{
t.TelegrafConfig.ID.String(),
t.TelegrafConfig.Name,
t.TelegrafConfig.Description,
}
})
}
if vars := sum.Variables; len(vars) > 0 {
headers := []string{"ID", "Name", "Description", "Arg Type", "Arg Values"}
tablePrintFn("VARIABLES", headers, len(vars), func(i int) []string {
v := vars[i]
args := v.Arguments
return []string{
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 MAPPINGS", 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]}
})
}
}
func (b *cmdPkgBuilder) 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 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 {
var colors []tablewriter.Colors
for i := 0; i < len(headers); i++ {
colors = append(colors, tablewriter.Color(tablewriter.FgHiCyanColor))
}
w.SetHeaderColor(colors...)
if len(headers) > 1 {
colors[len(colors)-2] = tablewriter.Color(tablewriter.FgHiBlueColor)
colors[len(colors)-1] = tablewriter.Color(tablewriter.FgHiBlueColor)
} else {
colors[0] = tablewriter.Color(tablewriter.FgHiBlueColor)
}
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 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 find(needle string, haystack []string) int {
for i, h := range haystack {
if strings.ToLower(h) == needle {
return i
}
}
return -1
}