From 1337bd9ae1025840d15dde2599d7dabd0d926426 Mon Sep 17 00:00:00 2001 From: Kelvin Wang Date: Tue, 10 Mar 2020 11:53:41 -0400 Subject: [PATCH] feat(cmd/influx): add profile management --- CHANGELOG.md | 1 + cmd/influx/authorization.go | 2 +- cmd/influx/backup.go | 4 +- cmd/influx/bucket.go | 4 +- cmd/influx/debug.go | 2 +- cmd/influx/delete.go | 4 +- cmd/influx/main.go | 76 ++++++--- cmd/influx/ping.go | 2 +- cmd/influx/pkg.go | 4 +- cmd/influx/profile.go | 195 +++++++++++++++++++++ cmd/influx/profile_test.go | 328 ++++++++++++++++++++++++++++++++++++ cmd/influx/query.go | 4 +- cmd/influx/repl.go | 4 +- cmd/influx/setup.go | 8 +- cmd/influx/task.go | 4 +- cmd/influx/user.go | 2 +- cmd/influx/write.go | 4 +- http/backup_service.go | 5 + mock/profile.go | 21 +++ profile.go | 135 +++++++++++++++ profile_test.go | 122 ++++++++++++++ 21 files changed, 885 insertions(+), 46 deletions(-) create mode 100644 cmd/influx/profile.go create mode 100644 cmd/influx/profile_test.go create mode 100644 mock/profile.go create mode 100644 profile.go create mode 100644 profile_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 459347f9534..dd732723019 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/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..2434d0eb319 100644 --- a/cmd/influx/main.go +++ b/cmd/influx/main.go @@ -42,7 +42,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 +95,7 @@ func out(w io.Writer) genericCLIOptFn { } type globalFlags struct { - token string - host string + influxdb.Profile local bool skipVerify bool } @@ -141,14 +140,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 +156,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 profile, then if a token flag // is provided too, the flag will take precedence. - flags.token = getTokenFromDefaultPath() + flags.Profile = getProfileFromDefaultPath() } cmd.PersistentFlags().BoolVar(&flags.local, "local", false, "Run commands locally against the filesystem") @@ -185,6 +187,7 @@ func influxCmd(opts ...genericCLIOptFn) *cobra.Command { cmdOrganization, cmdPing, cmdPkg, + cmdProfile, cmdQuery, cmdTranspile, cmdREPL, @@ -224,31 +227,56 @@ func seeHelp(c *cobra.Command, args []string) { c.Printf("See '%s -h' for help\n", c.CommandPath()) } -func defaultTokenPath() (string, string, error) { +func defaultProfilePath() (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.DefaultProfilesFile), dir, nil } -func getTokenFromDefaultPath() string { - path, _, err := defaultTokenPath() +func getProfileFromDefaultPath() influxdb.Profile { + path, _, err := defaultProfilePath() if err != nil { - return "" + return influxdb.DefaultProfile } - b, err := ioutil.ReadFile(path) + r, err := os.Open(path) if err != nil { - return "" + return influxdb.DefaultProfile } - return strings.TrimSpace(string(b)) + activated, _ := influxdb.ParseActiveProfile(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 = writeProfileToPath(strings.TrimSpace(string(tokB)), "", filepath.Join(dir, http.DefaultProfilesFile), dir) + if err != nil { + return } - return ioutil.WriteFile(path, []byte(tok), 0600) + // ignore the remove err + _ = os.Remove(filepath.Join(dir, http.DefaultTokenFile)) +} + +func writeProfileToPath(tok, org, path, dir string) error { + p := &influxdb.DefaultProfile + p.Token = tok + p.Org = org + pp := map[string]influxdb.Profile{ + "default": *p, + } + + return influxdb.LocalProfilesSVC{ + Path: path, + Dir: dir, + }.WriteProfiles(pp) } func checkSetup(host string, skipVerify bool) error { @@ -277,7 +305,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 +378,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/profile.go b/cmd/influx/profile.go new file mode 100644 index 00000000000..933da33725b --- /dev/null +++ b/cmd/influx/profile.go @@ -0,0 +1,195 @@ +package main + +import ( + "fmt" + + "github.com/influxdata/influxdb" + "github.com/spf13/cobra" +) + +func cmdProfile(f *globalFlags, opt genericCLIOpts) *cobra.Command { + path, dir, err := defaultProfilePath() + if err != nil { + panic(err) + } + builder := cmdProfileBuilder{ + genericCLIOpts: opt, + globalFlags: f, + ProfilesService: influxdb.LocalProfilesSVC{ + Path: path, + Dir: dir, + }, + } + builder.globalFlags = f + return builder.cmd() +} + +type cmdProfileBuilder struct { + genericCLIOpts + *globalFlags + + name string + url string + token string + active bool + org string + + influxdb.ProfilesService +} + +func (b *cmdProfileBuilder) cmd() *cobra.Command { + cmd := b.newCmd("profile", nil) + cmd.Short = "Profile management commands" + cmd.Run = seeHelp + cmd.AddCommand( + b.cmdCreate(), + b.cmdDelete(), + b.cmdUpdate(), + b.cmdList(), + ) + return cmd +} + +func (b *cmdProfileBuilder) cmdCreate() *cobra.Command { + cmd := b.newCmd("create", b.cmdCreateRunEFn) + cmd.Short = "Create profile" + cmd.Flags().StringVarP(&b.name, "name", "n", "", "The profile name (required)") + cmd.MarkFlagRequired("name") + cmd.Flags().StringVarP(&b.token, "token", "t", "", "The profile token (required)") + cmd.MarkFlagRequired("token") + cmd.Flags().StringVarP(&b.url, "url", "u", "", "The profile url (required)") + cmd.MarkFlagRequired("url") + + cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active profile") + cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name") + return cmd +} + +func (b *cmdProfileBuilder) cmdCreateRunEFn(*cobra.Command, []string) error { + pp, err := b.ParseProfiles() + if err != nil { + return err + } + p := influxdb.Profile{ + 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 + if p.Active { + if err := pp.Switch(b.name); err != nil { + return err + } + } + return b.WriteProfiles(pp) +} + +func (b *cmdProfileBuilder) cmdDelete() *cobra.Command { + cmd := b.newCmd("delete", b.cmdDeleteRunEFn) + cmd.Short = "Delete profile" + + cmd.Flags().StringVarP(&b.name, "name", "n", "", "The profile name (required)") + cmd.MarkFlagRequired("name") + + return cmd +} + +func (b *cmdProfileBuilder) cmdDeleteRunEFn(cmd *cobra.Command, args []string) error { + pp, err := b.ParseProfiles() + if err != nil { + return err + } + if _, ok := pp[b.name]; !ok { + return &influxdb.Error{ + Code: influxdb.ENotFound, + Msg: fmt.Sprintf("name %q is not found", b.name), + } + } + delete(pp, b.name) + return b.WriteProfiles(pp) +} + +func (b *cmdProfileBuilder) cmdUpdate() *cobra.Command { + cmd := b.newCmd("update", b.cmdUpdateRunEFn) + cmd.Short = "update profile" + cmd.Flags().StringVarP(&b.name, "name", "n", "", "The profile name (required)") + cmd.MarkFlagRequired("name") + + cmd.Flags().StringVarP(&b.token, "token", "t", "", "The profile token (required)") + cmd.Flags().StringVarP(&b.url, "url", "u", "", "The profile url (required)") + cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active profile") + cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name") + return cmd +} + +func (b *cmdProfileBuilder) cmdUpdateRunEFn(*cobra.Command, []string) error { + pp, err := b.ParseProfiles() + 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 + if b.active { + if err := pp.Switch(b.name); err != nil { + return err + } + } + return b.WriteProfiles(pp) +} + +func (b *cmdProfileBuilder) cmdList() *cobra.Command { + cmd := b.newCmd("list", b.cmdListRunEFn) + cmd.Short = "list profiles" + return cmd +} + +func (b *cmdProfileBuilder) cmdListRunEFn(*cobra.Command, []string) error { + pp, err := b.ParseProfiles() + 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/profile_test.go b/cmd/influx/profile_test.go new file mode 100644 index 00000000000..cb966662ba8 --- /dev/null +++ b/cmd/influx/profile_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/mock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestCmdProfile(t *testing.T) { + + t.Run("create", func(t *testing.T) { + tests := []struct { + name string + original influxdb.Profiles + expected influxdb.Profiles + flags []string + }{ + { + name: "basic", + flags: []string{ + "--name", "default", + "--org", "org1", + "--url", "http://localhost:9999", + "--token", "tok1", + "--active", + }, + original: make(influxdb.Profiles), + expected: influxdb.Profiles{ + "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(influxdb.Profiles), + expected: influxdb.Profiles{ + "default": { + Org: "org1", + Active: true, + Token: "tok1", + Host: "http://localhost:9999", + }, + }, + }, + } + cmdFn := func(orginal, expected influxdb.Profiles) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &mock.ProfileService{ + ParseProfilesFn: func() (influxdb.Profiles, error) { + return orginal, nil + }, + WriteProfilesFn: func(pp influxdb.Profiles) error { + if diff := cmp.Diff(expected, pp); diff != "" { + return &influxdb.Error{ + Msg: fmt.Sprintf("write profiles failed, diff %s", diff), + } + } + return nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdProfileBuilder{ + genericCLIOpts: opt, + globalFlags: g, + ProfilesService: 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{"profile", "create"}, tt.flags...)) + require.NoError(t, cmd.Execute()) + } + t.Run(tt.name, fn) + } + }) + + t.Run("update", func(t *testing.T) { + tests := []struct { + name string + original influxdb.Profiles + expected influxdb.Profiles + flags []string + }{ + { + name: "basic", + flags: []string{ + "--name", "default", + "--org", "org1", + "--url", "http://localhost:9999", + "--token", "tok1", + "--active", + }, + original: influxdb.Profiles{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: influxdb.Profiles{ + "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: influxdb.Profiles{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: influxdb.Profiles{ + "default": { + Org: "org1", + Active: true, + Token: "tok1", + Host: "http://localhost:9999", + }, + }, + }, + } + cmdFn := func(orginal, expected influxdb.Profiles) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &mock.ProfileService{ + ParseProfilesFn: func() (influxdb.Profiles, error) { + return orginal, nil + }, + WriteProfilesFn: func(pp influxdb.Profiles) error { + if diff := cmp.Diff(expected, pp); diff != "" { + return &influxdb.Error{ + Msg: fmt.Sprintf("write profiles failed, diff %s", diff), + } + } + return nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdProfileBuilder{ + genericCLIOpts: opt, + globalFlags: g, + ProfilesService: 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{"profile", "update"}, tt.flags...)) + require.NoError(t, cmd.Execute()) + } + t.Run(tt.name, fn) + } + }) + + t.Run("delete", func(t *testing.T) { + tests := []struct { + name string + original influxdb.Profiles + expected influxdb.Profiles + flags []string + }{ + { + name: "basic", + flags: []string{ + "--name", "default", + }, + original: influxdb.Profiles{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: make(influxdb.Profiles), + }, + { + name: "short", + flags: []string{ + "-n", "default", + }, + original: influxdb.Profiles{ + "default": { + Org: "org2", + Active: false, + Token: "tok2", + Host: "http://localhost:8888", + }, + }, + expected: make(influxdb.Profiles), + }, + } + cmdFn := func(orginal, expected influxdb.Profiles) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &mock.ProfileService{ + ParseProfilesFn: func() (influxdb.Profiles, error) { + return orginal, nil + }, + WriteProfilesFn: func(pp influxdb.Profiles) error { + if diff := cmp.Diff(expected, pp); diff != "" { + return &influxdb.Error{ + Msg: fmt.Sprintf("write profiles failed, diff %s", diff), + } + } + return nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdProfileBuilder{ + genericCLIOpts: opt, + globalFlags: g, + ProfilesService: 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{"profile", "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 influxdb.Profiles + }{ + { + name: "basic", + expected: influxdb.Profiles{ + "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 influxdb.Profiles) func(*globalFlags, genericCLIOpts) *cobra.Command { + svc := &mock.ProfileService{ + ParseProfilesFn: func() (influxdb.Profiles, error) { + return expected, nil + }, + } + + return func(g *globalFlags, opt genericCLIOpts) *cobra.Command { + builder := cmdProfileBuilder{ + genericCLIOpts: opt, + globalFlags: g, + ProfilesService: 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{"profile", "list"}) + require.NoError(t, cmd.Execute()) + } + t.Run(tt.name, fn) + } + }) +} 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..e0ec8bd5d2a 100644 --- a/cmd/influx/setup.go +++ b/cmd/influx/setup.go @@ -47,7 +47,7 @@ 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, } @@ -56,10 +56,10 @@ func setupF(cmd *cobra.Command, args []string) error { 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 := defaultProfilePath() if err != nil { return err } @@ -81,7 +81,7 @@ func setupF(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to setup instance: %v", err) } - err = writeTokenToPath(result.Auth.Token, dPath, dir) + err = writeProfileToPath(result.Auth.Token, result.Org.Name, dPath, dir) if err != nil { return fmt.Errorf("failed to write token to path %q: %v", dPath, err) } 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..f8a4ae4a0b2 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" +// DefaultProfilesFile stores cli credentials and hosts. +const DefaultProfilesFile = "profiles" + // 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 diff --git a/mock/profile.go b/mock/profile.go new file mode 100644 index 00000000000..8c50e842962 --- /dev/null +++ b/mock/profile.go @@ -0,0 +1,21 @@ +package mock + +import ( + "github.com/influxdata/influxdb" +) + +// ProfileService mocks the influxdb.ProfileService. +type ProfileService struct { + WriteProfilesFn func(pp influxdb.Profiles) error + ParseProfilesFn func() (influxdb.Profiles, error) +} + +// WriteProfiles returns the write fn. +func (s *ProfileService) WriteProfiles(pp influxdb.Profiles) error { + return s.WriteProfilesFn(pp) +} + +// ParseProfiles returns the parse fn. +func (s *ProfileService) ParseProfiles() (influxdb.Profiles, error) { + return s.ParseProfilesFn() +} diff --git a/profile.go b/profile.go new file mode 100644 index 00000000000..d1695ac6374 --- /dev/null +++ b/profile.go @@ -0,0 +1,135 @@ +package influxdb + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/BurntSushi/toml" +) + +// Profile store the crendentials of influxdb host and token. +type Profile struct { + Host string `toml:"url"` + // Token is base64 encoded sequence. + Token string `toml:"token"` + Org string `toml:"org,omitempty"` + Active bool `toml:"active,omitempty"` +} + +// DefaultProfile is default profile without token +var DefaultProfile = Profile{ + Host: "http://localhost:9999", + Active: true, +} + +// Profiles is map of profiles indexed by name. +type Profiles map[string]Profile + +// ProfilesService is the service to list and write profiles. +type ProfilesService interface { + WriteProfiles(pp Profiles) error + ParseProfiles() (Profiles, error) +} + +// Switch to another profile. +func (pp *Profiles) Switch(name string) error { + pc := *pp + if _, ok := pc[name]; !ok { + return &Error{ + Code: ENotFound, + Msg: fmt.Sprintf(`profile %q is not found`, name), + } + } + for k, v := range pc { + v.Active = k == name + pc[k] = v + } + return nil +} + +// LocalProfilesSVC has the path and dir to write and parse profiles. +type LocalProfilesSVC struct { + Path string + Dir string +} + +// ParseProfiles from the local path. +func (svc LocalProfilesSVC) ParseProfiles() (Profiles, error) { + r, err := os.Open(svc.Path) + if err != nil { + return make(Profiles), nil + } + return ParseProfiles(r) +} + +// WriteProfiles to the path. +func (svc LocalProfilesSVC) WriteProfiles(pp Profiles) 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]Profile{ + "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) +} + +// ParseProfiles decodes profiles from io readers +func ParseProfiles(r io.Reader) (Profiles, error) { + p := make(Profiles) + _, err := toml.DecodeReader(r, &p) + return p, err +} + +// ParseActiveProfile returns the active profile from the reader. +func ParseActiveProfile(r io.Reader) (Profile, error) { + pp, err := ParseProfiles(r) + if err != nil { + return DefaultProfile, err + } + var activated Profile + var hasActive bool + for _, p := range pp { + if p.Active && !hasActive { + activated = p + hasActive = true + } else if p.Active { + return DefaultProfile, &Error{ + Code: EConflict, + Msg: "more than one activated profiles found", + } + } + } + if hasActive { + return activated, nil + } + return DefaultProfile, &Error{ + Code: ENotFound, + Msg: "activated profile is not found", + } +} diff --git a/profile_test.go b/profile_test.go new file mode 100644 index 00000000000..2ec8d13d11f --- /dev/null +++ b/profile_test.go @@ -0,0 +1,122 @@ +package influxdb_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/influxdata/influxdb" + influxtesting "github.com/influxdata/influxdb/testing" +) + +func TestParseActiveProfile(t *testing.T) { + cases := []struct { + name string + hasErr bool + src string + p influxdb.Profile + }{ + { + 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: influxdb.Profile{ + Host: "host2", + Active: true, + }, + }, + } + for _, c := range cases { + r := bytes.NewBufferString(c.src) + p, err := influxdb.ParseActiveProfile(r) + if c.hasErr { + if err == nil { + t.Fatalf("parse active profile %q failed, should have error, got nil", c.name) + } + continue + } + if diff := cmp.Diff(p, c.p); diff != "" { + t.Fatalf("parse active profile %s failed, diff %s", c.name, diff) + } + } +} + +func TestProfilesSwith(t *testing.T) { + cases := []struct { + name string + old influxdb.Profiles + new influxdb.Profiles + target string + err error + }{ + { + name: "not found", + target: "p1", + old: influxdb.Profiles{ + "a1": {Host: "host1"}, + "a2": {Host: "host2"}, + }, + new: influxdb.Profiles{ + "a1": {Host: "host1"}, + "a2": {Host: "host2"}, + }, + err: &influxdb.Error{ + Code: influxdb.ENotFound, + Msg: `profile "p1" is not found`, + }, + }, + { + name: "regular switch", + target: "a3", + old: influxdb.Profiles{ + "a1": {Host: "host1", Active: true}, + "a2": {Host: "host2"}, + "a3": {Host: "host3"}, + }, + new: influxdb.Profiles{ + "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 profile %s failed, diff %s", c.name, diff) + } + } +}