diff --git a/CHANGELOG.md b/CHANGELOG.md index c31b37368a7..68ffdf68fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ 1. [17114](https://github.com/influxdata/influxdb/pull/17114): Allow for retention to be provided to influx setup command as a duration 1. [17138](https://github.com/influxdata/influxdb/pull/17138): Extend pkger export all capabilities to support filtering by lable name and resource type 1. [17049](https://github.com/influxdata/influxdb/pull/17049): Added new login and sign-up screen that for cloud users that allows direct login from their region +1. [17170](https://github.com/influxdata/influxdb/pull/17170): Added new cli multiple profiles management tool ### Bug Fixes diff --git a/cmd/influx/authorization.go b/cmd/influx/authorization.go index 2e6a0ba6ce9..be068f1f485 100644 --- a/cmd/influx/authorization.go +++ b/cmd/influx/authorization.go @@ -106,7 +106,7 @@ func authCreateCmd() *cobra.Command { } func authorizationCreateF(cmd *cobra.Command, args []string) error { - if err := authCreateFlags.org.validOrgFlags(); err != nil { + if err := authCreateFlags.org.validOrgFlags(&flags); err != nil { return err } diff --git a/cmd/influx/backup.go b/cmd/influx/backup.go index 2cc734758f8..e9d8c541d17 100644 --- a/cmd/influx/backup.go +++ b/cmd/influx/backup.go @@ -55,8 +55,8 @@ func init() { func newBackupService() (influxdb.BackupService, error) { return &http.BackupService{ - Addr: flags.host, - Token: flags.token, + Addr: flags.Host, + Token: flags.Token, }, nil } diff --git a/cmd/influx/bucket.go b/cmd/influx/bucket.go index db91c145181..9b1d171719a 100644 --- a/cmd/influx/bucket.go +++ b/cmd/influx/bucket.go @@ -78,7 +78,7 @@ func (b *cmdBucketBuilder) cmdCreate() *cobra.Command { } func (b *cmdBucketBuilder) cmdCreateRunEFn(*cobra.Command, []string) error { - if err := b.org.validOrgFlags(); err != nil { + if err := b.org.validOrgFlags(b.globalFlags); err != nil { return err } @@ -183,7 +183,7 @@ func (b *cmdBucketBuilder) cmdFind() *cobra.Command { } func (b *cmdBucketBuilder) cmdFindRunEFn(cmd *cobra.Command, args []string) error { - if err := b.org.validOrgFlags(); err != nil { + if err := b.org.validOrgFlags(b.globalFlags); err != nil { return err } diff --git a/cmd/influx/config.go b/cmd/influx/config.go new file mode 100644 index 00000000000..60753e4f6a8 --- /dev/null +++ b/cmd/influx/config.go @@ -0,0 +1,265 @@ +package main + +import ( + "fmt" + + "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb/cmd/influx/config" + "github.com/spf13/cobra" +) + +func cmdConfig(f *globalFlags, opt genericCLIOpts) *cobra.Command { + path, dir, err := defaultConfigPath() + if err != nil { + panic(err) + } + builder := cmdConfigBuilder{ + genericCLIOpts: opt, + globalFlags: f, + svc: config.LocalConfigsSVC{ + Path: path, + Dir: dir, + }, + } + builder.globalFlags = f + return builder.cmd() +} + +type cmdConfigBuilder struct { + genericCLIOpts + *globalFlags + + name string + url string + token string + active bool + org string + + svc config.ConfigsService +} + +func (b *cmdConfigBuilder) cmd() *cobra.Command { + cmd := b.newCmd("config", nil) + cmd.Short = "Config management commands" + cmd.Run = seeHelp + cmd.AddCommand( + b.cmdCreate(), + b.cmdDelete(), + b.cmdUpdate(), + b.cmdList(), + ) + return cmd +} + +func (b *cmdConfigBuilder) cmdCreate() *cobra.Command { + cmd := b.newCmd("create", nil) + cmd.RunE = b.cmdCreateRunEFn + cmd.Short = "Create config" + cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)") + cmd.MarkFlagRequired("name") + cmd.Flags().StringVarP(&b.token, "token", "t", "", "The config token (required)") + cmd.MarkFlagRequired("token") + cmd.Flags().StringVarP(&b.url, "url", "u", "", "The config url (required)") + cmd.MarkFlagRequired("url") + + cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active config") + cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name") + return cmd +} + +func (b *cmdConfigBuilder) cmdCreateRunEFn(*cobra.Command, []string) error { + pp, err := b.svc.ParseConfigs() + if err != nil { + return err + } + p := config.Config{ + Host: b.url, + Token: b.token, + Org: b.org, + Active: b.active, + } + if _, ok := pp[b.name]; ok { + return &influxdb.Error{ + Code: influxdb.EConflict, + Msg: fmt.Sprintf("name %q already exists", b.name), + } + } + pp[b.name] = p + active := "" + if p.Active { + active = "*" + if err := pp.Switch(b.name); err != nil { + return err + } + } + if err = b.svc.WriteConfigs(pp); err != nil { + return err + } + w := b.newTabWriter() + w.WriteHeaders( + "Active", + "Name", + "URL", + "Org", + "Created", + ) + + w.Write(map[string]interface{}{ + "Active": active, + "Name": b.name, + "URL": p.Host, + "Org": p.Org, + "Created": true, + }) + w.Flush() + return nil +} + +func (b *cmdConfigBuilder) cmdDelete() *cobra.Command { + cmd := b.newCmd("delete", nil) + cmd.RunE = b.cmdDeleteRunEFn + cmd.Short = "Delete config" + + cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)") + cmd.MarkFlagRequired("name") + + return cmd +} + +func (b *cmdConfigBuilder) cmdDeleteRunEFn(cmd *cobra.Command, args []string) error { + pp, err := b.svc.ParseConfigs() + if err != nil { + return err + } + p, ok := pp[b.name] + if !ok { + return &influxdb.Error{ + Code: influxdb.ENotFound, + Msg: fmt.Sprintf("name %q is not found", b.name), + } + } + delete(pp, b.name) + if err = b.svc.WriteConfigs(pp); err != nil { + return err + } + w := b.newTabWriter() + w.WriteHeaders( + "Name", + "URL", + "Org", + "Deleted", + ) + + w.Write(map[string]interface{}{ + "Name": b.name, + "URL": p.Host, + "Org": p.Org, + "Deleted": true, + }) + w.Flush() + return nil +} + +func (b *cmdConfigBuilder) cmdUpdate() *cobra.Command { + cmd := b.newCmd("set", b.cmdUpdateRunEFn) + cmd.Aliases = []string{"update"} + cmd.RunE = b.cmdUpdateRunEFn + cmd.Short = "Update config" + cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)") + cmd.MarkFlagRequired("name") + + cmd.Flags().StringVarP(&b.token, "token", "t", "", "The config token (required)") + cmd.Flags().StringVarP(&b.url, "url", "u", "", "The config url (required)") + cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active config") + cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name") + return cmd +} + +func (b *cmdConfigBuilder) cmdUpdateRunEFn(*cobra.Command, []string) error { + pp, err := b.svc.ParseConfigs() + if err != nil { + return err + } + p0, ok := pp[b.name] + if !ok { + return &influxdb.Error{ + Code: influxdb.ENotFound, + Msg: fmt.Sprintf("name %q is not found", b.name), + } + } + if b.token != "" { + p0.Token = b.token + } + if b.url != "" { + p0.Host = b.url + } + if b.org != "" { + p0.Org = b.org + } + pp[b.name] = p0 + active := "" + if b.active { + active = "*" + if err := pp.Switch(b.name); err != nil { + return err + } + } + if err = b.svc.WriteConfigs(pp); err != nil { + return err + } + w := b.newTabWriter() + w.WriteHeaders( + "Active", + "Name", + "URL", + "Org", + "Updated", + ) + + w.Write(map[string]interface{}{ + "Active": active, + "Name": b.name, + "URL": p0.Host, + "Org": p0.Org, + "Updated": true, + }) + w.Flush() + return nil +} + +func (b *cmdConfigBuilder) cmdList() *cobra.Command { + cmd := b.newCmd("list", nil) + cmd.RunE = b.cmdListRunEFn + cmd.Aliases = []string{"ls"} + cmd.Short = "List configs" + return cmd +} + +func (b *cmdConfigBuilder) cmdListRunEFn(*cobra.Command, []string) error { + pp, err := b.svc.ParseConfigs() + if err != nil { + return err + } + w := b.newTabWriter() + w.WriteHeaders( + "Active", + "Name", + "URL", + "Org", + ) + for n, p := range pp { + var active string + if p.Active { + active = "*" + } + w.Write(map[string]interface{}{ + "Active": active, + "Name": n, + "URL": p.Host, + "Org": p.Org, + }) + } + + w.Flush() + return nil +} diff --git a/cmd/influx/config/config.go b/cmd/influx/config/config.go new file mode 100644 index 00000000000..8882acd8c34 --- /dev/null +++ b/cmd/influx/config/config.go @@ -0,0 +1,136 @@ +package config + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/BurntSushi/toml" + "github.com/influxdata/influxdb" +) + +// Config store the crendentials of influxdb host and token. +type Config struct { + Host string `toml:"url"` + // Token is base64 encoded sequence. + Token string `toml:"token"` + Org string `toml:"org,omitempty"` + Active bool `toml:"active,omitempty"` +} + +// DefaultConfig is default config without token +var DefaultConfig = Config{ + Host: "http://localhost:9999", + Active: true, +} + +// Configs is map of configs indexed by name. +type Configs map[string]Config + +// ConfigsService is the service to list and write configs. +type ConfigsService interface { + WriteConfigs(pp Configs) error + ParseConfigs() (Configs, error) +} + +// Switch to another config. +func (pp *Configs) Switch(name string) error { + pc := *pp + if _, ok := pc[name]; !ok { + return &influxdb.Error{ + Code: influxdb.ENotFound, + Msg: fmt.Sprintf(`config %q is not found`, name), + } + } + for k, v := range pc { + v.Active = k == name + pc[k] = v + } + return nil +} + +// LocalConfigsSVC has the path and dir to write and parse configs. +type LocalConfigsSVC struct { + Path string + Dir string +} + +// ParseConfigs from the local path. +func (svc LocalConfigsSVC) ParseConfigs() (Configs, error) { + r, err := os.Open(svc.Path) + if err != nil { + return make(Configs), nil + } + return ParseConfigs(r) +} + +// WriteConfigs to the path. +func (svc LocalConfigsSVC) WriteConfigs(pp Configs) error { + if err := os.MkdirAll(svc.Dir, os.ModePerm); err != nil { + return err + } + var b1, b2 bytes.Buffer + err := toml.NewEncoder(&b1).Encode(pp) + if err != nil { + return err + } + // a list cloud 2 clusters, commented out + b1.WriteString("# \n") + pp = map[string]Config{ + "us-central": {Host: "https://us-central1-1.gcp.cloud2.influxdata.com", Token: "XXX"}, + "us-west": {Host: "https://us-west-2-1.aws.cloud2.influxdata.com", Token: "XXX"}, + "eu-central": {Host: "https://eu-central-1-1.aws.cloud2.influxdata.com", Token: "XXX"}, + } + + if err := toml.NewEncoder(&b2).Encode(pp); err != nil { + return err + } + reader := bufio.NewReader(&b2) + for { + line, _, err := reader.ReadLine() + + if err == io.EOF { + break + } + b1.WriteString("# " + string(line) + "\n") + } + return ioutil.WriteFile(svc.Path, b1.Bytes(), 0600) +} + +// ParseConfigs decodes configs from io readers +func ParseConfigs(r io.Reader) (Configs, error) { + p := make(Configs) + _, err := toml.DecodeReader(r, &p) + return p, err +} + +// ParseActiveConfig returns the active config from the reader. +func ParseActiveConfig(r io.Reader) (Config, error) { + pp, err := ParseConfigs(r) + if err != nil { + return DefaultConfig, err + } + var activated Config + var hasActive bool + for _, p := range pp { + if p.Active && !hasActive { + activated = p + hasActive = true + } else if p.Active { + return DefaultConfig, &influxdb.Error{ + Code: influxdb.EConflict, + Msg: "more than one activated configs found", + } + } + } + if hasActive { + return activated, nil + } + return DefaultConfig, &influxdb.Error{ + Code: influxdb.ENotFound, + Msg: "activated config is not found", + } +} diff --git a/cmd/influx/config/config_test.go b/cmd/influx/config/config_test.go new file mode 100644 index 00000000000..f25c08c1e70 --- /dev/null +++ b/cmd/influx/config/config_test.go @@ -0,0 +1,122 @@ +package config + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/influxdata/influxdb" + influxtesting "github.com/influxdata/influxdb/testing" +) + +func TestParseActiveConfig(t *testing.T) { + cases := []struct { + name string + hasErr bool + src string + p Config + }{ + { + name: "bad src", + src: "bad [toml", + hasErr: true, + }, + { + name: "nothing", + hasErr: true, + }, + { + name: "conflicted", + hasErr: true, + src: ` + [a1] + url = "host1" + active =true + [a2] + url = "host2" + active = true + `, + }, + { + name: "one active", + hasErr: false, + src: ` + [a1] + url = "host1" + [a2] + url = "host2" + active = true + [a3] + url = "host3" + [a4] + url = "host4" + `, + p: Config{ + Host: "host2", + Active: true, + }, + }, + } + for _, c := range cases { + r := bytes.NewBufferString(c.src) + p, err := ParseActiveConfig(r) + if c.hasErr { + if err == nil { + t.Fatalf("parse active config %q failed, should have error, got nil", c.name) + } + continue + } + if diff := cmp.Diff(p, c.p); diff != "" { + t.Fatalf("parse active config %s failed, diff %s", c.name, diff) + } + } +} + +func TestConfigsSwith(t *testing.T) { + cases := []struct { + name string + old Configs + new Configs + target string + err error + }{ + { + name: "not found", + target: "p1", + old: Configs{ + "a1": {Host: "host1"}, + "a2": {Host: "host2"}, + }, + new: Configs{ + "a1": {Host: "host1"}, + "a2": {Host: "host2"}, + }, + err: &influxdb.Error{ + Code: influxdb.ENotFound, + Msg: `config "p1" is not found`, + }, + }, + { + name: "regular switch", + target: "a3", + old: Configs{ + "a1": {Host: "host1", Active: true}, + "a2": {Host: "host2"}, + "a3": {Host: "host3"}, + }, + new: Configs{ + "a1": {Host: "host1"}, + "a2": {Host: "host2"}, + "a3": {Host: "host3", Active: true}, + }, + err: nil, + }, + } + for _, c := range cases { + err := c.old.Switch(c.target) + influxtesting.ErrorsEqual(t, err, c.err) + if diff := cmp.Diff(c.old, c.new); diff != "" { + t.Fatalf("switch config %s failed, diff %s", c.name, diff) + } + } +} diff --git a/cmd/influx/config/mock.go b/cmd/influx/config/mock.go new file mode 100644 index 00000000000..e8727a27acf --- /dev/null +++ b/cmd/influx/config/mock.go @@ -0,0 +1,17 @@ +package config + +// MockConfigService mocks the ConfigService. +type MockConfigService struct { + WriteConfigsFn func(pp Configs) error + ParseConfigsFn func() (Configs, error) +} + +// WriteConfigs returns the write fn. +func (s *MockConfigService) WriteConfigs(pp Configs) error { + return s.WriteConfigsFn(pp) +} + +// ParseConfigs returns the parse fn. +func (s *MockConfigService) ParseConfigs() (Configs, error) { + return s.ParseConfigsFn() +} diff --git a/cmd/influx/config_test.go b/cmd/influx/config_test.go new file mode 100644 index 00000000000..65734d9edc3 --- /dev/null +++ b/cmd/influx/config_test.go @@ -0,0 +1,328 @@ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb/cmd/influx/config" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestCmdConfig(t *testing.T) { + + t.Run("create", func(t *testing.T) { + tests := []struct { + name string + original config.Configs + expected config.Configs + flags []string + }{ + { + name: "basic", + flags: []string{ + "--name", "default", + "--org", "org1", + "--url", "http://localhost:9999", + "--token", "tok1", + "--active", + }, + original: make(config.Configs), + expected: config.Configs{ + "default": { + Org: "org1", + Active: true, + Token: "tok1", + Host: "http://localhost:9999", + }, + }, + }, + { + name: "short", + flags: []string{ + "-n", "default", + "-o", "org1", + "-u", "http://localhost:9999", + "-t", "tok1", + "-a", + }, + original: make(config.Configs), + expected: config.Configs{ + "default": { + Org: "org1", + Active: true, + Token: "tok1", + Host: "http://localhost:9999", + }, + }, + }, + } + cmdFn := func(orginal, expected config.Configs) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &config.MockConfigService{ + ParseConfigsFn: func() (config.Configs, error) { + return orginal, nil + }, + WriteConfigsFn: func(pp config.Configs) error { + if diff := cmp.Diff(expected, pp); diff != "" { + return &influxdb.Error{ + Msg: fmt.Sprintf("write configs failed, diff %s", diff), + } + } + return nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdConfigBuilder{ + genericCLIOpts: opt, + globalFlags: g, + svc: svc, + } + return builder.cmd() + } + } + for _, tt := range tests { + fn := func(t *testing.T) { + builder := newInfluxCmdBuilder( + in(new(bytes.Buffer)), + out(ioutil.Discard), + ) + cmd := builder.cmd(cmdFn(tt.original, tt.expected)) + cmd.SetArgs(append([]string{"config", "create"}, tt.flags...)) + require.NoError(t, cmd.Execute()) + } + t.Run(tt.name, fn) + } + }) + + t.Run("set", func(t *testing.T) { + tests := []struct { + name string + original config.Configs + expected config.Configs + flags []string + }{ + { + name: "basic", + flags: []string{ + "--name", "default", + "--org", "org1", + "--url", "http://localhost:9999", + "--token", "tok1", + "--active", + }, + original: config.Configs{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: config.Configs{ + "default": { + Org: "org1", + Active: true, + Token: "tok1", + Host: "http://localhost:9999", + }, + }, + }, + { + name: "short", + flags: []string{ + "-n", "default", + "-o", "org1", + "-u", "http://localhost:9999", + "-t", "tok1", + "-a", + }, + original: config.Configs{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: config.Configs{ + "default": { + Org: "org1", + Active: true, + Token: "tok1", + Host: "http://localhost:9999", + }, + }, + }, + } + cmdFn := func(orginal, expected config.Configs) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &config.MockConfigService{ + ParseConfigsFn: func() (config.Configs, error) { + return orginal, nil + }, + WriteConfigsFn: func(pp config.Configs) error { + if diff := cmp.Diff(expected, pp); diff != "" { + return &influxdb.Error{ + Msg: fmt.Sprintf("write configs failed, diff %s", diff), + } + } + return nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdConfigBuilder{ + genericCLIOpts: opt, + globalFlags: g, + svc: svc, + } + return builder.cmd() + } + } + for _, tt := range tests { + fn := func(t *testing.T) { + builder := newInfluxCmdBuilder( + in(new(bytes.Buffer)), + out(ioutil.Discard), + ) + cmd := builder.cmd(cmdFn(tt.original, tt.expected)) + cmd.SetArgs(append([]string{"config", "set"}, tt.flags...)) + require.NoError(t, cmd.Execute()) + } + t.Run(tt.name, fn) + } + }) + + t.Run("delete", func(t *testing.T) { + tests := []struct { + name string + original config.Configs + expected config.Configs + flags []string + }{ + { + name: "basic", + flags: []string{ + "--name", "default", + }, + original: config.Configs{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: make(config.Configs), + }, + { + name: "short", + flags: []string{ + "-n", "default", + }, + original: config.Configs{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: make(config.Configs), + }, + } + cmdFn := func(orginal, expected config.Configs) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &config.MockConfigService{ + ParseConfigsFn: func() (config.Configs, error) { + return orginal, nil + }, + WriteConfigsFn: func(pp config.Configs) error { + if diff := cmp.Diff(expected, pp); diff != "" { + return &influxdb.Error{ + Msg: fmt.Sprintf("write configs failed, diff %s", diff), + } + } + return nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdConfigBuilder{ + genericCLIOpts: opt, + globalFlags: g, + svc: svc, + } + return builder.cmd() + } + } + for _, tt := range tests { + fn := func(t *testing.T) { + builder := newInfluxCmdBuilder( + in(new(bytes.Buffer)), + out(ioutil.Discard), + ) + cmd := builder.cmd(cmdFn(tt.original, tt.expected)) + cmd.SetArgs(append([]string{"config", "delete"}, tt.flags...)) + require.NoError(t, cmd.Execute()) + } + t.Run(tt.name, fn) + } + }) + + t.Run("list", func(t *testing.T) { + tests := []struct { + name string + expected config.Configs + }{ + { + name: "basic", + expected: config.Configs{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + "kubone": { + Org: "org1", + Active: false, + Token: "tok1", + Host: "http://localhost:9999", + }, + }, + }, + } + cmdFn := func(expected config.Configs) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &config.MockConfigService{ + ParseConfigsFn: func() (config.Configs, error) { + return expected, nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdConfigBuilder{ + genericCLIOpts: opt, + globalFlags: g, + svc: svc, + } + return builder.cmd() + } + } + for _, tt := range tests { + fn := func(t *testing.T) { + builder := newInfluxCmdBuilder( + in(new(bytes.Buffer)), + out(ioutil.Discard), + ) + cmd := builder.cmd(cmdFn(tt.expected)) + cmd.SetArgs([]string{"config", "list"}) + require.NoError(t, cmd.Execute()) + } + t.Run(tt.name, fn) + } + }) +} diff --git a/cmd/influx/debug.go b/cmd/influx/debug.go index 7bb843c62e5..0d566aa9f1c 100644 --- a/cmd/influx/debug.go +++ b/cmd/influx/debug.go @@ -84,7 +84,7 @@ in the following ways: // inspectReportTSMF runs the report-tsm tool. func inspectReportTSMF(cmd *cobra.Command, args []string) error { - if err := inspectReportTSMFlags.organization.validOrgFlags(); err != nil { + if err := inspectReportTSMFlags.organization.validOrgFlags(&flags); err != nil { return err } report := &tsm1.Report{ diff --git a/cmd/influx/delete.go b/cmd/influx/delete.go index d01bf97acbf..1690a8873ae 100644 --- a/cmd/influx/delete.go +++ b/cmd/influx/delete.go @@ -68,8 +68,8 @@ func fluxDeleteF(cmd *cobra.Command, args []string) error { } s := &http.DeleteService{ - Addr: flags.host, - Token: flags.token, + Addr: flags.Host, + Token: flags.Token, InsecureSkipVerify: flags.skipVerify, } diff --git a/cmd/influx/main.go b/cmd/influx/main.go index 883e92240c0..9b614d0c592 100644 --- a/cmd/influx/main.go +++ b/cmd/influx/main.go @@ -12,6 +12,7 @@ import ( "github.com/influxdata/influxdb" "github.com/influxdata/influxdb/bolt" + "github.com/influxdata/influxdb/cmd/influx/config" "github.com/influxdata/influxdb/cmd/influx/internal" "github.com/influxdata/influxdb/http" "github.com/influxdata/influxdb/internal/fs" @@ -42,7 +43,7 @@ func newHTTPClient() (*httpc.Client, error) { return httpClient, nil } - c, err := http.NewHTTPClient(flags.host, flags.token, flags.skipVerify) + c, err := http.NewHTTPClient(flags.Host, flags.Token, flags.skipVerify) if err != nil { return nil, err } @@ -95,8 +96,7 @@ func out(w io.Writer) genericCLIOptFn { } type globalFlags struct { - token string - host string + config.Config local bool skipVerify bool } @@ -141,14 +141,14 @@ func (b *cmdInfluxBuilder) cmd(childCmdFns ...func(f *globalFlags, opt genericCL fOpts := flagOpts{ { - DestP: &flags.token, + DestP: &flags.Token, Flag: "token", Short: 't', Desc: "API token to be used throughout client calls", Persistent: true, }, { - DestP: &flags.host, + DestP: &flags.Host, Flag: "host", Default: "http://localhost:9999", Desc: "HTTP address of Influx", @@ -157,11 +157,14 @@ func (b *cmdInfluxBuilder) cmd(childCmdFns ...func(f *globalFlags, opt genericCL } fOpts.mustRegister(cmd) - if flags.token == "" { + if flags.Token == "" { + // migration credential token + migrateOldCredential() + // this is after the flagOpts register b/c we don't want to show the default value - // in the usage display. This will add it as the token value, then if a token flag + // in the usage display. This will add it as the config, then if a token flag // is provided too, the flag will take precedence. - flags.token = getTokenFromDefaultPath() + flags.Config = getConfigFromDefaultPath() } cmd.PersistentFlags().BoolVar(&flags.local, "local", false, "Run commands locally against the filesystem") @@ -185,6 +188,7 @@ func influxCmd(opts ...genericCLIOptFn) *cobra.Command { cmdOrganization, cmdPing, cmdPkg, + cmdConfig, cmdQuery, cmdTranspile, cmdREPL, @@ -224,31 +228,56 @@ func seeHelp(c *cobra.Command, args []string) { c.Printf("See '%s -h' for help\n", c.CommandPath()) } -func defaultTokenPath() (string, string, error) { +func defaultConfigPath() (string, string, error) { dir, err := fs.InfluxDir() if err != nil { return "", "", err } - return filepath.Join(dir, http.DefaultTokenFile), dir, nil + return filepath.Join(dir, http.DefaultConfigsFile), dir, nil } -func getTokenFromDefaultPath() string { - path, _, err := defaultTokenPath() +func getConfigFromDefaultPath() config.Config { + path, _, err := defaultConfigPath() if err != nil { - return "" + return config.DefaultConfig } - b, err := ioutil.ReadFile(path) + r, err := os.Open(path) if err != nil { - return "" + return config.DefaultConfig } - return strings.TrimSpace(string(b)) + activated, _ := config.ParseActiveConfig(r) + return activated } -func writeTokenToPath(tok, path, dir string) error { - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return err +func migrateOldCredential() { + dir, err := fs.InfluxDir() + if err != nil { + return // no need for migration + } + tokB, err := ioutil.ReadFile(filepath.Join(dir, http.DefaultTokenFile)) + if err != nil { + return // no need for migration + } + err = writeConfigToPath(strings.TrimSpace(string(tokB)), "", filepath.Join(dir, http.DefaultConfigsFile), dir) + if err != nil { + return } - return ioutil.WriteFile(path, []byte(tok), 0600) + // ignore the remove err + _ = os.Remove(filepath.Join(dir, http.DefaultTokenFile)) +} + +func writeConfigToPath(tok, org, path, dir string) error { + p := &config.DefaultConfig + p.Token = tok + p.Org = org + pp := map[string]config.Config{ + "default": *p, + } + + return config.LocalConfigsSVC{ + Path: path, + Dir: dir, + }.WriteConfigs(pp) } func checkSetup(host string, skipVerify bool) error { @@ -277,7 +306,7 @@ func checkSetupRunEMiddleware(f *globalFlags) cobraRuneEMiddleware { return nil } - if setupErr := checkSetup(f.host, f.skipVerify); setupErr != nil && influxdb.EUnauthorized != influxdb.ErrorCode(setupErr) { + if setupErr := checkSetup(f.Host, f.skipVerify); setupErr != nil && influxdb.EUnauthorized != influxdb.ErrorCode(setupErr) { return internal.ErrorFmt(setupErr) } @@ -350,7 +379,11 @@ func (o *organization) getID(orgSVC influxdb.OrganizationService) (influxdb.ID, return 0, fmt.Errorf("failed to locate an organization id") } -func (o *organization) validOrgFlags() error { +func (o *organization) validOrgFlags(f *globalFlags) error { + if o.id == "" && o.name == "" && f != nil { + o.name = f.Org + } + if o.id == "" && o.name == "" { return fmt.Errorf("must specify org-id, or org name") } else if o.id != "" && o.name != "" { diff --git a/cmd/influx/ping.go b/cmd/influx/ping.go index e784c97191f..ff99a40e52a 100644 --- a/cmd/influx/ping.go +++ b/cmd/influx/ping.go @@ -19,7 +19,7 @@ func cmdPing(f *globalFlags, opts genericCLIOpts) *cobra.Command { c := http.Client{ Timeout: 5 * time.Second, } - url := flags.host + "/health" + url := flags.Host + "/health" resp, err := c.Get(url) if err != nil { return err diff --git a/cmd/influx/pkg.go b/cmd/influx/pkg.go index 2cf9a7d11f9..9be63e317c1 100644 --- a/cmd/influx/pkg.go +++ b/cmd/influx/pkg.go @@ -105,7 +105,7 @@ func (b *cmdPkgBuilder) cmdPkgApply() *cobra.Command { } func (b *cmdPkgBuilder) pkgApplyRunEFn(cmd *cobra.Command, args []string) error { - if err := b.org.validOrgFlags(); err != nil { + if err := b.org.validOrgFlags(&flags); err != nil { return err } color.NoColor = b.disableColor @@ -115,7 +115,7 @@ func (b *cmdPkgBuilder) pkgApplyRunEFn(cmd *cobra.Command, args []string) error return err } - if err := b.org.validOrgFlags(); err != nil { + if err := b.org.validOrgFlags(&flags); err != nil { return err } diff --git a/cmd/influx/query.go b/cmd/influx/query.go index 1484f6733b9..ecf3064b040 100644 --- a/cmd/influx/query.go +++ b/cmd/influx/query.go @@ -31,7 +31,7 @@ func fluxQueryF(cmd *cobra.Command, args []string) error { return fmt.Errorf("local flag not supported for query command") } - if err := queryFlags.org.validOrgFlags(); err != nil { + if err := queryFlags.org.validOrgFlags(&flags); err != nil { return err } @@ -52,7 +52,7 @@ func fluxQueryF(cmd *cobra.Command, args []string) error { flux.FinalizeBuiltIns() - r, err := getFluxREPL(flags.host, flags.token, flags.skipVerify, orgID) + r, err := getFluxREPL(flags.Host, flags.Token, flags.skipVerify, orgID) if err != nil { return fmt.Errorf("failed to get the flux REPL: %v", err) } diff --git a/cmd/influx/repl.go b/cmd/influx/repl.go index b350e304750..b55cf22944a 100644 --- a/cmd/influx/repl.go +++ b/cmd/influx/repl.go @@ -33,7 +33,7 @@ func replF(cmd *cobra.Command, args []string) error { return fmt.Errorf("local flag not supported for repl command") } - if err := replFlags.org.validOrgFlags(); err != nil { + if err := replFlags.org.validOrgFlags(&flags); err != nil { return err } @@ -49,7 +49,7 @@ func replF(cmd *cobra.Command, args []string) error { flux.FinalizeBuiltIns() - r, err := getFluxREPL(flags.host, flags.token, flags.skipVerify, orgID) + r, err := getFluxREPL(flags.Host, flags.Token, flags.skipVerify, orgID) if err != nil { return err } diff --git a/cmd/influx/setup.go b/cmd/influx/setup.go index 6cef7254818..525bd337e67 100644 --- a/cmd/influx/setup.go +++ b/cmd/influx/setup.go @@ -2,13 +2,15 @@ package main import ( "context" + "errors" "fmt" "os" "strconv" "strings" "time" - platform "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb/cmd/influx/config" "github.com/influxdata/influxdb/cmd/influx/internal" "github.com/influxdata/influxdb/http" "github.com/spf13/cobra" @@ -22,11 +24,13 @@ var setupFlags struct { org string bucket string retention time.Duration + name string force bool } func cmdSetup(f *globalFlags, opt genericCLIOpts) *cobra.Command { - cmd := opt.newCmd("setup", setupF) + cmd := opt.newCmd("setup", nil) + cmd.RunE = setupF cmd.Short = "Setup instance with initial user, org, bucket" cmd.Flags().StringVarP(&setupFlags.username, "username", "u", "", "primary username") @@ -34,6 +38,7 @@ func cmdSetup(f *globalFlags, opt genericCLIOpts) *cobra.Command { cmd.Flags().StringVarP(&setupFlags.token, "token", "t", "", "token for username, else auto-generated") cmd.Flags().StringVarP(&setupFlags.org, "org", "o", "", "primary organization name") cmd.Flags().StringVarP(&setupFlags.bucket, "bucket", "b", "", "primary bucket name") + cmd.Flags().StringVarP(&setupFlags.name, "name", "n", "", "config name, only required if you already have existing configs") cmd.Flags().DurationVarP(&setupFlags.retention, "retention", "r", -1, "Duration bucket will retain data. 0 is infinite. Default is 0.") cmd.Flags().BoolVarP(&setupFlags.force, "force", "f", false, "skip confirmation prompt") @@ -47,27 +52,37 @@ func setupF(cmd *cobra.Command, args []string) error { // check if setup is allowed s := &http.SetupService{ - Addr: flags.host, + Addr: flags.Host, InsecureSkipVerify: flags.skipVerify, } - allowed, err := s.IsOnboarding(context.Background()) if err != nil { return fmt.Errorf("failed to determine if instance has been configured: %v", err) } if !allowed { - return fmt.Errorf("instance at %q has already been setup", flags.host) + return fmt.Errorf("instance at %q has already been setup", flags.Host) } - dPath, dir, err := defaultTokenPath() + dPath, dir, err := defaultConfigPath() if err != nil { return err } + existingConfigs := make(config.Configs) if _, err := os.Stat(dPath); err == nil { - return &platform.Error{ - Code: platform.EConflict, - Msg: fmt.Sprintf("token already exists at %s", dPath), + existingConfigs, _ = config.LocalConfigsSVC{ + Path: dPath, + Dir: dir, + }.ParseConfigs() + // ignore the error if found nothing + if setupFlags.name == "" { + return errors.New("flag name is required if you already have existing configs") + } + if _, ok := existingConfigs[setupFlags.name]; ok { + return &influxdb.Error{ + Code: influxdb.EConflict, + Msg: fmt.Sprintf("config name %q already existed", setupFlags.name), + } } } @@ -81,12 +96,29 @@ func setupF(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to setup instance: %v", err) } - err = writeTokenToPath(result.Auth.Token, dPath, dir) - if err != nil { - return fmt.Errorf("failed to write token to path %q: %v", dPath, err) + var configName string + var p *config.Config + if len(existingConfigs) > 0 { + configName = setupFlags.name + p = &config.Config{ + Host: flags.Host, + } + } else { + configName = "default" + p = &config.DefaultConfig + } + p.Token = result.Auth.Token + p.Org = result.Org.Name + existingConfigs[configName] = *p + localSVC := config.LocalConfigsSVC{ + Path: dPath, + Dir: dir, + } + if err = localSVC.WriteConfigs(existingConfigs); err != nil { + return fmt.Errorf("failed to write config to path %q: %v", dPath, err) } - fmt.Println(string(promptWithColor("Your token has been stored in "+dPath+".", colorCyan))) + fmt.Println(string(promptWithColor(fmt.Sprintf("Config %s has been stored in %s.", configName, dPath), colorCyan))) w := internal.NewTabWriter(os.Stdout) w.WriteHeaders( @@ -113,15 +145,15 @@ func isInteractive() bool { setupFlags.bucket == "" } -func onboardingRequest() (*platform.OnboardingRequest, error) { +func onboardingRequest() (*influxdb.OnboardingRequest, error) { if isInteractive() { return interactive() } return nonInteractive() } -func nonInteractive() (*platform.OnboardingRequest, error) { - req := &platform.OnboardingRequest{ +func nonInteractive() (*influxdb.OnboardingRequest, error) { + req := &influxdb.OnboardingRequest{ User: setupFlags.username, Password: setupFlags.password, Token: setupFlags.token, @@ -133,17 +165,17 @@ func nonInteractive() (*platform.OnboardingRequest, error) { } if setupFlags.retention < 0 { - req.RetentionPeriod = platform.InfiniteRetention + req.RetentionPeriod = influxdb.InfiniteRetention } return req, nil } -func interactive() (req *platform.OnboardingRequest, err error) { +func interactive() (req *influxdb.OnboardingRequest, err error) { ui := &input.UI{ Writer: os.Stdout, Reader: os.Stdin, } - req = new(platform.OnboardingRequest) + req = new(influxdb.OnboardingRequest) fmt.Println(string(promptWithColor(`Welcome to InfluxDB 2.0!`, colorYellow))) if setupFlags.username != "" { req.User = setupFlags.username @@ -173,7 +205,7 @@ func interactive() (req *platform.OnboardingRequest, err error) { req.RetentionPeriod = uint(setupFlags.retention) } else { for { - rpStr := getInput(ui, "Please type your retention period in hours.\r\nOr press ENTER for infinite.", strconv.Itoa(platform.InfiniteRetention)) + rpStr := getInput(ui, "Please type your retention period in hours.\r\nOr press ENTER for infinite.", strconv.Itoa(influxdb.InfiniteRetention)) rp, err := strconv.Atoi(rpStr) if rp >= 0 && err == nil { req.RetentionPeriod = uint(rp) @@ -205,7 +237,7 @@ func promptWithColor(s string, color []byte) []byte { return append(bb, keyReset...) } -func getConfirm(ui *input.UI, or *platform.OnboardingRequest) bool { +func getConfirm(ui *input.UI, or *influxdb.OnboardingRequest) bool { prompt := promptWithColor("Confirm? (y/n)", colorRed) for { rp := "infinite" diff --git a/cmd/influx/task.go b/cmd/influx/task.go index c793d2598dd..1080ade3ecb 100644 --- a/cmd/influx/task.go +++ b/cmd/influx/task.go @@ -53,7 +53,7 @@ func taskCreateCmd(opt genericCLIOpts) *cobra.Command { } func taskCreateF(cmd *cobra.Command, args []string) error { - if err := taskCreateFlags.org.validOrgFlags(); err != nil { + if err := taskCreateFlags.org.validOrgFlags(&flags); err != nil { return err } @@ -141,7 +141,7 @@ func taskFindCmd(opt genericCLIOpts) *cobra.Command { } func taskFindF(cmd *cobra.Command, args []string) error { - if err := taskFindFlags.org.validOrgFlags(); err != nil { + if err := taskFindFlags.org.validOrgFlags(&flags); err != nil { return err } diff --git a/cmd/influx/user.go b/cmd/influx/user.go index 8cca592d71d..0b524fedb4d 100644 --- a/cmd/influx/user.go +++ b/cmd/influx/user.go @@ -214,7 +214,7 @@ func (b *cmdUserBuilder) cmdCreate() *cobra.Command { func (b *cmdUserBuilder) cmdCreateRunEFn(*cobra.Command, []string) error { ctx := context.Background() - if err := b.org.validOrgFlags(); err != nil { + if err := b.org.validOrgFlags(b.globalFlags); err != nil { return err } diff --git a/cmd/influx/write.go b/cmd/influx/write.go index 44bf758a35f..35fb407c417 100644 --- a/cmd/influx/write.go +++ b/cmd/influx/write.go @@ -146,8 +146,8 @@ func fluxWriteF(cmd *cobra.Command, args []string) error { s := write.Batcher{ Service: &http.WriteService{ - Addr: flags.host, - Token: flags.token, + Addr: flags.Host, + Token: flags.Token, Precision: writeFlags.Precision, InsecureSkipVerify: flags.skipVerify, }, diff --git a/http/backup_service.go b/http/backup_service.go index 22b4184366b..349027aaade 100644 --- a/http/backup_service.go +++ b/http/backup_service.go @@ -22,8 +22,12 @@ import ( "go.uber.org/zap" ) +// DefaultTokenFile is deprecated, and will be only used for migration. const DefaultTokenFile = "credentials" +// DefaultConfigsFile stores cli credentials and hosts. +const DefaultConfigsFile = "configs" + // BackupBackend is all services and associated parameters required to construct the BackupHandler. type BackupBackend struct { Logger *zap.Logger @@ -44,6 +48,7 @@ func NewBackupBackend(b *APIBackend) *BackupBackend { } } +// BackupHandler is http handler for backup service. type BackupHandler struct { *httprouter.Router influxdb.HTTPErrorHandler @@ -125,7 +130,7 @@ func (h *BackupHandler) handleCreate(w http.ResponseWriter, r *http.Request) { } if credsExist { - files = append(files, DefaultTokenFile) + files = append(files, DefaultConfigsFile) } b := backup{ @@ -140,9 +145,9 @@ func (h *BackupHandler) handleCreate(w http.ResponseWriter, r *http.Request) { } func (h *BackupHandler) backupCredentials(internalBackupPath string) (bool, error) { - credBackupPath := filepath.Join(internalBackupPath, DefaultTokenFile) + credBackupPath := filepath.Join(internalBackupPath, DefaultConfigsFile) - credPath, err := defaultTokenPath() + credPath, err := defaultConfigsPath() if err != nil { return false, err } @@ -258,12 +263,12 @@ func (s *BackupService) FetchBackupFile(ctx context.Context, backupID int, backu return nil } -func defaultTokenPath() (string, error) { +func defaultConfigsPath() (string, error) { dir, err := fs.InfluxDir() if err != nil { return "", err } - return filepath.Join(dir, DefaultTokenFile), nil + return filepath.Join(dir, DefaultConfigsFile), nil } func (s *BackupService) InternalBackupPath(backupID int) string {