feat: port influx config from influxdb (#90)

This commit is contained in:
Daniel Moran 2021-05-13 11:47:03 -04:00 committed by GitHub
parent 6a9f17d100
commit 99d573bdaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 693 additions and 37 deletions

256
cmd/influx/config.go Normal file
View File

@ -0,0 +1,256 @@
package main
import (
"fmt"
"os"
"path"
"github.com/influxdata/influx-cli/v2/internal/cmd/config"
iconfig "github.com/influxdata/influx-cli/v2/internal/config"
"github.com/urfave/cli/v2"
)
var configPathAndPrintFlags = append([]cli.Flag{&configPathFlag}, printFlags...)
func newConfigCmd() *cli.Command {
return &cli.Command{
Name: "config",
Usage: "Config management commands",
ArgsUsage: "[config name]",
Description: `
Providing no argument to the config command will print the active configuration.
When an argument is provided, the active config will be switched to the config with
a name matching that of the argument provided.
Examples:
# show active config
influx config
# set active config to previously active config
influx config -
# set active config
influx config $CONFIG_NAME
The influx config command displays the active InfluxDB connection configuration and
manages multiple connection configurations stored, by default, in ~/.influxdbv2/configs.
Each connection includes a URL, token, associated organization, and active setting.
InfluxDB reads the token from the active connection configuration, so you don't have
to manually enter a token to log into InfluxDB.
For information about the config command, see
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/
`,
Before: withCli(),
Flags: configPathAndPrintFlags,
Action: func(ctx *cli.Context) error {
prog := path.Base(os.Args[0])
if ctx.NArg() > 1 {
return fmt.Errorf("usage: %s config [config name]", prog)
}
client := config.Client{CLI: getCLI(ctx)}
if ctx.NArg() == 1 {
return client.SwitchActive(ctx.Args().Get(0))
}
return client.PrintActive()
},
Subcommands: []*cli.Command{
newConfigCreateCmd(),
newConfigDeleteCmd(),
newConfigUpdateCmd(),
newConfigListCmd(),
},
}
}
func newConfigCreateCmd() *cli.Command {
var cfg iconfig.Config
return &cli.Command{
Name: "create",
Usage: "Create config",
Description: `
The influx config create command creates a new InfluxDB connection configuration
and stores it in the configs file (by default, stored at ~/.influxdbv2/configs).
Examples:
# create a config and set it active
influx config create -a -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME
# create a config and without setting it active
influx config create -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME
For information about the config command, see
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/
and
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/create/
`,
Before: withCli(),
Flags: append(
configPathAndPrintFlags,
&cli.StringFlag{
Name: "config-name",
Usage: "Name for the new config",
Aliases: []string{"n"},
Required: true,
Destination: &cfg.Name,
},
&cli.StringFlag{
Name: "host-url",
Usage: "Base URL of the InfluxDB server the new config should target",
Aliases: []string{"u"},
Required: true,
Destination: &cfg.Host,
},
&cli.StringFlag{
Name: "token",
Usage: "Auth token to use when communicating with the InfluxDB server",
Aliases: []string{"t"},
Required: true,
Destination: &cfg.Token,
},
&cli.StringFlag{
Name: "org",
Usage: "Default organization name to use in the new config",
Aliases: []string{"o"},
Destination: &cfg.Org,
},
&cli.BoolFlag{
Name: "active",
Usage: "Set the new config as active",
Aliases: []string{"a"},
Destination: &cfg.Active,
},
),
Action: func(ctx *cli.Context) error {
client := config.Client{CLI: getCLI(ctx)}
return client.Create(cfg)
},
}
}
func newConfigDeleteCmd() *cli.Command {
return &cli.Command{
Name: "rm",
Aliases: []string{"delete", "remove"},
Usage: "Delete config",
ArgsUsage: "[cfg_name]...",
Description: `
The influx config delete command deletes an InfluxDB connection configuration from
the configs file (by default, stored at ~/.influxdbv2/configs).
Examples:
# delete a config
influx config rm $CFG_NAME
# delete multiple configs
influx config rm $CFG_NAME_1 $CFG_NAME_2
For information about the config command, see
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/
and
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/rm/
`,
Before: withCli(),
Flags: append([]cli.Flag{&configPathFlag}, printFlags...),
Action: func(ctx *cli.Context) error {
client := config.Client{CLI: getCLI(ctx)}
return client.Delete(ctx.Args().Slice())
},
}
}
func newConfigUpdateCmd() *cli.Command {
var cfg iconfig.Config
return &cli.Command{
Name: "set",
Aliases: []string{"update"},
Usage: "Update config",
Description: `
The influx config set command updates information in an InfluxDB connection
configuration in the configs file (by default, stored at ~/.influxdbv2/configs).
Examples:
# update a config and set active
influx config set -a -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME
# update a config and do not set to active
influx config set -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME
For information about the config command, see
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/
and
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/set/
`,
Before: withCli(),
Flags: append(
configPathAndPrintFlags,
&cli.StringFlag{
Name: "config-name",
Usage: "Name of the config to update",
Aliases: []string{"n"},
Required: true,
Destination: &cfg.Name,
},
&cli.StringFlag{
Name: "host-url",
Usage: "New URL to set on the config",
Aliases: []string{"u"},
Destination: &cfg.Host,
},
&cli.StringFlag{
Name: "token",
Usage: "New auth token to set on the config",
Aliases: []string{"t"},
Destination: &cfg.Token,
},
&cli.StringFlag{
Name: "org",
Usage: "New default organization to set on the config",
Aliases: []string{"o"},
Destination: &cfg.Org,
},
&cli.BoolFlag{
Name: "active",
Usage: "Set the config as active",
Aliases: []string{"a"},
Destination: &cfg.Active,
},
),
Action: func(ctx *cli.Context) error {
client := config.Client{CLI: getCLI(ctx)}
return client.Update(cfg)
},
}
}
func newConfigListCmd() *cli.Command {
return &cli.Command{
Name: "ls",
Aliases: []string{"list"},
Usage: "List configs",
Description: `
The influx config ls command lists all InfluxDB connection configurations
in the configs file (by default, stored at ~/.influxdbv2/configs). Each
connection configuration includes a URL, authentication token, and active
setting. An asterisk (*) indicates the active configuration.
Examples:
# list configs
influx config ls
# list configs with long alias
influx config list
For information about the config command, see
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/
and
https://docs.influxdata.com/influxdb/latest/reference/cli/influx/config/list/
`,
Before: withCli(),
Flags: configPathAndPrintFlags,
Action: func(ctx *cli.Context) error {
client := config.Client{CLI: getCLI(ctx)}
return client.List()
},
}
}

View File

@ -35,15 +35,15 @@ func init() {
}
var (
tokenFlag = "token"
hostFlag = "host"
skipVerifyFlag = "skip-verify"
traceIdFlag = "trace-debug-id"
configPathFlag = "configs-path"
configNameFlag = "active-config"
httpDebugFlag = "http-debug"
printJsonFlag = "json"
hideHeadersFlag = "hide-headers"
tokenFlagName = "token"
hostFlagName = "host"
skipVerifyFlagName = "skip-verify"
traceIdFlagName = "trace-debug-id"
configPathFlagName = "configs-path"
configNameFlagName = "active-config"
httpDebugFlagName = "http-debug"
printJsonFlagName = "json"
hideHeadersFlagName = "hide-headers"
)
// NOTE: urfave/cli has dedicated support for global flags, but it only parses those flags
@ -53,35 +53,37 @@ var (
//
// We replicate the pattern from the old CLI so existing scripts and docs stay valid.
// Flags used by all CLI commands.
var configPathFlag = cli.PathFlag{
Name: configPathFlagName,
Usage: "Path to the influx CLI configurations",
EnvVars: []string{"INFLUX_CLI_CONFIGS_PATH"},
}
// Flags used by all CLI commands that make HTTP requests.
var coreFlags = []cli.Flag{
&cli.StringFlag{
Name: hostFlag,
Name: hostFlagName,
Usage: "HTTP address of InfluxDB",
EnvVars: []string{"INFLUX_HOST"},
},
&cli.BoolFlag{
Name: skipVerifyFlag,
Name: skipVerifyFlagName,
Usage: "Skip TLS certificate chain and host name verification",
},
&cli.PathFlag{
Name: configPathFlag,
Usage: "Path to the influx CLI configurations",
EnvVars: []string{"INFLUX_CLI_CONFIGS_PATH"},
},
&configPathFlag,
&cli.StringFlag{
Name: configNameFlag,
Name: configNameFlagName,
Usage: "Config name to use for command",
Aliases: []string{"c"},
EnvVars: []string{"INFLUX_ACTIVE_CONFIG"},
},
&cli.StringFlag{
Name: traceIdFlag,
Name: traceIdFlagName,
Hidden: true,
EnvVars: []string{"INFLUX_TRACE_DEBUG_ID"},
},
&cli.BoolFlag{
Name: httpDebugFlag,
Name: httpDebugFlagName,
Hidden: true,
},
}
@ -89,12 +91,12 @@ var coreFlags = []cli.Flag{
// Flags used by commands that display API resources to the user.
var printFlags = []cli.Flag{
&cli.BoolFlag{
Name: printJsonFlag,
Name: printJsonFlagName,
Usage: "Output data as JSON",
EnvVars: []string{"INFLUX_OUTPUT_JSON"},
},
&cli.BoolFlag{
Name: hideHeadersFlag,
Name: hideHeadersFlagName,
Usage: "Hide the table headers in output data",
EnvVars: []string{"INFLUX_HIDE_HEADERS"},
},
@ -102,7 +104,7 @@ var printFlags = []cli.Flag{
// Flag used by commands that hit an authenticated API.
var commonTokenFlag = cli.StringFlag{
Name: tokenFlag,
Name: tokenFlagName,
Usage: "Authentication token",
Aliases: []string{"t"},
EnvVars: []string{"INFLUX_TOKEN"},
@ -115,7 +117,7 @@ var commonFlags = append(commonFlagsNoToken, &commonTokenFlag)
// newCli builds a CLI core that reads from stdin, writes to stdout/stderr, manages a local config store,
// and optionally tracks a trace ID specified over the CLI.
func newCli(ctx *cli.Context) (cmd.CLI, error) {
configPath := ctx.String(configPathFlag)
configPath := ctx.String(configPathFlagName)
var err error
if configPath == "" {
configPath, err = config.DefaultPath()
@ -125,8 +127,8 @@ func newCli(ctx *cli.Context) (cmd.CLI, error) {
}
configSvc := config.NewLocalConfigService(configPath)
var activeConfig config.Config
if ctx.IsSet(configNameFlag) {
if activeConfig, err = configSvc.SwitchActive(ctx.String(configNameFlag)); err != nil {
if ctx.IsSet(configNameFlagName) {
if activeConfig, err = configSvc.SwitchActive(ctx.String(configNameFlagName)); err != nil {
return cmd.CLI{}, err
}
} else if activeConfig, err = configSvc.Active(); err != nil {
@ -135,8 +137,8 @@ func newCli(ctx *cli.Context) (cmd.CLI, error) {
return cmd.CLI{
StdIO: stdio.TerminalStdio,
PrintAsJSON: ctx.Bool(printJsonFlag),
HideTableHeaders: ctx.Bool(hideHeadersFlag),
PrintAsJSON: ctx.Bool(printJsonFlagName),
HideTableHeaders: ctx.Bool(hideHeadersFlagName),
ActiveConfig: activeConfig,
ConfigService: configSvc,
}, nil
@ -149,11 +151,11 @@ func newApiClient(ctx *cli.Context, configSvc config.Service, injectToken bool)
if err != nil {
return nil, err
}
if ctx.IsSet(tokenFlag) {
cfg.Token = ctx.String(tokenFlag)
if ctx.IsSet(tokenFlagName) {
cfg.Token = ctx.String(tokenFlagName)
}
if ctx.IsSet(hostFlag) {
cfg.Host = ctx.String(hostFlag)
if ctx.IsSet(hostFlagName) {
cfg.Host = ctx.String(hostFlagName)
}
parsedHost, err := url.Parse(cfg.Host)
@ -162,7 +164,7 @@ func newApiClient(ctx *cli.Context, configSvc config.Service, injectToken bool)
}
clientTransport := http.DefaultTransport.(*http.Transport)
clientTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: ctx.Bool(skipVerifyFlag)}
clientTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: ctx.Bool(skipVerifyFlagName)}
apiConfig := api.NewConfiguration()
apiConfig.Host = parsedHost.Host
@ -172,16 +174,16 @@ func newApiClient(ctx *cli.Context, configSvc config.Service, injectToken bool)
if injectToken {
apiConfig.DefaultHeader["Authorization"] = fmt.Sprintf("Token %s", cfg.Token)
}
if ctx.IsSet(traceIdFlag) {
if ctx.IsSet(traceIdFlagName) {
// NOTE: This is circumventing our codegen. If the header we use for tracing ever changes,
// we'll need to manually update the string here to match.
//
// The alternative is to pass the trace ID to the business logic for every CLI command, and
// use codegen'd logic to set the header on every HTTP request. Early versions of the CLI
// used that technique, and we found it to be error-prone and easy to forget during testing.
apiConfig.DefaultHeader["Zap-Trace-Span"] = ctx.String(traceIdFlag)
apiConfig.DefaultHeader["Zap-Trace-Span"] = ctx.String(traceIdFlagName)
}
apiConfig.Debug = ctx.Bool(httpDebugFlag)
apiConfig.Debug = ctx.Bool(httpDebugFlagName)
return api.NewAPIClient(apiConfig), nil
}
@ -200,6 +202,7 @@ var app = cli.App{
newCompletionCmd(),
newBucketSchemaCmd(),
newQueryCmd(),
newConfigCmd(),
},
}

View File

@ -27,7 +27,7 @@ func newSetupCmd() *cli.Command {
Destination: &params.Password,
},
&cli.StringFlag{
Name: tokenFlag,
Name: tokenFlagName,
Usage: "Auth token to set on the initial user",
Aliases: []string{"t"},
EnvVars: []string{"INFLUX_TOKEN"},

View File

@ -0,0 +1,150 @@
package config
import (
"errors"
"fmt"
"net/url"
"github.com/influxdata/influx-cli/v2/internal/api"
"github.com/influxdata/influx-cli/v2/internal/cmd"
"github.com/influxdata/influx-cli/v2/internal/config"
)
var ErrInvalidHostUrlScheme = errors.New("a scheme of http or https must be provided for host url")
type Client struct {
cmd.CLI
}
func (c Client) SwitchActive(name string) error {
cfg, err := c.ConfigService.SwitchActive(name)
if err != nil {
return err
}
return c.printConfigs(configPrintOpts{config: &cfg})
}
func (c Client) PrintActive() error {
active, err := c.CLI.ConfigService.Active()
if err != nil {
return err
}
return c.printConfigs(configPrintOpts{config: &active})
}
func (c Client) Create(cfg config.Config) error {
name := cfg.Name
validated, err := validateHostUrl(cfg.Host)
if err != nil {
return fmt.Errorf("host URL %q is invalid: %w", cfg.Host, err)
}
cfg.Host = validated
cfg, err = c.ConfigService.CreateConfig(cfg)
if err != nil {
return fmt.Errorf("failed to create config %q: %w", name, err)
}
return c.printConfigs(configPrintOpts{config: &cfg})
}
func (c Client) Delete(names []string) error {
deleted := make(config.Configs)
for _, name := range names {
if name == "" {
continue
}
cfg, err := c.ConfigService.DeleteConfig(name)
if apiErr, ok := err.(*api.Error); ok && apiErr.Code == api.ERRORCODE_NOT_FOUND {
continue
} else if err != nil {
return err
}
deleted[name] = cfg
}
return c.printConfigs(configPrintOpts{configs: deleted, deleted: true})
}
func (c Client) Update(cfg config.Config) error {
name := cfg.Name
if cfg.Host != "" {
validated, err := validateHostUrl(cfg.Host)
if err != nil {
return fmt.Errorf("host URL %q is invalid: %w", cfg.Host, err)
}
cfg.Host = validated
}
cfg, err := c.ConfigService.UpdateConfig(cfg)
if err != nil {
return fmt.Errorf("failed to update config %q: %w", name, err)
}
return c.printConfigs(configPrintOpts{config: &cfg})
}
func (c Client) List() error {
cfgs, err := c.ConfigService.ListConfigs()
if err != nil {
return err
}
return c.printConfigs(configPrintOpts{configs: cfgs})
}
type configPrintOpts struct {
deleted bool
config *config.Config
configs config.Configs
}
func (c Client) printConfigs(opts configPrintOpts) error {
if c.PrintAsJSON {
var v interface{}
if opts.config != nil {
v = opts.config
} else {
v = opts.configs
}
return c.PrintJSON(v)
}
headers := []string{"Active", "Name", "URL", "Org"}
if opts.deleted {
headers = append(headers, "Deleted")
}
if opts.config != nil {
opts.configs = config.Configs{
opts.config.Name: *opts.config,
}
}
var rows []map[string]interface{}
for _, c := range opts.configs {
var active string
if c.Active {
active = "*"
}
row := map[string]interface{}{
"Active": active,
"Name": c.Name,
"URL": c.Host,
"Org": c.Org,
}
if opts.deleted {
row["Deleted"] = true
}
rows = append(rows, row)
}
return c.PrintTable(headers, rows...)
}
func validateHostUrl(hostUrl string) (string, error) {
u, err := url.Parse(hostUrl)
if err != nil {
return "", err
}
if u.Scheme != "http" && u.Scheme != "https" {
return "", ErrInvalidHostUrlScheme
}
return u.String(), nil
}

View File

@ -0,0 +1,247 @@
package config_test
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/influxdata/influx-cli/v2/internal/api"
"github.com/influxdata/influx-cli/v2/internal/cmd"
"github.com/influxdata/influx-cli/v2/internal/cmd/config"
iconfig "github.com/influxdata/influx-cli/v2/internal/config"
"github.com/influxdata/influx-cli/v2/internal/mock"
"github.com/influxdata/influx-cli/v2/internal/testutils"
"github.com/stretchr/testify/require"
)
func TestClient_SwitchActive(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdio := mock.NewMockStdIO(ctrl)
writtenBytes := bytes.Buffer{}
stdio.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes()
name := "foo"
cfg := iconfig.Config{
Name: name,
Active: true,
Host: "http://localhost:8086",
Token: "supersecret",
Org: "me",
}
svc := mock.NewMockConfigService(ctrl)
svc.EXPECT().SwitchActive(gomock.Eq(name)).Return(cfg, nil)
cli := config.Client{CLI: cmd.CLI{ConfigService: svc, StdIO: stdio}}
require.NoError(t, cli.SwitchActive(name))
testutils.MatchLines(t, []string{
`Active\s+Name\s+URL\s+Org`,
fmt.Sprintf(`\*\s+%s\s+%s\s+%s`, cfg.Name, cfg.Host, cfg.Org),
}, strings.Split(writtenBytes.String(), "\n"))
}
func TestClient_PrintActive(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdio := mock.NewMockStdIO(ctrl)
writtenBytes := bytes.Buffer{}
stdio.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes()
cfg := iconfig.Config{
Name: "foo",
Active: true,
Host: "http://localhost:8086",
Token: "supersecret",
Org: "me",
}
svc := mock.NewMockConfigService(ctrl)
svc.EXPECT().Active().Return(cfg, nil)
cli := config.Client{CLI: cmd.CLI{ConfigService: svc, StdIO: stdio}}
require.NoError(t, cli.PrintActive())
testutils.MatchLines(t, []string{
`Active\s+Name\s+URL\s+Org`,
fmt.Sprintf(`\*\s+%s\s+%s\s+%s`, cfg.Name, cfg.Host, cfg.Org),
}, strings.Split(writtenBytes.String(), "\n"))
}
func TestClient_Create(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdio := mock.NewMockStdIO(ctrl)
writtenBytes := bytes.Buffer{}
stdio.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes()
cfg := iconfig.Config{
Name: "foo",
Active: true,
Host: "http://localhost:8086",
Token: "supersecret",
Org: "me",
}
svc := mock.NewMockConfigService(ctrl)
svc.EXPECT().CreateConfig(cfg).Return(cfg, nil)
cli := config.Client{CLI: cmd.CLI{ConfigService: svc, StdIO: stdio}}
require.NoError(t, cli.Create(cfg))
testutils.MatchLines(t, []string{
`Active\s+Name\s+URL\s+Org`,
fmt.Sprintf(`\*\s+%s\s+%s\s+%s`, cfg.Name, cfg.Host, cfg.Org),
}, strings.Split(writtenBytes.String(), "\n"))
}
func TestClient_Delete(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in []string
registerExpectations func(service *mock.MockConfigService)
out []string
}{
{
name: "empty",
},
{
name: "one",
in: []string{"foo"},
registerExpectations: func(svc *mock.MockConfigService) {
svc.EXPECT().DeleteConfig(gomock.Eq("foo")).
Return(iconfig.Config{Name: "foo", Host: "bar", Org: "baz"}, nil)
},
out: []string{`^\s+foo\s+bar\s+baz\s+true`},
},
{
name: "many",
in: []string{"foo", "qux", "wibble"},
registerExpectations: func(svc *mock.MockConfigService) {
svc.EXPECT().DeleteConfig(gomock.Eq("foo")).
Return(iconfig.Config{Name: "foo", Host: "bar", Org: "baz"}, nil)
svc.EXPECT().DeleteConfig(gomock.Eq("qux")).
Return(iconfig.Config{}, &api.Error{Code: api.ERRORCODE_NOT_FOUND})
svc.EXPECT().DeleteConfig(gomock.Eq("wibble")).
Return(iconfig.Config{Name: "wibble", Host: "bar", Active: true}, nil)
},
out: []string{
`^\s+foo\s+bar\s+baz\s+true`,
`\*\s+wibble\s+bar\s+true`,
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdio := mock.NewMockStdIO(ctrl)
writtenBytes := bytes.Buffer{}
stdio.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes()
svc := mock.NewMockConfigService(ctrl)
if tc.registerExpectations != nil {
tc.registerExpectations(svc)
}
cli := config.Client{CLI: cmd.CLI{ConfigService: svc, StdIO: stdio}}
require.NoError(t, cli.Delete(tc.in))
testutils.MatchLines(t,
append([]string{`Active\s+Name\s+URL\s+Org\s+Deleted`}, tc.out...),
strings.Split(writtenBytes.String(), "\n"))
})
}
}
func TestClient_Update(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdio := mock.NewMockStdIO(ctrl)
writtenBytes := bytes.Buffer{}
stdio.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes()
updates := iconfig.Config{
Name: "foo",
Active: true,
Token: "doublesecret",
}
cfg := iconfig.Config{
Name: updates.Name,
Active: updates.Active,
Host: "http://localhost:8086",
Token: updates.Token,
Org: "me",
}
svc := mock.NewMockConfigService(ctrl)
svc.EXPECT().UpdateConfig(updates).Return(cfg, nil)
cli := config.Client{CLI: cmd.CLI{ConfigService: svc, StdIO: stdio}}
require.NoError(t, cli.Update(updates))
testutils.MatchLines(t, []string{
`Active\s+Name\s+URL\s+Org`,
fmt.Sprintf(`\*\s+%s\s+%s\s+%s`, cfg.Name, cfg.Host, cfg.Org),
}, strings.Split(writtenBytes.String(), "\n"))
}
func TestClient_List(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
cfgs iconfig.Configs
expected []string
}{
{
name: "empty",
},
{
name: "one",
cfgs: iconfig.Configs{
"foo": iconfig.Config{Name: "foo", Host: "bar", Org: "baz"},
},
expected: []string{`\s+foo\s+bar\s+baz`},
},
{
name: "many",
cfgs: iconfig.Configs{
"foo": iconfig.Config{Name: "foo", Host: "bar", Org: "baz"},
"wibble": iconfig.Config{Name: "wibble", Host: "bar", Active: true},
},
expected: []string{
`\s+foo\s+bar\s+baz`,
`\*\s+wibble\s+bar`,
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
stdio := mock.NewMockStdIO(ctrl)
writtenBytes := bytes.Buffer{}
stdio.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes()
svc := mock.NewMockConfigService(ctrl)
svc.EXPECT().ListConfigs().Return(tc.cfgs, nil)
cli := config.Client{CLI: cmd.CLI{ConfigService: svc, StdIO: stdio}}
require.NoError(t, cli.List())
// Can't use our usual 'MatchLines' because list output depends on map iteration,
// so the order isn't well-defined.
out := writtenBytes.String()
for _, l := range append([]string{`Active\s+Name\s+URL\s+Org`}, tc.expected...) {
require.Regexp(t, l, out)
}
})
}
}