1513 lines
39 KiB
Go
1513 lines
39 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/influxdata/influxdb/v2"
|
|
ihttp "github.com/influxdata/influxdb/v2/http"
|
|
ierror "github.com/influxdata/influxdb/v2/kit/errors"
|
|
"github.com/influxdata/influxdb/v2/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
|
|
description string
|
|
disableColor bool
|
|
disableTableBorders bool
|
|
hideHeaders bool
|
|
json bool
|
|
name string
|
|
org organization
|
|
quiet bool
|
|
recurse bool
|
|
stackID 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
|
|
}
|
|
}
|
|
|
|
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(),
|
|
b.cmdStack(),
|
|
)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (b *cmdPkgBuilder) cmdPkgApply() *cobra.Command {
|
|
cmd := b.newCmd("pkg", b.pkgApplyRunEFn, true)
|
|
cmd.Short = "Apply a pkg to create resources"
|
|
|
|
b.org.register(cmd, false)
|
|
b.registerPkgFileFlags(cmd)
|
|
b.registerPkgPrintOpts(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().StringVar(&b.stackID, "stack-id", "", "Stack ID to associate pkg application")
|
|
|
|
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(&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
|
|
}
|
|
|
|
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, "")
|
|
}
|
|
}
|
|
|
|
var stackID influxdb.ID
|
|
if b.stackID != "" {
|
|
if err := stackID.DecodeFromString(b.stackID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
opts := []pkger.ApplyOptFn{
|
|
pkger.ApplyWithEnvRefs(providedEnvRefs),
|
|
pkger.ApplyWithStackID(stackID),
|
|
}
|
|
|
|
drySum, diff, err := svc.DryRun(context.Background(), influxOrgID, 0, pkg, opts...)
|
|
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 err := b.printPkgDiff(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 package")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if b.applyOpts.force != "conflict" && isTTY && diff.HasConflicts() {
|
|
return errors.New("package has conflicts with existing resources and cannot safely apply")
|
|
}
|
|
|
|
opts = append(opts, pkger.ApplyWithSecrets(providedSecrets))
|
|
|
|
summary, _, err := svc.Apply(context.Background(), influxOrgID, 0, pkg, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.printPkgSummary(summary)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *cmdPkgBuilder) cmdPkgExport() *cobra.Command {
|
|
cmd := b.newCmd("export", b.pkgExportRunEFn, true)
|
|
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, true)
|
|
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
|
|
}
|
|
|
|
return b.printPkgSummary(pkg.Summary())
|
|
}
|
|
|
|
cmd := b.newCmd("summary", runE, false)
|
|
cmd.Short = "Summarize the provided package"
|
|
|
|
b.registerPkgFileFlags(cmd)
|
|
b.registerPkgPrintOpts(cmd)
|
|
|
|
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, false)
|
|
cmd.Short = "Validate the provided package"
|
|
|
|
b.registerPkgFileFlags(cmd)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (b *cmdPkgBuilder) cmdStack() *cobra.Command {
|
|
cmd := b.newCmd("stack", nil, false)
|
|
cmd.Short = "Stack management commands"
|
|
cmd.AddCommand(b.cmdStackInit())
|
|
return cmd
|
|
}
|
|
|
|
func (b *cmdPkgBuilder) cmdStackInit() *cobra.Command {
|
|
cmd := b.newCmd("init", b.stackInitRunEFn, true)
|
|
cmd.Short = "Initialize a stack"
|
|
|
|
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, "package-url", "u", nil, "Package urls to associate with new stack")
|
|
registerPrintOptions(cmd, &b.hideHeaders, &b.json)
|
|
|
|
b.org.register(cmd, false)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (b *cmdPkgBuilder) stackInitRunEFn(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
|
|
}
|
|
|
|
const fakeUserID = 0 // is 0 because user is pulled from token...
|
|
stack, err := pkgSVC.InitStack(context.Background(), fakeUserID, pkger.Stack{
|
|
OrgID: orgID,
|
|
Name: b.name,
|
|
Description: b.description,
|
|
URLs: b.urls,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.json {
|
|
return b.writeJSON(stack)
|
|
}
|
|
|
|
tabW := b.newTabWriter()
|
|
defer tabW.Flush()
|
|
|
|
tabW.HideHeaders(b.hideHeaders)
|
|
|
|
tabW.WriteHeaders("ID", "OrgID", "Name", "Description", "URLs", "Created At")
|
|
tabW.Write(map[string]interface{}{
|
|
"ID": stack.ID,
|
|
"OrgID": stack.OrgID,
|
|
"Name": stack.Name,
|
|
"Description": stack.Description,
|
|
"URLs": stack.URLs,
|
|
"Created At": stack.CreatedAt,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *cmdPkgBuilder) registerPkgPrintOpts(cmd *cobra.Command) {
|
|
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")
|
|
registerPrintOptions(cmd, nil, &b.json)
|
|
}
|
|
|
|
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...)
|
|
|
|
// the pkger.ValidSkipParseError option allows our server to be the one to validate the
|
|
// the pkg 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 := b.inStdIn(); err != nil {
|
|
pkg, err := pkger.Combine(pkgs, pkger.ValidSkipParseError())
|
|
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), pkger.ValidSkipParseError())
|
|
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) error {
|
|
if b.quiet {
|
|
return nil
|
|
}
|
|
|
|
if b.json {
|
|
return b.writeJSON(diff)
|
|
}
|
|
|
|
diffPrinterGen := func(title string, headers []string) *diffPrinter {
|
|
commonHeaders := []string{"Package 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, pkgName string, v pkger.DiffLabelValues) []string {
|
|
return []string{pkgName, id.String(), v.Name, v.Color, v.Description}
|
|
}
|
|
|
|
for _, l := range labels {
|
|
var oldRow []string
|
|
if l.Old != nil {
|
|
oldRow = appendValues(l.ID, l.PkgName, *l.Old)
|
|
}
|
|
|
|
newRow := appendValues(l.ID, l.PkgName, l.New)
|
|
switch {
|
|
case l.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case l.Remove:
|
|
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, pkgName string, v pkger.DiffBucketValues) []string {
|
|
return []string{pkgName, 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.PkgName, *b.Old)
|
|
}
|
|
|
|
newRow := appendValues(b.ID, b.PkgName, b.New)
|
|
switch {
|
|
case b.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case b.Remove:
|
|
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, pkgName string, v pkger.DiffCheckValues) []string {
|
|
out := []string{pkgName, 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.PkgName, *c.Old)
|
|
}
|
|
|
|
newRow := appendValues(c.ID, c.PkgName, c.New)
|
|
switch {
|
|
case c.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case c.Remove:
|
|
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, pkgName string, v pkger.DiffDashboardValues) []string {
|
|
return []string{pkgName, 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.PkgName, *d.Old)
|
|
}
|
|
|
|
newRow := appendValues(d.ID, d.PkgName, d.New)
|
|
switch {
|
|
case d.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case d.Remove:
|
|
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, pkgName string, v pkger.DiffNotificationEndpointValues) []string {
|
|
out := []string{pkgName, 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.PkgName, *e.Old)
|
|
}
|
|
|
|
newRow := appendValues(e.ID, e.PkgName, e.New)
|
|
switch {
|
|
case e.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case e.Remove:
|
|
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, pkgName string, v pkger.DiffNotificationRuleValues) []string {
|
|
return []string{
|
|
pkgName,
|
|
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.PkgName, *e.Old)
|
|
}
|
|
|
|
newRow := appendValues(e.ID, e.PkgName, e.New)
|
|
switch {
|
|
case e.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case e.Remove:
|
|
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, pkgName string, v influxdb.TelegrafConfig) []string {
|
|
return []string{pkgName, id.String(), v.Name, v.Description}
|
|
}
|
|
|
|
for _, e := range teles {
|
|
var oldRow []string
|
|
if e.Old != nil {
|
|
oldRow = appendValues(e.ID, e.PkgName, *e.Old)
|
|
}
|
|
|
|
newRow := appendValues(e.ID, e.PkgName, e.New)
|
|
switch {
|
|
case e.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case e.Remove:
|
|
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, pkgName string, v pkger.DiffTaskValues) []string {
|
|
timing := v.Cron
|
|
if v.Cron == "" {
|
|
timing = fmt.Sprintf("every: %s offset: %s", v.Every, v.Offset)
|
|
}
|
|
return []string{pkgName, id.String(), v.Name, v.Description, timing}
|
|
}
|
|
|
|
for _, e := range tasks {
|
|
var oldRow []string
|
|
if e.Old != nil {
|
|
oldRow = appendValues(e.ID, e.PkgName, *e.Old)
|
|
}
|
|
|
|
newRow := appendValues(e.ID, e.PkgName, e.New)
|
|
switch {
|
|
case e.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case e.Remove:
|
|
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, pkgName string, v pkger.DiffVariableValues) []string {
|
|
var argType string
|
|
if v.Args != nil {
|
|
argType = v.Args.Type
|
|
}
|
|
return []string{pkgName, 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.PkgName, *v.Old)
|
|
}
|
|
|
|
newRow := appendValues(v.ID, v.PkgName, v.New)
|
|
switch {
|
|
case v.IsNew():
|
|
printer.AppendDiff(nil, newRow)
|
|
case v.Remove:
|
|
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 Package Name", "Resource Name", "Resource ID",
|
|
"Label Package Name", "Label Name", "Label ID",
|
|
)
|
|
|
|
for _, m := range diff.LabelMappings {
|
|
newRow := []string{
|
|
string(m.ResType),
|
|
m.ResPkgName, m.ResName, m.ResID.String(),
|
|
m.LabelPkgName, m.LabelName, m.LabelID.String(),
|
|
}
|
|
oldRow := newRow
|
|
if pkger.IsNew(m.StateStatus) {
|
|
oldRow = nil
|
|
}
|
|
printer.AppendDiff(oldRow, newRow)
|
|
}
|
|
printer.Render()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *cmdPkgBuilder) printPkgSummary(sum pkger.Summary) error {
|
|
if b.quiet {
|
|
return nil
|
|
}
|
|
|
|
if b.json {
|
|
return b.writeJSON(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.PkgName,
|
|
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.PkgName,
|
|
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].PkgName,
|
|
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 := append(commonHeaders, "Description", "Status")
|
|
tablePrintFn("NOTIFICATION ENDPOINTS", headers, len(endpoints), func(i int) []string {
|
|
v := endpoints[i]
|
|
return []string{
|
|
v.PkgName,
|
|
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.EndpointPkgName,
|
|
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 := 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.PkgName,
|
|
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]}
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 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
|
|
}
|