diff --git a/cmd/influx/bucket.go b/cmd/influx/bucket.go index 04337cf..dad8fa6 100644 --- a/cmd/influx/bucket.go +++ b/cmd/influx/bucket.go @@ -1,35 +1,12 @@ package main import ( - "github.com/influxdata/influx-cli/v2/internal" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd/bucket" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" "github.com/urfave/cli/v2" ) -func withBucketsClient() cli.BeforeFunc { - return middleware.WithBeforeFns( - withCli(), - withApi(true), - func(ctx *cli.Context) error { - client := getAPI(ctx) - ctx.App.Metadata["bucketsClient"] = internal.BucketsClients{ - BucketApi: client.BucketsApi, - OrgApi: client.OrganizationsApi, - } - return nil - }, - ) -} - -func getBucketsClient(ctx *cli.Context) internal.BucketsClients { - i, ok := ctx.App.Metadata["bucketsClient"].(internal.BucketsClients) - if !ok { - panic("missing buckets client") - } - return i -} - func newBucketCmd() *cli.Command { return &cli.Command{ Name: "bucket", @@ -45,13 +22,16 @@ func newBucketCmd() *cli.Command { } func newBucketCreateCmd() *cli.Command { - params := internal.BucketsCreateParams{ + params := bucket.BucketsCreateParams{ SchemaType: api.SCHEMATYPE_IMPLICIT, } return &cli.Command{ - Name: "create", - Usage: "Create bucket", - Before: withBucketsClient(), + Name: "create", + Usage: "Create bucket", + Before: middleware.WithBeforeFns( + withCli(), + withApi(true), + ), Flags: append( commonFlags, &cli.StringFlag{ @@ -102,14 +82,19 @@ func newBucketCreateCmd() *cli.Command { }, ), Action: func(ctx *cli.Context) error { - clients := getBucketsClient(ctx) - return getCLI(ctx).BucketsCreate(ctx.Context, &clients, ¶ms) + api := getAPI(ctx) + client := bucket.Client{ + CLI: getCLI(ctx), + BucketsApi: api.BucketsApi, + OrganizationsApi: api.OrganizationsApi, + } + return client.Create(ctx.Context, ¶ms) }, } } func newBucketDeleteCmd() *cli.Command { - var params internal.BucketsDeleteParams + var params bucket.BucketsDeleteParams return &cli.Command{ Name: "delete", Usage: "Delete bucket", @@ -143,13 +128,19 @@ func newBucketDeleteCmd() *cli.Command { }, ), Action: func(ctx *cli.Context) error { - return getCLI(ctx).BucketsDelete(ctx.Context, getAPI(ctx).BucketsApi, ¶ms) + api := getAPI(ctx) + client := bucket.Client{ + CLI: getCLI(ctx), + BucketsApi: api.BucketsApi, + OrganizationsApi: api.OrganizationsApi, + } + return client.Delete(ctx.Context, ¶ms) }, } } func newBucketListCmd() *cli.Command { - var params internal.BucketsListParams + var params bucket.BucketsListParams return &cli.Command{ Name: "list", Usage: "List buckets", @@ -184,13 +175,19 @@ func newBucketListCmd() *cli.Command { }, ), Action: func(ctx *cli.Context) error { - return getCLI(ctx).BucketsList(ctx.Context, getAPI(ctx).BucketsApi, ¶ms) + api := getAPI(ctx) + client := bucket.Client{ + CLI: getCLI(ctx), + BucketsApi: api.BucketsApi, + OrganizationsApi: api.OrganizationsApi, + } + return client.List(ctx.Context, ¶ms) }, } } func newBucketUpdateCmd() *cli.Command { - var params internal.BucketsUpdateParams + var params bucket.BucketsUpdateParams return &cli.Command{ Name: "update", Usage: "Update bucket", @@ -231,7 +228,13 @@ func newBucketUpdateCmd() *cli.Command { }, ), Action: func(ctx *cli.Context) error { - return getCLI(ctx).BucketsUpdate(ctx.Context, getAPI(ctx).BucketsApi, ¶ms) + api := getAPI(ctx) + client := bucket.Client{ + CLI: getCLI(ctx), + BucketsApi: api.BucketsApi, + OrganizationsApi: api.OrganizationsApi, + } + return client.Update(ctx.Context, ¶ms) }, } } diff --git a/cmd/influx/bucket_schema.go b/cmd/influx/bucket_schema.go index f33d9ad..e041bed 100644 --- a/cmd/influx/bucket_schema.go +++ b/cmd/influx/bucket_schema.go @@ -1,7 +1,7 @@ package main import ( - "github.com/influxdata/influx-cli/v2/internal" + "github.com/influxdata/influx-cli/v2/internal/cmd" "github.com/influxdata/influx-cli/v2/internal/cmd/bucket_schema" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" "github.com/influxdata/influx-cli/v2/pkg/influxid" @@ -13,12 +13,11 @@ func withBucketSchemaClient() cli.BeforeFunc { withCli(), withApi(true), func(ctx *cli.Context) error { - c := getCLI(ctx) client := getAPI(ctx) ctx.App.Metadata["measurement_schema"] = bucket_schema.Client{ - BucketApi: client.BucketsApi, + BucketsApi: client.BucketsApi, BucketSchemasApi: client.BucketSchemasApi, - CLI: c, + CLI: getCLI(ctx), } return nil }) @@ -46,7 +45,7 @@ func newBucketSchemaCmd() *cli.Command { func newBucketSchemaCreateCmd() *cli.Command { var params struct { - internal.OrgBucketParams + cmd.OrgBucketParams Name string ColumnsFile string ColumnsFormat bucket_schema.ColumnsFormat @@ -100,7 +99,7 @@ func newBucketSchemaCreateCmd() *cli.Command { func newBucketSchemaUpdateCmd() *cli.Command { var params struct { - internal.OrgBucketParams + cmd.OrgBucketParams ID influxid.ID Name string ColumnsFile string diff --git a/cmd/influx/main.go b/cmd/influx/main.go index cda7df5..089b437 100644 --- a/cmd/influx/main.go +++ b/cmd/influx/main.go @@ -10,8 +10,8 @@ import ( "runtime" "time" - "github.com/influxdata/influx-cli/v2/internal" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" "github.com/influxdata/influx-cli/v2/internal/config" "github.com/influxdata/influx-cli/v2/internal/stdio" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" @@ -114,26 +114,26 @@ 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) (*internal.CLI, error) { +func newCli(ctx *cli.Context) (cmd.CLI, error) { configPath := ctx.String(configPathFlag) var err error if configPath == "" { configPath, err = config.DefaultPath() if err != nil { - return nil, err + return cmd.CLI{}, err } } configSvc := config.NewLocalConfigService(configPath) var activeConfig config.Config if ctx.IsSet(configNameFlag) { if activeConfig, err = configSvc.SwitchActive(ctx.String(configNameFlag)); err != nil { - return nil, err + return cmd.CLI{}, err } } else if activeConfig, err = configSvc.Active(); err != nil { - return nil, err + return cmd.CLI{}, err } - return &internal.CLI{ + return cmd.CLI{ StdIO: stdio.TerminalStdio, PrintAsJSON: ctx.Bool(printJsonFlag), HideTableHeaders: ctx.Bool(hideHeadersFlag), @@ -144,8 +144,8 @@ func newCli(ctx *cli.Context) (*internal.CLI, error) { // newApiClient returns an API client configured to communicate with a remote InfluxDB instance over HTTP. // Client parameters are pulled from the CLI context. -func newApiClient(ctx *cli.Context, cli *internal.CLI, injectToken bool) (*api.APIClient, error) { - cfg, err := cli.ConfigService.Active() +func newApiClient(ctx *cli.Context, configSvc config.Service, injectToken bool) (*api.APIClient, error) { + cfg, err := configSvc.Active() if err != nil { return nil, err } @@ -213,8 +213,8 @@ func withCli() cli.BeforeFunc { } } -func getCLI(ctx *cli.Context) *internal.CLI { - i, ok := ctx.App.Metadata["cli"].(*internal.CLI) +func getCLI(ctx *cli.Context) cmd.CLI { + i, ok := ctx.App.Metadata["cli"].(cmd.CLI) if !ok { panic("missing CLI") } @@ -229,7 +229,7 @@ func withApi(injectToken bool) cli.BeforeFunc { makeFn := func(ctx *cli.Context) error { c := getCLI(ctx) - apiClient, err := newApiClient(ctx, c, injectToken) + apiClient, err := newApiClient(ctx, c.ConfigService, injectToken) if err != nil { return err } diff --git a/cmd/influx/params.go b/cmd/influx/params.go index 49099d4..96f9373 100644 --- a/cmd/influx/params.go +++ b/cmd/influx/params.go @@ -1,11 +1,11 @@ package main import ( - "github.com/influxdata/influx-cli/v2/internal" + "github.com/influxdata/influx-cli/v2/internal/cmd" "github.com/urfave/cli/v2" ) -func getOrgBucketFlags(c *internal.OrgBucketParams) []cli.Flag { +func getOrgBucketFlags(c *cmd.OrgBucketParams) []cli.Flag { return []cli.Flag{ &cli.GenericFlag{ Name: "bucket-id", diff --git a/cmd/influx/ping.go b/cmd/influx/ping.go index ba486ff..2f43c80 100644 --- a/cmd/influx/ping.go +++ b/cmd/influx/ping.go @@ -1,6 +1,7 @@ package main import ( + "github.com/influxdata/influx-cli/v2/internal/cmd/ping" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" "github.com/urfave/cli/v2" ) @@ -12,7 +13,11 @@ func newPingCmd() *cli.Command { Before: middleware.WithBeforeFns(withCli(), withApi(false)), Flags: coreFlags, Action: func(ctx *cli.Context) error { - return getCLI(ctx).Ping(ctx.Context, getAPINoToken(ctx).HealthApi) + client := ping.Client{ + CLI: getCLI(ctx), + HealthApi: getAPINoToken(ctx).HealthApi, + } + return client.Ping(ctx.Context) }, } } diff --git a/cmd/influx/setup.go b/cmd/influx/setup.go index 81baa00..a5325f1 100644 --- a/cmd/influx/setup.go +++ b/cmd/influx/setup.go @@ -1,13 +1,13 @@ package main import ( - "github.com/influxdata/influx-cli/v2/internal" + "github.com/influxdata/influx-cli/v2/internal/cmd/setup" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" "github.com/urfave/cli/v2" ) func newSetupCmd() *cli.Command { - var params internal.SetupParams + var params setup.Params return &cli.Command{ Name: "setup", Usage: "Setup instance with initial user, org, bucket", @@ -67,7 +67,11 @@ func newSetupCmd() *cli.Command { }, ), Action: func(ctx *cli.Context) error { - return getCLI(ctx).Setup(ctx.Context, getAPINoToken(ctx).SetupApi, ¶ms) + client := setup.Client{ + CLI: getCLI(ctx), + SetupApi: getAPINoToken(ctx).SetupApi, + } + return client.Setup(ctx.Context, ¶ms) }, } } diff --git a/cmd/influx/write.go b/cmd/influx/write.go index 53e6e96..8e7ef6a 100644 --- a/cmd/influx/write.go +++ b/cmd/influx/write.go @@ -6,11 +6,8 @@ import ( "net/http" "os" - "github.com/influxdata/influx-cli/v2/internal" "github.com/influxdata/influx-cli/v2/internal/api" - "github.com/influxdata/influx-cli/v2/internal/batcher" - "github.com/influxdata/influx-cli/v2/internal/linereader" - "github.com/influxdata/influx-cli/v2/internal/throttler" + "github.com/influxdata/influx-cli/v2/internal/cmd/write" "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" "github.com/urfave/cli/v2" ) @@ -18,8 +15,8 @@ import ( type writeParams struct { Files cli.StringSlice URLs cli.StringSlice - Format linereader.InputFormat - Compression linereader.InputCompression + Format write.InputFormat + Compression write.InputCompression Encoding string // CSV-specific options. @@ -31,13 +28,13 @@ type writeParams struct { ErrorsFile string MaxLineLength int - RateLimit throttler.BytesPerSec + RateLimit write.BytesPerSec - internal.WriteParams + write.Params } -func (p *writeParams) makeLineReader(args []string, errorOut io.Writer) *linereader.MultiInputLineReader { - return &linereader.MultiInputLineReader{ +func (p *writeParams) makeLineReader(args []string, errorOut io.Writer) *write.MultiInputLineReader { + return &write.MultiInputLineReader{ StdIn: os.Stdin, HttpClient: http.DefaultClient, ErrorOut: errorOut, @@ -183,7 +180,7 @@ func (p *writeParams) Flags() []cli.Flag { func newWriteCmd() *cli.Command { params := writeParams{ - WriteParams: internal.WriteParams{ + Params: write.Params{ Precision: api.WRITEPRECISION_NS, }, } @@ -200,19 +197,19 @@ func newWriteCmd() *cli.Command { } defer func() { _ = errorFile.Close() }() - client := getAPI(ctx) - writeClients := &internal.WriteClients{ - Client: client.WriteApi, - Reader: params.makeLineReader(ctx.Args().Slice(), errorFile), - Throttler: throttler.NewThrottler(params.RateLimit), - Writer: &batcher.BufferBatcher{ - MaxFlushBytes: batcher.DefaultMaxBytes, - MaxFlushInterval: batcher.DefaultInterval, + client := &write.Client{ + CLI: getCLI(ctx), + WriteApi: getAPI(ctx).WriteApi, + LineReader: params.makeLineReader(ctx.Args().Slice(), errorFile), + RateLimiter: write.NewThrottler(params.RateLimit), + BatchWriter: &write.BufferBatcher{ + MaxFlushBytes: write.DefaultMaxBytes, + MaxFlushInterval: write.DefaultInterval, MaxLineLength: params.MaxLineLength, }, } - return getCLI(ctx).Write(ctx.Context, writeClients, ¶ms.WriteParams) + return client.Write(ctx.Context, ¶ms.Params) }, Subcommands: []*cli.Command{ newWriteDryRun(), @@ -222,7 +219,7 @@ func newWriteCmd() *cli.Command { func newWriteDryRun() *cli.Command { params := writeParams{ - WriteParams: internal.WriteParams{ + Params: write.Params{ Precision: api.WRITEPRECISION_NS, }, } @@ -240,7 +237,11 @@ func newWriteDryRun() *cli.Command { } defer func() { _ = errorFile.Close() }() - return getCLI(ctx).WriteDryRun(ctx.Context, params.makeLineReader(ctx.Args().Slice(), errorFile)) + client := write.DryRunClient{ + CLI: getCLI(ctx), + LineReader: params.makeLineReader(ctx.Args().Slice(), errorFile), + } + return client.WriteDryRun(ctx.Context) }, } } diff --git a/internal/bucket.go b/internal/bucket.go deleted file mode 100644 index 7358f65..0000000 --- a/internal/bucket.go +++ /dev/null @@ -1,306 +0,0 @@ -package internal - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/influxdata/influx-cli/v2/internal/api" - "github.com/influxdata/influx-cli/v2/internal/duration" -) - -const InfiniteRetention = 0 - -type BucketsClients struct { - OrgApi api.OrganizationsApi - BucketApi api.BucketsApi -} - -type BucketsCreateParams struct { - OrgID string - OrgName string - Name string - Description string - Retention string - ShardGroupDuration string - SchemaType api.SchemaType -} - -var ( - ErrMustSpecifyOrg = errors.New("must specify org ID or org name") - ErrMustSpecifyOrgDeleteByName = errors.New("must specify org ID or org name when deleting bucket by name") - ErrMustSpecifyBucket = errors.New("must specify bucket ID or bucket name") -) - -func (c *CLI) BucketsCreate(ctx context.Context, clients *BucketsClients, params *BucketsCreateParams) error { - if params.OrgID == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { - return ErrMustSpecifyOrg - } - - rp, err := duration.RawDurationToTimeDuration(params.Retention) - if err != nil { - return err - } - sgd, err := duration.RawDurationToTimeDuration(params.ShardGroupDuration) - if err != nil { - return err - } - - reqBody := api.PostBucketRequest{ - OrgID: params.OrgID, - Name: params.Name, - RetentionRules: []api.RetentionRule{}, - SchemaType: ¶ms.SchemaType, - } - if params.Description != "" { - reqBody.Description = ¶ms.Description - } - // Only append a retention rule if the user wants to explicitly set - // a parameter on the rule. - // - // This is for backwards-compatibility with older versions of the API, - // which didn't support setting shard-group durations and used an empty - // array of rules to represent infinite retention. - if rp > 0 || sgd > 0 { - rule := api.NewRetentionRuleWithDefaults() - if rp > 0 { - rule.SetEverySeconds(int64(rp.Round(time.Second) / time.Second)) - } - if sgd > 0 { - rule.SetShardGroupDurationSeconds(int64(sgd.Round(time.Second) / time.Second)) - } - reqBody.RetentionRules = append(reqBody.RetentionRules, *rule) - } - if reqBody.OrgID == "" { - name := params.OrgName - if name == "" { - name = c.ActiveConfig.Org - } - resp, err := clients.OrgApi.GetOrgs(ctx).Org(name).Execute() - if err != nil { - return fmt.Errorf("failed to lookup ID of org %q: %w", name, err) - } - orgs := resp.GetOrgs() - if len(orgs) == 0 { - return fmt.Errorf("no organization found with name %q", name) - } - reqBody.OrgID = orgs[0].GetId() - } - - bucket, err := clients.BucketApi.PostBuckets(ctx).PostBucketRequest(reqBody).Execute() - if err != nil { - return fmt.Errorf("failed to create bucket: %w", err) - } - - return c.printBuckets(bucketPrintOptions{bucket: &bucket}) -} - -type BucketsListParams struct { - OrgID string - OrgName string - Name string - ID string -} - -func (c *CLI) BucketsList(ctx context.Context, client api.BucketsApi, params *BucketsListParams) error { - if params.OrgID == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { - return ErrMustSpecifyOrg - } - - req := client.GetBuckets(ctx) - if params.OrgID != "" { - req = req.OrgID(params.OrgID) - } - if params.OrgName != "" { - req = req.Org(params.OrgName) - } - if params.OrgID == "" && params.OrgName == "" { - req = req.Org(c.ActiveConfig.Org) - } - if params.Name != "" { - req = req.Name(params.Name) - } - if params.ID != "" { - req = req.Id(params.ID) - } - - buckets, err := req.Execute() - if err != nil { - return fmt.Errorf("failed to list buckets: %w", err) - } - - printOpts := bucketPrintOptions{} - if buckets.Buckets != nil { - printOpts.buckets = *buckets.Buckets - } - return c.printBuckets(printOpts) -} - -type BucketsUpdateParams struct { - ID string - Name string - Description string - Retention string - ShardGroupDuration string -} - -func (c *CLI) BucketsUpdate(ctx context.Context, client api.BucketsApi, params *BucketsUpdateParams) error { - reqBody := api.PatchBucketRequest{} - if params.Name != "" { - reqBody.SetName(params.Name) - } - if params.Description != "" { - reqBody.SetDescription(params.Description) - } - if params.Retention != "" || params.ShardGroupDuration != "" { - patchRule := api.NewPatchRetentionRuleWithDefaults() - if params.Retention != "" { - rp, err := duration.RawDurationToTimeDuration(params.Retention) - if err != nil { - return err - } - patchRule.SetEverySeconds(int64(rp.Round(time.Second) / time.Second)) - } - if params.ShardGroupDuration != "" { - sgd, err := duration.RawDurationToTimeDuration(params.ShardGroupDuration) - if err != nil { - return err - } - patchRule.SetShardGroupDurationSeconds(int64(sgd.Round(time.Second) / time.Second)) - } - reqBody.SetRetentionRules([]api.PatchRetentionRule{*patchRule}) - } - - bucket, err := client.PatchBucketsID(ctx, params.ID).PatchBucketRequest(reqBody).Execute() - if err != nil { - return fmt.Errorf("failed to update bucket %q: %w", params.ID, err) - } - - return c.printBuckets(bucketPrintOptions{bucket: &bucket}) -} - -type BucketsDeleteParams struct { - ID string - Name string - OrgID string - OrgName string -} - -func (c *CLI) BucketsDelete(ctx context.Context, client api.BucketsApi, params *BucketsDeleteParams) error { - if params.ID == "" && params.Name == "" { - return ErrMustSpecifyBucket - } - - var bucket api.Bucket - var getReq api.ApiGetBucketsRequest - if params.ID != "" { - getReq = client.GetBuckets(ctx).Id(params.ID) - } else { - if params.OrgID == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { - return ErrMustSpecifyOrgDeleteByName - } - getReq = client.GetBuckets(ctx) - getReq = getReq.Name(params.Name) - if params.OrgID != "" { - getReq = getReq.OrgID(params.OrgID) - } - if params.OrgName != "" { - getReq = getReq.Org(params.OrgName) - } - if params.OrgID == "" && params.OrgName == "" { - getReq = getReq.Org(c.ActiveConfig.Org) - } - } - - displayId := params.ID - if displayId == "" { - displayId = params.Name - } - - resp, err := getReq.Execute() - if err != nil { - return fmt.Errorf("failed to find bucket %q: %w", displayId, err) - } - buckets := resp.GetBuckets() - if len(buckets) == 0 { - return fmt.Errorf("bucket %q not found", displayId) - } - bucket = buckets[0] - - if err := client.DeleteBucketsID(ctx, bucket.GetId()).Execute(); err != nil { - return fmt.Errorf("failed to delete bucket %q: %w", displayId, err) - } - - return c.printBuckets(bucketPrintOptions{bucket: &bucket, deleted: true}) -} - -type bucketPrintOptions struct { - deleted bool - bucket *api.Bucket - buckets []api.Bucket -} - -func (c *CLI) printBuckets(options bucketPrintOptions) error { - if c.PrintAsJSON { - var v interface{} - if options.bucket != nil { - v = options.bucket - } else { - v = options.buckets - } - return c.PrintJSON(v) - } - - headers := []string{"ID", "Name", "Retention", "Shard group duration", "Organization ID", "Schema Type"} - if options.deleted { - headers = append(headers, "Deleted") - } - - if options.bucket != nil { - options.buckets = append(options.buckets, *options.bucket) - } - - var rows []map[string]interface{} - for _, bkt := range options.buckets { - var rpDuration time.Duration // zero value implies infinite retention policy - var sgDuration time.Duration // zero value implies the server should pick a value - - if rules := bkt.GetRetentionRules(); len(rules) > 0 { - rpDuration = time.Duration(rules[0].GetEverySeconds()) * time.Second - sgDuration = time.Duration(rules[0].GetShardGroupDurationSeconds()) * time.Second - } - - rp := rpDuration.String() - if rpDuration == InfiniteRetention { - rp = "infinite" - } - sgDur := sgDuration.String() - // ShardGroupDuration will be zero if listing buckets from InfluxDB Cloud. - // Show something more useful here in that case. - if sgDuration == 0 { - sgDur = "n/a" - } - - schemaType := bkt.GetSchemaType() - if schemaType == "" { - // schemaType will be empty when querying OSS. - schemaType = api.SCHEMATYPE_IMPLICIT - } - - row := map[string]interface{}{ - "ID": bkt.GetId(), - "Name": bkt.GetName(), - "Retention": rp, - "Shard group duration": sgDur, - "Organization ID": bkt.GetOrgID(), - "Schema Type": schemaType, - } - if options.deleted { - row["Deleted"] = true - } - rows = append(rows, row) - } - - return c.PrintTable(headers, rows...) -} diff --git a/internal/bucket_test.go b/internal/bucket_test.go deleted file mode 100644 index 0050188..0000000 --- a/internal/bucket_test.go +++ /dev/null @@ -1,865 +0,0 @@ -package internal_test - -import ( - "bytes" - "context" - "strings" - "testing" - - "github.com/golang/mock/gomock" - "github.com/influxdata/influx-cli/v2/internal" - "github.com/influxdata/influx-cli/v2/internal/api" - "github.com/influxdata/influx-cli/v2/internal/config" - "github.com/influxdata/influx-cli/v2/internal/mock" - "github.com/stretchr/testify/assert" - tmock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestBucketsCreate(t *testing.T) { - t.Parallel() - - var testCases = []struct { - name string - configOrgName string - params internal.BucketsCreateParams - registerOrgExpectations func(*testing.T, *mock.MockOrganizationsApi) - registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) - expectedStdoutPattern string - expectedInErr string - }{ - { - name: "minimal", - params: internal.BucketsCreateParams{ - OrgID: "123", - Name: "my-bucket", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT(). - PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { - body := in.GetPostBucketRequest() - return assert.NotNil(t, body) && - assert.Equal(t, "123", body.OrgID) && - assert.Equal(t, "my-bucket", body.Name) && - assert.Nil(t, body.Description) && - assert.Empty(t, body.RetentionRules) - })). - Return(api.Bucket{ - Id: api.PtrString("456"), - OrgID: api.PtrString("123"), - Name: "my-bucket", - RetentionRules: nil, - }, nil) - }, - expectedStdoutPattern: "456\\s+my-bucket\\s+infinite\\s+n/a\\s+123", - }, - { - name: "fully specified", - params: internal.BucketsCreateParams{ - OrgID: "123", - Name: "my-bucket", - Description: "my cool bucket", - Retention: "24h", - ShardGroupDuration: "1h", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT(). - PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { - body := in.GetPostBucketRequest() - return assert.NotNil(t, body) && - assert.Equal(t, "123", body.OrgID) && - assert.Equal(t, "my-bucket", body.Name) && - assert.Equal(t, "my cool bucket", *body.Description) && - assert.Len(t, body.RetentionRules, 1) && - assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && - assert.Equal(t, int64(3600), *body.RetentionRules[0].ShardGroupDurationSeconds) - })). - Return(api.Bucket{ - Id: api.PtrString("456"), - OrgID: api.PtrString("123"), - Name: "my-bucket", - RetentionRules: []api.RetentionRule{ - {EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)}, - }, - }, nil) - }, - expectedStdoutPattern: "456\\s+my-bucket\\s+24h0m0s\\s+1h0m0s\\s+123", - }, - { - name: "retention but not shard-group duration", - params: internal.BucketsCreateParams{ - OrgID: "123", - Name: "my-bucket", - Retention: "24h", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT(). - PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { - body := in.GetPostBucketRequest() - return assert.NotNil(t, body) && - assert.Equal(t, "123", body.OrgID) && - assert.Equal(t, "my-bucket", body.Name) && - assert.Nil(t, body.Description) && - assert.Len(t, body.RetentionRules, 1) && - assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && - assert.Nil(t, body.RetentionRules[0].ShardGroupDurationSeconds) - })). - Return(api.Bucket{ - Id: api.PtrString("456"), - OrgID: api.PtrString("123"), - Name: "my-bucket", - RetentionRules: []api.RetentionRule{{EverySeconds: 86400}}, - }, nil) - }, - }, - { - name: "create bucket with explicit schema", - params: internal.BucketsCreateParams{ - OrgID: "123", - Name: "my-bucket", - SchemaType: api.SCHEMATYPE_EXPLICIT, - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT(). - PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { - body := in.GetPostBucketRequest() - return assert.NotNil(t, body) && - assert.Equal(t, "123", body.OrgID) && - assert.Equal(t, "my-bucket", body.Name) && - assert.Nil(t, body.Description) && - assert.Empty(t, body.RetentionRules) && - assert.Equal(t, api.SCHEMATYPE_EXPLICIT, *body.SchemaType) - })). - Return(api.Bucket{ - Id: api.PtrString("456"), - OrgID: api.PtrString("123"), - Name: "my-bucket", - SchemaType: api.SCHEMATYPE_EXPLICIT.Ptr(), - }, nil) - }, - expectedStdoutPattern: `456\s+my-bucket\s+infinite\s+n/a\s+123\s+explicit`, - }, - { - name: "look up org by name", - params: internal.BucketsCreateParams{ - OrgName: "my-org", - Name: "my-bucket", - Description: "my cool bucket", - Retention: "24h", - ShardGroupDuration: "1h", - }, - registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { - orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) - orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { - return assert.Equal(t, "my-org", *in.GetOrg()) - })). - Return(api.Organizations{ - Orgs: &[]api.Organization{{Id: api.PtrString("123")}}, - }, nil) - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT(). - PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { - body := in.GetPostBucketRequest() - return assert.NotNil(t, body) && - assert.Equal(t, "123", body.OrgID) && - assert.Equal(t, "my-bucket", body.Name) && - assert.Equal(t, "my cool bucket", *body.Description) && - assert.Len(t, body.RetentionRules, 1) && - assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && - assert.Equal(t, int64(3600), *body.RetentionRules[0].ShardGroupDurationSeconds) - })). - Return(api.Bucket{ - Id: api.PtrString("456"), - OrgID: api.PtrString("123"), - Name: "my-bucket", - RetentionRules: []api.RetentionRule{ - {EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)}, - }, - }, nil) - }, - expectedStdoutPattern: "456\\s+my-bucket\\s+24h0m0s\\s+1h0m0s\\s+123", - }, - { - name: "look up org by name from config", - params: internal.BucketsCreateParams{ - Name: "my-bucket", - Description: "my cool bucket", - Retention: "24h", - ShardGroupDuration: "1h", - }, - configOrgName: "my-org", - registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { - orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) - orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { - return assert.Equal(t, "my-org", *in.GetOrg()) - })). - Return(api.Organizations{ - Orgs: &[]api.Organization{{Id: api.PtrString("123")}}, - }, nil) - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT(). - PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { - body := in.GetPostBucketRequest() - return assert.NotNil(t, body) && - assert.Equal(t, "123", body.OrgID) && - assert.Equal(t, "my-bucket", body.Name) && - assert.Equal(t, "my cool bucket", *body.Description) && - assert.Len(t, body.RetentionRules, 1) && - assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && - assert.Equal(t, int64(3600), *body.RetentionRules[0].ShardGroupDurationSeconds) - })). - Return(api.Bucket{ - Id: api.PtrString("456"), - OrgID: api.PtrString("123"), - Name: "my-bucket", - RetentionRules: []api.RetentionRule{ - {EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)}, - }, - }, nil) - }, - expectedStdoutPattern: "456\\s+my-bucket\\s+24h0m0s\\s+1h0m0s\\s+123", - }, - { - name: "no org specified", - params: internal.BucketsCreateParams{ - Name: "my-bucket", - Description: "my cool bucket", - Retention: "24h", - ShardGroupDuration: "1h", - }, - expectedInErr: "must specify org ID or org name", - }, - { - name: "no such org", - params: internal.BucketsCreateParams{ - Name: "my-bucket", - OrgName: "fake-org", - Description: "my cool bucket", - Retention: "24h", - ShardGroupDuration: "1h", - }, - registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { - orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) - orgApi.EXPECT().GetOrgsExecute(gomock.Any()).Return(api.Organizations{}, nil) - }, - expectedInErr: "no organization found", - }, - } - - 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() - - orgApi := mock.NewMockOrganizationsApi(ctrl) - if tc.registerOrgExpectations != nil { - tc.registerOrgExpectations(t, orgApi) - } - bucketApi := mock.NewMockBucketsApi(ctrl) - if tc.registerBucketExpectations != nil { - tc.registerBucketExpectations(t, bucketApi) - } - cli := internal.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio} - clients := internal.BucketsClients{ - OrgApi: orgApi, - BucketApi: bucketApi, - } - - err := cli.BucketsCreate(context.Background(), &clients, &tc.params) - if tc.expectedInErr != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tc.expectedInErr) - require.Empty(t, writtenBytes.String()) - return - } - require.NoError(t, err) - outLines := strings.Split(writtenBytes.String(), "\n") - if outLines[len(outLines)-1] == "" { - outLines = outLines[:len(outLines)-1] - } - require.Equal(t, 2, len(outLines)) - require.Regexp(t, "ID\\s+Name\\s+Retention\\s+Shard group duration\\s+Organization ID", outLines[0]) - require.Regexp(t, tc.expectedStdoutPattern, outLines[1]) - }) - } -} - -func TestBucketsList(t *testing.T) { - t.Parallel() - - var testCases = []struct { - name string - configOrgName string - params internal.BucketsListParams - registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) - expectedStdoutPatterns []string - expectedInErr string - }{ - { - name: "by ID", - params: internal.BucketsListParams{ - ID: "123", - }, - configOrgName: "my-default-org", - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Equal(t, "123", *in.GetId()) && - assert.Equal(t, "my-default-org", *in.GetOrg()) && - assert.Nil(t, in.GetName()) && - assert.Nil(t, in.GetOrgID()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - }, - }, nil) - }, - expectedStdoutPatterns: []string{ - "123\\s+my-bucket\\s+1h0m0s\\s+n/a\\s+456", - }, - }, - { - name: "by name", - params: internal.BucketsListParams{ - Name: "my-bucket", - }, - configOrgName: "my-default-org", - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Equal(t, "my-bucket", *in.GetName()) && - assert.Equal(t, "my-default-org", *in.GetOrg()) && - assert.Nil(t, in.GetId()) && - assert.Nil(t, in.GetOrgID()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - }, - }, nil) - }, - expectedStdoutPatterns: []string{ - "123\\s+my-bucket\\s+1h0m0s\\s+n/a\\s+456", - }, - }, - { - name: "override org by ID", - params: internal.BucketsListParams{ - OrgID: "456", - }, - configOrgName: "my-default-org", - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Equal(t, "456", *in.GetOrgID()) && - assert.Nil(t, in.GetId()) && - assert.Nil(t, in.GetOrg()) && - assert.Nil(t, in.GetName()) - })).Return(api.Buckets{}, nil) - }, - }, - { - name: "override org by name", - params: internal.BucketsListParams{ - OrgName: "my-org", - }, - configOrgName: "my-default-org", - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Equal(t, "my-org", *in.GetOrg()) && - assert.Nil(t, in.GetId()) && - assert.Nil(t, in.GetName()) && - assert.Nil(t, in.GetOrgID()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - { - Id: api.PtrString("999"), - Name: "bucket2", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 0, ShardGroupDurationSeconds: api.PtrInt64(60)}, - }, - }, - }, - }, nil) - }, - expectedStdoutPatterns: []string{ - "123\\s+my-bucket\\s+1h0m0s\\s+n/a\\s+456", - "999\\s+bucket2\\s+infinite\\s+1m0s\\s+456", - }, - }, - { - name: "list multiple bucket schema types", - params: internal.BucketsListParams{ - OrgName: "my-org", - }, - configOrgName: "my-default-org", - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Equal(t, "my-org", *in.GetOrg()) && - assert.Nil(t, in.GetId()) && - assert.Nil(t, in.GetName()) && - assert.Nil(t, in.GetOrgID()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("001"), - Name: "omit-schema-type", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - { - Id: api.PtrString("002"), - Name: "implicit-schema-type", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - SchemaType: api.SCHEMATYPE_IMPLICIT.Ptr(), - }, - { - Id: api.PtrString("003"), - Name: "explicit-schema-type", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - SchemaType: api.SCHEMATYPE_EXPLICIT.Ptr(), - }, - }, - }, nil) - }, - expectedStdoutPatterns: []string{ - `001\s+omit-schema-type\s+1h0m0s\s+n/a\s+456\s+implicit`, - `002\s+implicit-schema-type\s+1h0m0s\s+n/a\s+456\s+implicit`, - `003\s+explicit-schema-type\s+1h0m0s\s+n/a\s+456\s+explicit`, - }, - }, - { - name: "no org specified", - expectedInErr: "must specify org ID or org name", - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - stdio := mock.NewMockStdIO(ctrl) - bytesWritten := bytes.Buffer{} - stdio.EXPECT().Write(gomock.Any()).DoAndReturn(bytesWritten.Write).AnyTimes() - cli := internal.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio} - - client := mock.NewMockBucketsApi(ctrl) - if tc.registerBucketExpectations != nil { - tc.registerBucketExpectations(t, client) - } - - err := cli.BucketsList(context.Background(), client, &tc.params) - if tc.expectedInErr != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tc.expectedInErr) - require.Empty(t, bytesWritten.String()) - return - } - require.NoError(t, err) - outLines := strings.Split(bytesWritten.String(), "\n") - if outLines[len(outLines)-1] == "" { - outLines = outLines[:len(outLines)-1] - } - require.Equal(t, len(tc.expectedStdoutPatterns)+1, len(outLines)) - require.Regexp(t, "ID\\s+Name\\s+Retention\\s+Shard group duration\\s+Organization ID", outLines[0]) - for i, pattern := range tc.expectedStdoutPatterns { - require.Regexp(t, pattern, outLines[i+1]) - } - }) - } -} - -func TestBucketsUpdate(t *testing.T) { - t.Parallel() - - var testCases = []struct { - name string - params internal.BucketsUpdateParams - registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) - expectedStdoutPattern string - }{ - { - name: "name", - params: internal.BucketsUpdateParams{ - ID: "123", - Name: "cold-storage", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { - body := in.GetPatchBucketRequest() - return assert.Equal(t, "123", in.GetBucketID()) && - assert.NotNil(t, body) && - assert.Equal(t, "cold-storage", body.GetName()) && - assert.Nil(t, body.Description) && - assert.Empty(t, body.GetRetentionRules()) - })).Return(api.Bucket{ - Id: api.PtrString("123"), - Name: "cold-storage", - OrgID: api.PtrString("456"), - }, nil) - }, - expectedStdoutPattern: "123\\s+cold-storage\\s+infinite\\s+n/a\\s+456", - }, - { - name: "description", - params: internal.BucketsUpdateParams{ - ID: "123", - Description: "a very useful description", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { - body := in.GetPatchBucketRequest() - return assert.Equal(t, "123", in.GetBucketID()) && - assert.NotNil(t, body) && - assert.Equal(t, "a very useful description", body.GetDescription()) && - assert.Nil(t, body.Name) && - assert.Empty(t, body.GetRetentionRules()) - })).Return(api.Bucket{ - Id: api.PtrString("123"), - Name: "my-bucket", - Description: api.PtrString("a very useful description"), - OrgID: api.PtrString("456"), - }, nil) - }, - expectedStdoutPattern: "123\\s+my-bucket\\s+infinite\\s+n/a\\s+456", - }, - { - name: "retention", - params: internal.BucketsUpdateParams{ - ID: "123", - Retention: "3w", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { - body := in.GetPatchBucketRequest() - return assert.Equal(t, "123", in.GetBucketID()) && - assert.NotNil(t, body) && - assert.Nil(t, body.Name) && - assert.Nil(t, body.Description) && - assert.Len(t, body.GetRetentionRules(), 1) && - assert.Nil(t, body.GetRetentionRules()[0].ShardGroupDurationSeconds) && - assert.Equal(t, int64(3*7*24*3600), *body.GetRetentionRules()[0].EverySeconds) - })).Return(api.Bucket{ - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: int64(3 * 7 * 24 * 3600)}, - }, - }, nil) - }, - expectedStdoutPattern: "123\\s+my-bucket\\s+504h0m0s\\s+n/a\\s+456", - }, - { - name: "shard-group duration", - params: internal.BucketsUpdateParams{ - ID: "123", - ShardGroupDuration: "10h30m", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { - body := in.GetPatchBucketRequest() - return assert.Equal(t, "123", in.GetBucketID()) && - assert.NotNil(t, body) && - assert.Nil(t, body.Name) && - assert.Nil(t, body.Description) && - assert.Len(t, body.GetRetentionRules(), 1) && - assert.Nil(t, body.GetRetentionRules()[0].EverySeconds) && - assert.Equal(t, int64(10*3600+30*60), *body.GetRetentionRules()[0].ShardGroupDurationSeconds) - })).Return(api.Bucket{ - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {ShardGroupDurationSeconds: api.PtrInt64(10*3600 + 30*60)}, - }, - }, nil) - }, - expectedStdoutPattern: "123\\s+my-bucket\\s+infinite\\s+10h30m0s\\s+456", - }, - } - - 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() - cli := internal.CLI{StdIO: stdio} - client := mock.NewMockBucketsApi(ctrl) - if tc.registerBucketExpectations != nil { - tc.registerBucketExpectations(t, client) - } - - err := cli.BucketsUpdate(context.Background(), client, &tc.params) - require.NoError(t, err) - outLines := strings.Split(writtenBytes.String(), "\n") - if outLines[len(outLines)-1] == "" { - outLines = outLines[:len(outLines)-1] - } - require.Equal(t, 2, len(outLines)) - require.Regexp(t, "ID\\s+Name\\s+Retention\\s+Shard group duration\\s+Organization ID", outLines[0]) - require.Regexp(t, tc.expectedStdoutPattern, outLines[1]) - }) - } -} - -func TestBucketsDelete(t *testing.T) { - t.Parallel() - - var testCases = []struct { - name string - configOrgName string - params internal.BucketsDeleteParams - registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) - expectedStdoutPattern string - expectedInErr string - }{ - { - name: "by ID", - configOrgName: "my-default-org", - params: internal.BucketsDeleteParams{ - ID: "123", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Equal(t, "123", *in.GetId()) && - assert.Nil(t, in.GetName()) && - assert.Nil(t, in.GetOrgID()) && - assert.Nil(t, in.GetOrg()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - }, - }, nil) - - bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { - return assert.Equal(t, "123", in.GetBucketID()) - })).Return(nil) - }, - expectedStdoutPattern: "123\\s+my-bucket\\s+1h0m0s\\s+n/a\\s+456\\s+implicit", - }, - { - name: "by name and org ID", - configOrgName: "my-default-org", - params: internal.BucketsDeleteParams{ - Name: "my-bucket", - OrgID: "456", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Nil(t, in.GetId()) && - assert.Equal(t, "my-bucket", *in.GetName()) && - assert.Equal(t, "456", *in.GetOrgID()) && - assert.Nil(t, in.GetOrg()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - }, - }, nil) - - bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { - return assert.Equal(t, "123", in.GetBucketID()) - })).Return(nil) - }, - expectedStdoutPattern: "123\\s+my-bucket\\s+1h0m0s\\s+n/a\\s+456\\s+implicit", - }, - { - name: "by name and org name", - configOrgName: "my-default-org", - params: internal.BucketsDeleteParams{ - Name: "my-bucket", - OrgName: "my-org", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Nil(t, in.GetId()) && - assert.Equal(t, "my-bucket", *in.GetName()) && - assert.Nil(t, in.GetOrgID()) && - assert.Equal(t, "my-org", *in.GetOrg()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - }, - }, nil) - - bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { - return assert.Equal(t, "123", in.GetBucketID()) - })).Return(nil) - }, - expectedStdoutPattern: "123\\s+my-bucket\\s+1h0m0s\\s+n/a\\s+456\\s+implicit", - }, - { - name: "by name in default org", - configOrgName: "my-default-org", - params: internal.BucketsDeleteParams{ - Name: "my-bucket", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return assert.Nil(t, in.GetId()) && - assert.Equal(t, "my-bucket", *in.GetName()) && - assert.Nil(t, in.GetOrgID()) && - assert.Equal(t, "my-default-org", *in.GetOrg()) - })).Return(api.Buckets{ - Buckets: &[]api.Bucket{ - { - Id: api.PtrString("123"), - Name: "my-bucket", - OrgID: api.PtrString("456"), - RetentionRules: []api.RetentionRule{ - {EverySeconds: 3600}, - }, - }, - }, - }, nil) - - bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). - Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) - bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { - return assert.Equal(t, "123", in.GetBucketID()) - })).Return(nil) - }, - expectedStdoutPattern: "123\\s+my-bucket\\s+1h0m0s\\s+n/a\\s+456\\s+implicit", - }, - { - name: "by name without org", - params: internal.BucketsDeleteParams{ - Name: "my-bucket", - }, - expectedInErr: "must specify org ID or org name", - }, - { - name: "no such bucket", - params: internal.BucketsDeleteParams{ - ID: "123", - }, - registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { - bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) - bucketsApi.EXPECT().GetBucketsExecute(gomock.Any()).Return(api.Buckets{}, nil) - }, - expectedInErr: "not found", - }, - } - - 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() - cli := internal.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio} - client := mock.NewMockBucketsApi(ctrl) - if tc.registerBucketExpectations != nil { - tc.registerBucketExpectations(t, client) - } - - err := cli.BucketsDelete(context.Background(), client, &tc.params) - if tc.expectedInErr != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tc.expectedInErr) - require.Empty(t, writtenBytes.String()) - return - } - require.NoError(t, err) - outLines := strings.Split(writtenBytes.String(), "\n") - if outLines[len(outLines)-1] == "" { - outLines = outLines[:len(outLines)-1] - } - require.Regexp(t, `ID\s+Name\s+Retention\s+Shard group duration\s+Organization ID\s+Schema Type\s+Deleted`, outLines[0]) - require.Regexp(t, tc.expectedStdoutPattern+"\\s+true", outLines[1]) - }) - } -} diff --git a/internal/cmd/bucket/client.go b/internal/cmd/bucket/client.go new file mode 100644 index 0000000..4e21057 --- /dev/null +++ b/internal/cmd/bucket/client.go @@ -0,0 +1,93 @@ +package bucket + +import ( + "errors" + "time" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" +) + +const InfiniteRetention = 0 + +var ( + ErrMustSpecifyOrg = errors.New("must specify org ID or org name") + ErrMustSpecifyOrgDeleteByName = errors.New("must specify org ID or org name when deleting bucket by name") + ErrMustSpecifyBucket = errors.New("must specify bucket ID or bucket name") +) + +type Client struct { + cmd.CLI + api.OrganizationsApi + api.BucketsApi +} + +type bucketPrintOptions struct { + deleted bool + bucket *api.Bucket + buckets []api.Bucket +} + +func (c Client) printBuckets(options bucketPrintOptions) error { + if c.PrintAsJSON { + var v interface{} + if options.bucket != nil { + v = options.bucket + } else { + v = options.buckets + } + return c.PrintJSON(v) + } + + headers := []string{"ID", "Name", "Retention", "Shard group duration", "Organization ID", "Schema Type"} + if options.deleted { + headers = append(headers, "Deleted") + } + + if options.bucket != nil { + options.buckets = append(options.buckets, *options.bucket) + } + + var rows []map[string]interface{} + for _, bkt := range options.buckets { + var rpDuration time.Duration // zero value implies infinite retention policy + var sgDuration time.Duration // zero value implies the server should pick a value + + if rules := bkt.GetRetentionRules(); len(rules) > 0 { + rpDuration = time.Duration(rules[0].GetEverySeconds()) * time.Second + sgDuration = time.Duration(rules[0].GetShardGroupDurationSeconds()) * time.Second + } + + rp := rpDuration.String() + if rpDuration == InfiniteRetention { + rp = "infinite" + } + sgDur := sgDuration.String() + // ShardGroupDuration will be zero if listing buckets from InfluxDB Cloud. + // Show something more useful here in that case. + if sgDuration == 0 { + sgDur = "n/a" + } + + schemaType := bkt.GetSchemaType() + if schemaType == "" { + // schemaType will be empty when querying OSS. + schemaType = api.SCHEMATYPE_IMPLICIT + } + + row := map[string]interface{}{ + "ID": bkt.GetId(), + "Name": bkt.GetName(), + "Retention": rp, + "Shard group duration": sgDur, + "Organization ID": bkt.GetOrgID(), + "Schema Type": schemaType, + } + if options.deleted { + row["Deleted"] = true + } + rows = append(rows, row) + } + + return c.PrintTable(headers, rows...) +} diff --git a/internal/cmd/bucket/create.go b/internal/cmd/bucket/create.go new file mode 100644 index 0000000..e1012ff --- /dev/null +++ b/internal/cmd/bucket/create.go @@ -0,0 +1,83 @@ +package bucket + +import ( + "context" + "fmt" + "time" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/duration" +) + +type BucketsCreateParams struct { + OrgID string + OrgName string + Name string + Description string + Retention string + ShardGroupDuration string + SchemaType api.SchemaType +} + +func (c Client) Create(ctx context.Context, params *BucketsCreateParams) error { + if params.OrgID == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { + return ErrMustSpecifyOrg + } + + rp, err := duration.RawDurationToTimeDuration(params.Retention) + if err != nil { + return err + } + sgd, err := duration.RawDurationToTimeDuration(params.ShardGroupDuration) + if err != nil { + return err + } + + reqBody := api.PostBucketRequest{ + OrgID: params.OrgID, + Name: params.Name, + RetentionRules: []api.RetentionRule{}, + SchemaType: ¶ms.SchemaType, + } + if params.Description != "" { + reqBody.Description = ¶ms.Description + } + // Only append a retention rule if the user wants to explicitly set + // a parameter on the rule. + // + // This is for backwards-compatibility with older versions of the API, + // which didn't support setting shard-group durations and used an empty + // array of rules to represent infinite retention. + if rp > 0 || sgd > 0 { + rule := api.NewRetentionRuleWithDefaults() + if rp > 0 { + rule.SetEverySeconds(int64(rp.Round(time.Second) / time.Second)) + } + if sgd > 0 { + rule.SetShardGroupDurationSeconds(int64(sgd.Round(time.Second) / time.Second)) + } + reqBody.RetentionRules = append(reqBody.RetentionRules, *rule) + } + if reqBody.OrgID == "" { + name := params.OrgName + if name == "" { + name = c.ActiveConfig.Org + } + resp, err := c.GetOrgs(ctx).Org(name).Execute() + if err != nil { + return fmt.Errorf("failed to lookup ID of org %q: %w", name, err) + } + orgs := resp.GetOrgs() + if len(orgs) == 0 { + return fmt.Errorf("no organization found with name %q", name) + } + reqBody.OrgID = orgs[0].GetId() + } + + bucket, err := c.PostBuckets(ctx).PostBucketRequest(reqBody).Execute() + if err != nil { + return fmt.Errorf("failed to create bucket: %w", err) + } + + return c.printBuckets(bucketPrintOptions{bucket: &bucket}) +} diff --git a/internal/cmd/bucket/create_test.go b/internal/cmd/bucket/create_test.go new file mode 100644 index 0000000..3914471 --- /dev/null +++ b/internal/cmd/bucket/create_test.go @@ -0,0 +1,297 @@ +package bucket_test + +import ( + "bytes" + "context" + "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/bucket" + "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/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestBucketsCreate(t *testing.T) { + t.Parallel() + + var testCases = []struct { + name string + configOrgName string + params bucket.BucketsCreateParams + registerOrgExpectations func(*testing.T, *mock.MockOrganizationsApi) + registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) + expectedStdoutPattern string + expectedInErr string + }{ + { + name: "minimal", + params: bucket.BucketsCreateParams{ + OrgID: "123", + Name: "my-bucket", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT(). + PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { + body := in.GetPostBucketRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "123", body.OrgID) && + assert.Equal(t, "my-bucket", body.Name) && + assert.Nil(t, body.Description) && + assert.Empty(t, body.RetentionRules) + })). + Return(api.Bucket{ + Id: api.PtrString("456"), + OrgID: api.PtrString("123"), + Name: "my-bucket", + RetentionRules: nil, + }, nil) + }, + expectedStdoutPattern: `456\s+my-bucket\s+infinite\s+n/a\s+123`, + }, + { + name: "fully specified", + params: bucket.BucketsCreateParams{ + OrgID: "123", + Name: "my-bucket", + Description: "my cool bucket", + Retention: "24h", + ShardGroupDuration: "1h", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT(). + PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { + body := in.GetPostBucketRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "123", body.OrgID) && + assert.Equal(t, "my-bucket", body.Name) && + assert.Equal(t, "my cool bucket", *body.Description) && + assert.Len(t, body.RetentionRules, 1) && + assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && + assert.Equal(t, int64(3600), *body.RetentionRules[0].ShardGroupDurationSeconds) + })). + Return(api.Bucket{ + Id: api.PtrString("456"), + OrgID: api.PtrString("123"), + Name: "my-bucket", + RetentionRules: []api.RetentionRule{ + {EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)}, + }, + }, nil) + }, + expectedStdoutPattern: `456\s+my-bucket\s+24h0m0s\s+1h0m0s\s+123`, + }, + { + name: "retention but not shard-group duration", + params: bucket.BucketsCreateParams{ + OrgID: "123", + Name: "my-bucket", + Retention: "24h", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT(). + PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { + body := in.GetPostBucketRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "123", body.OrgID) && + assert.Equal(t, "my-bucket", body.Name) && + assert.Nil(t, body.Description) && + assert.Len(t, body.RetentionRules, 1) && + assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && + assert.Nil(t, body.RetentionRules[0].ShardGroupDurationSeconds) + })). + Return(api.Bucket{ + Id: api.PtrString("456"), + OrgID: api.PtrString("123"), + Name: "my-bucket", + RetentionRules: []api.RetentionRule{{EverySeconds: 86400}}, + }, nil) + }, + }, + { + name: "create bucket with explicit schema", + params: bucket.BucketsCreateParams{ + OrgID: "123", + Name: "my-bucket", + SchemaType: api.SCHEMATYPE_EXPLICIT, + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT(). + PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { + body := in.GetPostBucketRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "123", body.OrgID) && + assert.Equal(t, "my-bucket", body.Name) && + assert.Nil(t, body.Description) && + assert.Empty(t, body.RetentionRules) && + assert.Equal(t, api.SCHEMATYPE_EXPLICIT, *body.SchemaType) + })). + Return(api.Bucket{ + Id: api.PtrString("456"), + OrgID: api.PtrString("123"), + Name: "my-bucket", + SchemaType: api.SCHEMATYPE_EXPLICIT.Ptr(), + }, nil) + }, + expectedStdoutPattern: `456\s+my-bucket\s+infinite\s+n/a\s+123\s+explicit`, + }, + { + name: "look up org by name", + params: bucket.BucketsCreateParams{ + OrgName: "my-org", + Name: "my-bucket", + Description: "my cool bucket", + Retention: "24h", + ShardGroupDuration: "1h", + }, + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })). + Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString("123")}}, + }, nil) + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT(). + PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { + body := in.GetPostBucketRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "123", body.OrgID) && + assert.Equal(t, "my-bucket", body.Name) && + assert.Equal(t, "my cool bucket", *body.Description) && + assert.Len(t, body.RetentionRules, 1) && + assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && + assert.Equal(t, int64(3600), *body.RetentionRules[0].ShardGroupDurationSeconds) + })). + Return(api.Bucket{ + Id: api.PtrString("456"), + OrgID: api.PtrString("123"), + Name: "my-bucket", + RetentionRules: []api.RetentionRule{ + {EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)}, + }, + }, nil) + }, + expectedStdoutPattern: `456\s+my-bucket\s+24h0m0s\s+1h0m0s\s+123`, + }, + { + name: "look up org by name from config", + params: bucket.BucketsCreateParams{ + Name: "my-bucket", + Description: "my cool bucket", + Retention: "24h", + ShardGroupDuration: "1h", + }, + configOrgName: "my-org", + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })). + Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString("123")}}, + }, nil) + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PostBuckets(gomock.Any()).Return(api.ApiPostBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT(). + PostBucketsExecute(tmock.MatchedBy(func(in api.ApiPostBucketsRequest) bool { + body := in.GetPostBucketRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "123", body.OrgID) && + assert.Equal(t, "my-bucket", body.Name) && + assert.Equal(t, "my cool bucket", *body.Description) && + assert.Len(t, body.RetentionRules, 1) && + assert.Equal(t, int64(86400), body.RetentionRules[0].EverySeconds) && + assert.Equal(t, int64(3600), *body.RetentionRules[0].ShardGroupDurationSeconds) + })). + Return(api.Bucket{ + Id: api.PtrString("456"), + OrgID: api.PtrString("123"), + Name: "my-bucket", + RetentionRules: []api.RetentionRule{ + {EverySeconds: 86400, ShardGroupDurationSeconds: api.PtrInt64(3600)}, + }, + }, nil) + }, + expectedStdoutPattern: `456\s+my-bucket\s+24h0m0s\s+1h0m0s\s+123`, + }, + { + name: "no org specified", + params: bucket.BucketsCreateParams{ + Name: "my-bucket", + Description: "my cool bucket", + Retention: "24h", + ShardGroupDuration: "1h", + }, + expectedInErr: "must specify org ID or org name", + }, + { + name: "no such org", + params: bucket.BucketsCreateParams{ + Name: "my-bucket", + OrgName: "fake-org", + Description: "my cool bucket", + Retention: "24h", + ShardGroupDuration: "1h", + }, + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(gomock.Any()).Return(api.Organizations{}, nil) + }, + expectedInErr: "no organization found", + }, + } + + 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() + + orgApi := mock.NewMockOrganizationsApi(ctrl) + if tc.registerOrgExpectations != nil { + tc.registerOrgExpectations(t, orgApi) + } + bucketApi := mock.NewMockBucketsApi(ctrl) + if tc.registerBucketExpectations != nil { + tc.registerBucketExpectations(t, bucketApi) + } + cli := bucket.Client{ + CLI: cmd.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio}, + OrganizationsApi: orgApi, + BucketsApi: bucketApi, + } + + err := cli.Create(context.Background(), &tc.params) + if tc.expectedInErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedInErr) + require.Empty(t, writtenBytes.String()) + return + } + require.NoError(t, err) + testutils.MatchLines(t, []string{ + `ID\s+Name\s+Retention\s+Shard group duration\s+Organization ID\s+Schema Type`, + tc.expectedStdoutPattern, + }, strings.Split(writtenBytes.String(), "\n")) + }) + } +} diff --git a/internal/cmd/bucket/delete.go b/internal/cmd/bucket/delete.go new file mode 100644 index 0000000..595a2cd --- /dev/null +++ b/internal/cmd/bucket/delete.go @@ -0,0 +1,63 @@ +package bucket + +import ( + "context" + "fmt" + + "github.com/influxdata/influx-cli/v2/internal/api" +) + +type BucketsDeleteParams struct { + ID string + Name string + OrgID string + OrgName string +} + +func (c Client) Delete(ctx context.Context, params *BucketsDeleteParams) error { + if params.ID == "" && params.Name == "" { + return ErrMustSpecifyBucket + } + + var bucket api.Bucket + var getReq api.ApiGetBucketsRequest + if params.ID != "" { + getReq = c.GetBuckets(ctx).Id(params.ID) + } else { + if params.OrgID == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { + return ErrMustSpecifyOrgDeleteByName + } + getReq = c.GetBuckets(ctx) + getReq = getReq.Name(params.Name) + if params.OrgID != "" { + getReq = getReq.OrgID(params.OrgID) + } + if params.OrgName != "" { + getReq = getReq.Org(params.OrgName) + } + if params.OrgID == "" && params.OrgName == "" { + getReq = getReq.Org(c.ActiveConfig.Org) + } + } + + displayId := params.ID + if displayId == "" { + displayId = params.Name + } + + resp, err := getReq.Execute() + if err != nil { + return fmt.Errorf("failed to find bucket %q: %w", displayId, err) + } + buckets := resp.GetBuckets() + if len(buckets) == 0 { + return fmt.Errorf("bucket %q not found", displayId) + } + bucket = buckets[0] + + if err := c.DeleteBucketsID(ctx, bucket.GetId()).Execute(); err != nil { + return fmt.Errorf("failed to delete bucket %q: %w", displayId, err) + } + + return c.printBuckets(bucketPrintOptions{bucket: &bucket, deleted: true}) +} diff --git a/internal/cmd/bucket/delete_test.go b/internal/cmd/bucket/delete_test.go new file mode 100644 index 0000000..375e3b2 --- /dev/null +++ b/internal/cmd/bucket/delete_test.go @@ -0,0 +1,222 @@ +package bucket_test + +import ( + "bytes" + "context" + "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/bucket" + "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/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestBucketsDelete(t *testing.T) { + t.Parallel() + + var testCases = []struct { + name string + configOrgName string + params bucket.BucketsDeleteParams + registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) + expectedStdoutPattern string + expectedInErr string + }{ + { + name: "by ID", + configOrgName: "my-default-org", + params: bucket.BucketsDeleteParams{ + ID: "123", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Equal(t, "123", *in.GetId()) && + assert.Nil(t, in.GetName()) && + assert.Nil(t, in.GetOrgID()) && + assert.Nil(t, in.GetOrg()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + }, + }, nil) + + bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { + return assert.Equal(t, "123", in.GetBucketID()) + })).Return(nil) + }, + expectedStdoutPattern: `123\s+my-bucket\s+1h0m0s\s+n/a\s+456\s+implicit`, + }, + { + name: "by name and org ID", + configOrgName: "my-default-org", + params: bucket.BucketsDeleteParams{ + Name: "my-bucket", + OrgID: "456", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Nil(t, in.GetId()) && + assert.Equal(t, "my-bucket", *in.GetName()) && + assert.Equal(t, "456", *in.GetOrgID()) && + assert.Nil(t, in.GetOrg()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + }, + }, nil) + + bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { + return assert.Equal(t, "123", in.GetBucketID()) + })).Return(nil) + }, + expectedStdoutPattern: `123\s+my-bucket\s+1h0m0s\s+n/a\s+456\s+implicit`, + }, + { + name: "by name and org name", + configOrgName: "my-default-org", + params: bucket.BucketsDeleteParams{ + Name: "my-bucket", + OrgName: "my-org", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Nil(t, in.GetId()) && + assert.Equal(t, "my-bucket", *in.GetName()) && + assert.Nil(t, in.GetOrgID()) && + assert.Equal(t, "my-org", *in.GetOrg()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + }, + }, nil) + + bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { + return assert.Equal(t, "123", in.GetBucketID()) + })).Return(nil) + }, + expectedStdoutPattern: `123\s+my-bucket\s+1h0m0s\s+n/a\s+456\s+implicit`, + }, + { + name: "by name in default org", + configOrgName: "my-default-org", + params: bucket.BucketsDeleteParams{ + Name: "my-bucket", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Nil(t, in.GetId()) && + assert.Equal(t, "my-bucket", *in.GetName()) && + assert.Nil(t, in.GetOrgID()) && + assert.Equal(t, "my-default-org", *in.GetOrg()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + }, + }, nil) + + bucketsApi.EXPECT().DeleteBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiDeleteBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().DeleteBucketsIDExecute(tmock.MatchedBy(func(in api.ApiDeleteBucketsIDRequest) bool { + return assert.Equal(t, "123", in.GetBucketID()) + })).Return(nil) + }, + expectedStdoutPattern: `123\s+my-bucket\s+1h0m0s\s+n/a\s+456\s+implicit`, + }, + { + name: "by name without org", + params: bucket.BucketsDeleteParams{ + Name: "my-bucket", + }, + expectedInErr: "must specify org ID or org name", + }, + { + name: "no such bucket", + params: bucket.BucketsDeleteParams{ + ID: "123", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(gomock.Any()).Return(api.Buckets{}, nil) + }, + expectedInErr: "not found", + }, + } + + 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() + client := mock.NewMockBucketsApi(ctrl) + if tc.registerBucketExpectations != nil { + tc.registerBucketExpectations(t, client) + } + cli := bucket.Client{ + CLI: cmd.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio}, + BucketsApi: client, + } + + err := cli.Delete(context.Background(), &tc.params) + if tc.expectedInErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedInErr) + require.Empty(t, writtenBytes.String()) + return + } + require.NoError(t, err) + testutils.MatchLines(t, []string{ + `ID\s+Name\s+Retention\s+Shard group duration\s+Organization ID\s+Schema Type\s+Deleted`, + tc.expectedStdoutPattern+`\s+true`, + }, strings.Split(writtenBytes.String(), "\n")) + }) + } +} diff --git a/internal/cmd/bucket/list.go b/internal/cmd/bucket/list.go new file mode 100644 index 0000000..4f47521 --- /dev/null +++ b/internal/cmd/bucket/list.go @@ -0,0 +1,47 @@ +package bucket + +import ( + "context" + "fmt" +) + +type BucketsListParams struct { + OrgID string + OrgName string + Name string + ID string +} + +func (c Client) List(ctx context.Context, params *BucketsListParams) error { + if params.OrgID == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { + return ErrMustSpecifyOrg + } + + req := c.GetBuckets(ctx) + if params.OrgID != "" { + req = req.OrgID(params.OrgID) + } + if params.OrgName != "" { + req = req.Org(params.OrgName) + } + if params.OrgID == "" && params.OrgName == "" { + req = req.Org(c.ActiveConfig.Org) + } + if params.Name != "" { + req = req.Name(params.Name) + } + if params.ID != "" { + req = req.Id(params.ID) + } + + buckets, err := req.Execute() + if err != nil { + return fmt.Errorf("failed to list buckets: %w", err) + } + + printOpts := bucketPrintOptions{} + if buckets.Buckets != nil { + printOpts.buckets = *buckets.Buckets + } + return c.printBuckets(printOpts) +} diff --git a/internal/cmd/bucket/list_test.go b/internal/cmd/bucket/list_test.go new file mode 100644 index 0000000..3f015c2 --- /dev/null +++ b/internal/cmd/bucket/list_test.go @@ -0,0 +1,235 @@ +package bucket_test + +import ( + "bytes" + "context" + "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/bucket" + "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/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestBucketsList(t *testing.T) { + t.Parallel() + + var testCases = []struct { + name string + configOrgName string + params bucket.BucketsListParams + registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) + expectedStdoutPatterns []string + expectedInErr string + }{ + { + name: "by ID", + params: bucket.BucketsListParams{ + ID: "123", + }, + configOrgName: "my-default-org", + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Equal(t, "123", *in.GetId()) && + assert.Equal(t, "my-default-org", *in.GetOrg()) && + assert.Nil(t, in.GetName()) && + assert.Nil(t, in.GetOrgID()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + }, + }, nil) + }, + expectedStdoutPatterns: []string{ + `123\s+my-bucket\s+1h0m0s\s+n/a\s+456`, + }, + }, + { + name: "by name", + params: bucket.BucketsListParams{ + Name: "my-bucket", + }, + configOrgName: "my-default-org", + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Equal(t, "my-bucket", *in.GetName()) && + assert.Equal(t, "my-default-org", *in.GetOrg()) && + assert.Nil(t, in.GetId()) && + assert.Nil(t, in.GetOrgID()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + }, + }, nil) + }, + expectedStdoutPatterns: []string{ + `123\s+my-bucket\s+1h0m0s\s+n/a\s+456`, + }, + }, + { + name: "override org by ID", + params: bucket.BucketsListParams{ + OrgID: "456", + }, + configOrgName: "my-default-org", + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Equal(t, "456", *in.GetOrgID()) && + assert.Nil(t, in.GetId()) && + assert.Nil(t, in.GetOrg()) && + assert.Nil(t, in.GetName()) + })).Return(api.Buckets{}, nil) + }, + }, + { + name: "override org by name", + params: bucket.BucketsListParams{ + OrgName: "my-org", + }, + configOrgName: "my-default-org", + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) && + assert.Nil(t, in.GetId()) && + assert.Nil(t, in.GetName()) && + assert.Nil(t, in.GetOrgID()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + { + Id: api.PtrString("999"), + Name: "bucket2", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 0, ShardGroupDurationSeconds: api.PtrInt64(60)}, + }, + }, + }, + }, nil) + }, + expectedStdoutPatterns: []string{ + `123\s+my-bucket\s+1h0m0s\s+n/a\s+456`, + `999\s+bucket2\s+infinite\s+1m0s\s+456`, + }, + }, + { + name: "list multiple bucket schema types", + params: bucket.BucketsListParams{ + OrgName: "my-org", + }, + configOrgName: "my-default-org", + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().GetBuckets(gomock.Any()).Return(api.ApiGetBucketsRequest{ApiService: bucketsApi}) + bucketsApi.EXPECT().GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) && + assert.Nil(t, in.GetId()) && + assert.Nil(t, in.GetName()) && + assert.Nil(t, in.GetOrgID()) + })).Return(api.Buckets{ + Buckets: &[]api.Bucket{ + { + Id: api.PtrString("001"), + Name: "omit-schema-type", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + }, + { + Id: api.PtrString("002"), + Name: "implicit-schema-type", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + SchemaType: api.SCHEMATYPE_IMPLICIT.Ptr(), + }, + { + Id: api.PtrString("003"), + Name: "explicit-schema-type", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: 3600}, + }, + SchemaType: api.SCHEMATYPE_EXPLICIT.Ptr(), + }, + }, + }, nil) + }, + expectedStdoutPatterns: []string{ + `001\s+omit-schema-type\s+1h0m0s\s+n/a\s+456\s+implicit`, + `002\s+implicit-schema-type\s+1h0m0s\s+n/a\s+456\s+implicit`, + `003\s+explicit-schema-type\s+1h0m0s\s+n/a\s+456\s+explicit`, + }, + }, + { + name: "no org specified", + expectedInErr: "must specify org ID or org name", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + client := mock.NewMockBucketsApi(ctrl) + if tc.registerBucketExpectations != nil { + tc.registerBucketExpectations(t, client) + } + stdio := mock.NewMockStdIO(ctrl) + bytesWritten := bytes.Buffer{} + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(bytesWritten.Write).AnyTimes() + cli := bucket.Client{ + CLI: cmd.CLI{ActiveConfig: config.Config{Org: tc.configOrgName}, StdIO: stdio}, + BucketsApi: client, + } + + err := cli.List(context.Background(), &tc.params) + if tc.expectedInErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedInErr) + require.Empty(t, bytesWritten.String()) + return + } + require.NoError(t, err) + testutils.MatchLines(t, append( + []string{`ID\s+Name\s+Retention\s+Shard group duration\s+Organization ID\s+Schema Type`}, + tc.expectedStdoutPatterns..., + ), strings.Split(bytesWritten.String(), "\n")) + }) + } +} diff --git a/internal/cmd/bucket/update.go b/internal/cmd/bucket/update.go new file mode 100644 index 0000000..15e7f48 --- /dev/null +++ b/internal/cmd/bucket/update.go @@ -0,0 +1,53 @@ +package bucket + +import ( + "context" + "fmt" + "time" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/duration" +) + +type BucketsUpdateParams struct { + ID string + Name string + Description string + Retention string + ShardGroupDuration string +} + +func (c Client) Update(ctx context.Context, params *BucketsUpdateParams) error { + reqBody := api.PatchBucketRequest{} + if params.Name != "" { + reqBody.SetName(params.Name) + } + if params.Description != "" { + reqBody.SetDescription(params.Description) + } + if params.Retention != "" || params.ShardGroupDuration != "" { + patchRule := api.NewPatchRetentionRuleWithDefaults() + if params.Retention != "" { + rp, err := duration.RawDurationToTimeDuration(params.Retention) + if err != nil { + return err + } + patchRule.SetEverySeconds(int64(rp.Round(time.Second) / time.Second)) + } + if params.ShardGroupDuration != "" { + sgd, err := duration.RawDurationToTimeDuration(params.ShardGroupDuration) + if err != nil { + return err + } + patchRule.SetShardGroupDurationSeconds(int64(sgd.Round(time.Second) / time.Second)) + } + reqBody.SetRetentionRules([]api.PatchRetentionRule{*patchRule}) + } + + bucket, err := c.PatchBucketsID(ctx, params.ID).PatchBucketRequest(reqBody).Execute() + if err != nil { + return fmt.Errorf("failed to update bucket %q: %w", params.ID, err) + } + + return c.printBuckets(bucketPrintOptions{bucket: &bucket}) +} diff --git a/internal/cmd/bucket/update_test.go b/internal/cmd/bucket/update_test.go new file mode 100644 index 0000000..69e64be --- /dev/null +++ b/internal/cmd/bucket/update_test.go @@ -0,0 +1,164 @@ +package bucket_test + +import ( + "bytes" + "context" + "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/bucket" + "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/influxdata/influx-cli/v2/internal/testutils" + "github.com/stretchr/testify/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestBucketsUpdate(t *testing.T) { + t.Parallel() + + var testCases = []struct { + name string + params bucket.BucketsUpdateParams + registerBucketExpectations func(*testing.T, *mock.MockBucketsApi) + expectedStdoutPattern string + }{ + { + name: "name", + params: bucket.BucketsUpdateParams{ + ID: "123", + Name: "cold-storage", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { + body := in.GetPatchBucketRequest() + return assert.Equal(t, "123", in.GetBucketID()) && + assert.NotNil(t, body) && + assert.Equal(t, "cold-storage", body.GetName()) && + assert.Nil(t, body.Description) && + assert.Empty(t, body.GetRetentionRules()) + })).Return(api.Bucket{ + Id: api.PtrString("123"), + Name: "cold-storage", + OrgID: api.PtrString("456"), + }, nil) + }, + expectedStdoutPattern: `123\s+cold-storage\s+infinite\s+n/a\s+456`, + }, + { + name: "description", + params: bucket.BucketsUpdateParams{ + ID: "123", + Description: "a very useful description", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { + body := in.GetPatchBucketRequest() + return assert.Equal(t, "123", in.GetBucketID()) && + assert.NotNil(t, body) && + assert.Equal(t, "a very useful description", body.GetDescription()) && + assert.Nil(t, body.Name) && + assert.Empty(t, body.GetRetentionRules()) + })).Return(api.Bucket{ + Id: api.PtrString("123"), + Name: "my-bucket", + Description: api.PtrString("a very useful description"), + OrgID: api.PtrString("456"), + }, nil) + }, + expectedStdoutPattern: `123\s+my-bucket\s+infinite\s+n/a\s+456`, + }, + { + name: "retention", + params: bucket.BucketsUpdateParams{ + ID: "123", + Retention: "3w", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { + body := in.GetPatchBucketRequest() + return assert.Equal(t, "123", in.GetBucketID()) && + assert.NotNil(t, body) && + assert.Nil(t, body.Name) && + assert.Nil(t, body.Description) && + assert.Len(t, body.GetRetentionRules(), 1) && + assert.Nil(t, body.GetRetentionRules()[0].ShardGroupDurationSeconds) && + assert.Equal(t, int64(3*7*24*3600), *body.GetRetentionRules()[0].EverySeconds) + })).Return(api.Bucket{ + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {EverySeconds: int64(3 * 7 * 24 * 3600)}, + }, + }, nil) + }, + expectedStdoutPattern: `123\s+my-bucket\s+504h0m0s\s+n/a\s+456`, + }, + { + name: "shard-group duration", + params: bucket.BucketsUpdateParams{ + ID: "123", + ShardGroupDuration: "10h30m", + }, + registerBucketExpectations: func(t *testing.T, bucketsApi *mock.MockBucketsApi) { + bucketsApi.EXPECT().PatchBucketsID(gomock.Any(), gomock.Eq("123")). + Return(api.ApiPatchBucketsIDRequest{ApiService: bucketsApi}.BucketID("123")) + bucketsApi.EXPECT().PatchBucketsIDExecute(tmock.MatchedBy(func(in api.ApiPatchBucketsIDRequest) bool { + body := in.GetPatchBucketRequest() + return assert.Equal(t, "123", in.GetBucketID()) && + assert.NotNil(t, body) && + assert.Nil(t, body.Name) && + assert.Nil(t, body.Description) && + assert.Len(t, body.GetRetentionRules(), 1) && + assert.Nil(t, body.GetRetentionRules()[0].EverySeconds) && + assert.Equal(t, int64(10*3600+30*60), *body.GetRetentionRules()[0].ShardGroupDurationSeconds) + })).Return(api.Bucket{ + Id: api.PtrString("123"), + Name: "my-bucket", + OrgID: api.PtrString("456"), + RetentionRules: []api.RetentionRule{ + {ShardGroupDurationSeconds: api.PtrInt64(10*3600 + 30*60)}, + }, + }, nil) + }, + expectedStdoutPattern: `123\s+my-bucket\s+infinite\s+10h30m0s\s+456`, + }, + } + + 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() + client := mock.NewMockBucketsApi(ctrl) + if tc.registerBucketExpectations != nil { + tc.registerBucketExpectations(t, client) + } + cli := bucket.Client{ + CLI: cmd.CLI{StdIO: stdio}, + BucketsApi: client, + } + + err := cli.Update(context.Background(), &tc.params) + require.NoError(t, err) + testutils.MatchLines(t, []string{ + `ID\s+Name\s+Retention\s+Shard group duration\s+Organization ID\s+Schema Type`, + tc.expectedStdoutPattern, + }, strings.Split(writtenBytes.String(), "\n")) + }) + } +} diff --git a/internal/cmd/bucket_schema/client.go b/internal/cmd/bucket_schema/client.go index f376a10..2633562 100644 --- a/internal/cmd/bucket_schema/client.go +++ b/internal/cmd/bucket_schema/client.go @@ -8,14 +8,14 @@ import ( "io" "os" - "github.com/influxdata/influx-cli/v2/internal" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" ) type Client struct { - BucketApi api.BucketsApi - BucketSchemasApi api.BucketSchemasApi - CLI *internal.CLI + api.BucketsApi + api.BucketSchemasApi + cmd.CLI } type orgBucketID struct { @@ -24,8 +24,7 @@ type orgBucketID struct { } func (c Client) resolveMeasurement(ctx context.Context, ids orgBucketID, name string) (string, error) { - res, err := c.BucketSchemasApi. - GetMeasurementSchemas(ctx, ids.BucketID). + res, err := c.GetMeasurementSchemas(ctx, ids.BucketID). OrgID(ids.OrgID). Name(name). Execute() @@ -40,7 +39,7 @@ func (c Client) resolveMeasurement(ctx context.Context, ids orgBucketID, name st return res.MeasurementSchemas[0].Id, nil } -func (c Client) resolveOrgBucketIds(ctx context.Context, params internal.OrgBucketParams) (*orgBucketID, error) { +func (c Client) resolveOrgBucketIds(ctx context.Context, params cmd.OrgBucketParams) (*orgBucketID, error) { if params.OrgID.Valid() && params.BucketID.Valid() { return &orgBucketID{OrgID: params.OrgID.String(), BucketID: params.BucketID.String()}, nil } @@ -49,17 +48,17 @@ func (c Client) resolveOrgBucketIds(ctx context.Context, params internal.OrgBuck return nil, errors.New("bucket missing: specify bucket ID or bucket name") } - if !params.OrgID.Valid() && params.OrgName == "" && c.CLI.ActiveConfig.Org == "" { + if !params.OrgID.Valid() && params.OrgName == "" && c.ActiveConfig.Org == "" { return nil, errors.New("org missing: specify org ID or org name") } - req := c.BucketApi.GetBuckets(ctx).Name(params.BucketName) + req := c.GetBuckets(ctx).Name(params.BucketName) if params.OrgID.Valid() { req = req.OrgID(params.OrgID.String()) } else if params.OrgName != "" { req = req.Org(params.OrgName) } else { - req = req.Org(c.CLI.ActiveConfig.Org) + req = req.Org(c.ActiveConfig.Org) } resp, err := req.Execute() @@ -100,117 +99,6 @@ func (c Client) readColumns(stdin io.Reader, f ColumnsFormat, path string) ([]ap return reader(r) } -type CreateParams struct { - internal.OrgBucketParams - Name string - Stdin io.Reader - ColumnsFile string - ColumnsFormat ColumnsFormat - ExtendedOutput bool -} - -func (c Client) Create(ctx context.Context, params CreateParams) error { - cols, err := c.readColumns(params.Stdin, params.ColumnsFormat, params.ColumnsFile) - if err != nil { - return err - } - - ids, err := c.resolveOrgBucketIds(ctx, params.OrgBucketParams) - if err != nil { - return err - } - - res, err := c.BucketSchemasApi. - CreateMeasurementSchema(ctx, ids.BucketID). - OrgID(ids.OrgID). - MeasurementSchemaCreateRequest(api.MeasurementSchemaCreateRequest{ - Name: params.Name, - Columns: cols, - }). - Execute() - - if err != nil { - return fmt.Errorf("failed to create measurement: %w", err) - } - - return c.printMeasurements(ids.BucketID, []api.MeasurementSchema{res}, params.ExtendedOutput) -} - -type UpdateParams struct { - internal.OrgBucketParams - Name string - ID string - Stdin io.Reader - ColumnsFile string - ColumnsFormat ColumnsFormat - ExtendedOutput bool -} - -func (c Client) Update(ctx context.Context, params UpdateParams) error { - cols, err := c.readColumns(params.Stdin, params.ColumnsFormat, params.ColumnsFile) - if err != nil { - return err - } - - ids, err := c.resolveOrgBucketIds(ctx, params.OrgBucketParams) - if err != nil { - return err - } - - var id string - if params.ID == "" && params.Name == "" { - return errors.New("measurement id or name required") - } else if params.ID != "" { - id = params.ID - } else { - id, err = c.resolveMeasurement(ctx, *ids, params.Name) - if err != nil { - return err - } - } - - res, err := c.BucketSchemasApi. - UpdateMeasurementSchema(ctx, ids.BucketID, id). - OrgID(ids.OrgID). - MeasurementSchemaUpdateRequest(api.MeasurementSchemaUpdateRequest{ - Columns: cols, - }). - Execute() - - if err != nil { - return fmt.Errorf("failed to update measurement schema: %w", err) - } - - return c.printMeasurements(ids.BucketID, []api.MeasurementSchema{res}, params.ExtendedOutput) -} - -type ListParams struct { - internal.OrgBucketParams - Name string - ExtendedOutput bool -} - -func (c Client) List(ctx context.Context, params ListParams) error { - ids, err := c.resolveOrgBucketIds(ctx, params.OrgBucketParams) - if err != nil { - return err - } - - req := c.BucketSchemasApi. - GetMeasurementSchemas(ctx, ids.BucketID). - OrgID(ids.OrgID) - - if params.Name != "" { - req = req.Name(params.Name) - } - - res, err := req.Execute() - if err != nil { - return fmt.Errorf("failed to list measurement schemas: %w", err) - } - return c.printMeasurements(ids.BucketID, res.MeasurementSchemas, params.ExtendedOutput) -} - // Constants for table column headers const ( IDHdr = "ID" @@ -226,8 +114,8 @@ func (c Client) printMeasurements(bucketID string, m []api.MeasurementSchema, ex return nil } - if c.CLI.PrintAsJSON { - return c.CLI.PrintJSON(m) + if c.PrintAsJSON { + return c.PrintJSON(m) } var headers []string @@ -261,7 +149,7 @@ func (c Client) printMeasurements(bucketID string, m []api.MeasurementSchema, ex rows = append(rows, makeRow(bucketID, &m[i])...) } - return c.CLI.PrintTable(headers, rows...) + return c.PrintTable(headers, rows...) } type measurementRowFn func(bucketID string, m *api.MeasurementSchema) []map[string]interface{} diff --git a/internal/cmd/bucket_schema/client_test.go b/internal/cmd/bucket_schema/client_test.go deleted file mode 100644 index 0a6ae9f..0000000 --- a/internal/cmd/bucket_schema/client_test.go +++ /dev/null @@ -1,753 +0,0 @@ -package bucket_schema_test - -import ( - "bytes" - "context" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/google/go-cmp/cmp" - "github.com/influxdata/influx-cli/v2/internal" - "github.com/influxdata/influx-cli/v2/internal/api" - "github.com/influxdata/influx-cli/v2/internal/cmd/bucket_schema" - "github.com/influxdata/influx-cli/v2/internal/mock" - "github.com/stretchr/testify/assert" - tmock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func matchLines(t *testing.T, expectedLines []string, lines []string) { - var nonEmptyLines []string - for _, l := range lines { - if l != "" { - nonEmptyLines = append(nonEmptyLines, l) - } - } - require.Equal(t, len(expectedLines), len(nonEmptyLines)) - for i, expected := range expectedLines { - require.Regexp(t, expected, nonEmptyLines[i]) - } -} - -func TestClient_Create(t *testing.T) { - t.Parallel() - - var ( - orgID = "dead" - bucketID = "f00d" - measurementID = "1010" - createdAt = time.Date(2004, 4, 9, 2, 15, 0, 0, time.UTC) - ) - - type setupArgs struct { - buckets *mock.MockBucketsApi - schemas *mock.MockBucketSchemasApi - cli *internal.CLI - params bucket_schema.CreateParams - cols []api.MeasurementSchemaColumn - stdio *mock.MockStdIO - } - - type optFn func(t *testing.T, a *setupArgs) - - type args struct { - OrgName string - BucketName string - Name string - ColumnsFile string - ExtendedOutput bool - } - - withArgs := func(args args) optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - var colFile string - if args.ColumnsFile != "" { - colFile = filepath.Join("testdata", args.ColumnsFile) - } - - a.params = bucket_schema.CreateParams{ - OrgBucketParams: internal.OrgBucketParams{ - OrgParams: internal.OrgParams{OrgName: args.OrgName}, - BucketParams: internal.BucketParams{BucketName: args.BucketName}, - }, - Name: args.Name, - ColumnsFile: colFile, - ExtendedOutput: args.ExtendedOutput, - } - } - } - - expGetBuckets := func(n ...string) optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - require.True(t, len(n) <= 1, "either zero or one bucket name") - var buckets []api.Bucket - if len(n) == 1 { - bucket := api.NewBucket(n[0], nil) - bucket.SetOrgID(orgID) - bucket.SetId(bucketID) - bucket.SetName(n[0]) - buckets = []api.Bucket{*bucket} - } - - req := api.ApiGetBucketsRequest{ApiService: a.buckets} - - a.buckets.EXPECT(). - GetBuckets(gomock.Any()). - Return(req) - - a.buckets.EXPECT(). - GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return cmp.Equal(in.GetOrg(), &a.params.OrgName) && cmp.Equal(in.GetName(), &a.params.BucketName) - })). - Return(api.Buckets{Buckets: &buckets}, nil) - } - } - - withCols := func(p string) optFn { - return func(t *testing.T, a *setupArgs) { - data, err := os.ReadFile(filepath.Join("testdata", p)) - require.NoError(t, err) - - var f bucket_schema.ColumnsFormat - decoder, err := f.DecoderFn(p) - require.NoError(t, err) - cols, err := decoder(bytes.NewReader(data)) - require.NoError(t, err) - a.cols = cols - } - } - - expCreate := func() optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - - req := api.ApiCreateMeasurementSchemaRequest{ApiService: a.schemas}.BucketID(bucketID) - - a.schemas.EXPECT(). - CreateMeasurementSchema(gomock.Any(), bucketID). - Return(req) - - a.schemas.EXPECT(). - CreateMeasurementSchemaExecute(tmock.MatchedBy(func(in api.ApiCreateMeasurementSchemaRequest) bool { - return cmp.Equal(in.GetOrgID(), &orgID) && cmp.Equal(in.GetBucketID(), bucketID) - })). - Return(api.MeasurementSchema{ - Id: measurementID, - Name: a.params.Name, - Columns: a.cols, - CreatedAt: createdAt, - UpdatedAt: createdAt, - }, nil) - } - } - - opts := func(opts ...optFn) []optFn { return opts } - - lines := func(lines ...string) []string { return lines } - - cases := []struct { - name string - opts []optFn - expErr string - expLines []string - }{ - { - name: "unable to guess from stdin", - expErr: `unable to guess format for file "stdin"`, - }, - { - name: "org arg missing", - opts: opts( - withArgs(args{BucketName: "my-bucket", ColumnsFile: "columns.csv"}), - ), - expErr: "org missing: specify org ID or org name", - }, - { - name: "bucket arg missing", - opts: opts( - withArgs(args{OrgName: "my-org", ColumnsFile: "columns.csv"}), - ), - expErr: "bucket missing: specify bucket ID or bucket name", - }, - { - name: "bucket not found", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", ColumnsFile: "columns.csv"}), - expGetBuckets(), - ), - expErr: `bucket "my-bucket" not found`, - }, - { - name: "create succeeds with csv", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv"}), - withCols("columns.csv"), - expGetBuckets("my-bucket"), - expCreate(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Bucket ID$`, - `^1010\s+cpu\s+f00d$`, - ), - }, - { - name: "create succeeds with json", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.json"}), - withCols("columns.csv"), - expGetBuckets("my-bucket"), - expCreate(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Bucket ID$`, - `^1010\s+cpu\s+f00d$`, - ), - }, - { - name: "create succeeds with ndjson", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.ndjson"}), - withCols("columns.csv"), - expGetBuckets("my-bucket"), - expCreate(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Bucket ID$`, - `^1010\s+cpu\s+f00d$`, - ), - }, - { - name: "create succeeds with extended output", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv", ExtendedOutput: true}), - withCols("columns.csv"), - expGetBuckets("my-bucket"), - expCreate(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Column Name\s+Column Type\s+Column Data Type\s+Bucket ID$`, - `^1010\s+cpu\s+time\s+timestamp\s+f00d$`, - `^1010\s+cpu\s+host\s+tag\s+f00d$`, - `^1010\s+cpu\s+usage_user\s+field\s+float\s+f00d$`, - ), - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - mockIO := mock.NewMockStdIO(ctrl) - writtenBytes := bytes.Buffer{} - mockIO.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes() - - args := &setupArgs{ - buckets: mock.NewMockBucketsApi(ctrl), - schemas: mock.NewMockBucketSchemasApi(ctrl), - stdio: mockIO, - cli: &internal.CLI{StdIO: mockIO}, - } - - for _, opt := range tc.opts { - opt(t, args) - } - - c := bucket_schema.Client{ - BucketApi: args.buckets, - BucketSchemasApi: args.schemas, - CLI: args.cli, - } - - ctx := context.Background() - err := c.Create(ctx, args.params) - if tc.expErr != "" { - assert.EqualError(t, err, tc.expErr) - } else { - require.NoError(t, err) - matchLines(t, tc.expLines, strings.Split(writtenBytes.String(), "\n")) - } - }) - } -} - -func TestClient_Update(t *testing.T) { - t.Parallel() - - var ( - orgID = "dead" - bucketID = "f00d" - measurementID = "1010" - createdAt = time.Date(2004, 4, 9, 2, 15, 0, 0, time.UTC) - updatedAt = time.Date(2009, 9, 1, 2, 15, 0, 0, time.UTC) - ) - - type setupArgs struct { - buckets *mock.MockBucketsApi - schemas *mock.MockBucketSchemasApi - cli *internal.CLI - params bucket_schema.UpdateParams - cols []api.MeasurementSchemaColumn - stdio *mock.MockStdIO - } - - type optFn func(t *testing.T, a *setupArgs) - - type args struct { - OrgName string - BucketName string - Name string - ColumnsFile string - ExtendedOutput bool - } - - withArgs := func(args args) optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - var colFile string - if args.ColumnsFile != "" { - colFile = filepath.Join("testdata", args.ColumnsFile) - } - - a.params = bucket_schema.UpdateParams{ - OrgBucketParams: internal.OrgBucketParams{ - OrgParams: internal.OrgParams{OrgName: args.OrgName}, - BucketParams: internal.BucketParams{BucketName: args.BucketName}, - }, - Name: args.Name, - ColumnsFile: colFile, - ExtendedOutput: args.ExtendedOutput, - } - } - } - - expGetBuckets := func(n ...string) optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - require.True(t, len(n) <= 1, "either zero or one bucket name") - var buckets []api.Bucket - if len(n) == 1 { - bucket := api.NewBucket(n[0], nil) - bucket.SetOrgID(orgID) - bucket.SetId(bucketID) - bucket.SetName(n[0]) - buckets = []api.Bucket{*bucket} - } - - req := api.ApiGetBucketsRequest{ApiService: a.buckets} - - a.buckets.EXPECT(). - GetBuckets(gomock.Any()). - Return(req) - - a.buckets.EXPECT(). - GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return (in.GetOrg() != nil && *in.GetOrg() == a.params.OrgName) && - (in.GetName() != nil && *in.GetName() == a.params.BucketName) - })). - Return(api.Buckets{Buckets: &buckets}, nil) - } - } - - withCols := func(p string) optFn { - return func(t *testing.T, a *setupArgs) { - data, err := os.ReadFile(filepath.Join("testdata", p)) - require.NoError(t, err) - - var f bucket_schema.ColumnsFormat - decoder, err := f.DecoderFn(p) - require.NoError(t, err) - cols, err := decoder(bytes.NewReader(data)) - require.NoError(t, err) - a.cols = cols - } - } - - expGetMeasurementSchema := func() optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - - req := api.ApiGetMeasurementSchemasRequest{ApiService: a.schemas}.BucketID(bucketID) - - a.schemas.EXPECT(). - GetMeasurementSchemas(gomock.Any(), bucketID). - Return(req) - - a.schemas.EXPECT(). - GetMeasurementSchemasExecute(tmock.MatchedBy(func(in api.ApiGetMeasurementSchemasRequest) bool { - return (in.GetOrgID() != nil && *in.GetOrgID() == orgID) && - in.GetBucketID() == bucketID && - (in.GetName() != nil && *in.GetName() == a.params.Name) - })). - Return(api.MeasurementSchemaList{ - MeasurementSchemas: []api.MeasurementSchema{ - { - Id: measurementID, - Name: a.params.Name, - Columns: a.cols, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }, - }, - }, nil) - } - } - - expUpdate := func() optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - - req := api.ApiUpdateMeasurementSchemaRequest{ApiService: a.schemas}.BucketID(bucketID).MeasurementID(measurementID) - - a.schemas.EXPECT(). - UpdateMeasurementSchema(gomock.Any(), bucketID, measurementID). - Return(req) - - a.schemas.EXPECT(). - UpdateMeasurementSchemaExecute(tmock.MatchedBy(func(in api.ApiUpdateMeasurementSchemaRequest) bool { - return cmp.Equal(in.GetOrgID(), &orgID) && - cmp.Equal(in.GetBucketID(), bucketID) && - cmp.Equal(in.GetMeasurementID(), measurementID) && - cmp.Equal(in.GetMeasurementSchemaUpdateRequest().Columns, a.cols) - })). - Return(api.MeasurementSchema{ - Id: measurementID, - Name: a.params.Name, - Columns: a.cols, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }, nil) - } - } - - opts := func(opts ...optFn) []optFn { return opts } - - lines := func(lines ...string) []string { return lines } - - cases := []struct { - name string - opts []optFn - expErr string - expLines []string - }{ - { - name: "unable to guess from stdin", - expErr: `unable to guess format for file "stdin"`, - }, - { - name: "org arg missing", - opts: opts( - withArgs(args{BucketName: "my-bucket", ColumnsFile: "columns.csv"}), - ), - expErr: "org missing: specify org ID or org name", - }, - { - name: "bucket arg missing", - opts: opts( - withArgs(args{OrgName: "my-org", ColumnsFile: "columns.csv"}), - ), - expErr: "bucket missing: specify bucket ID or bucket name", - }, - { - name: "bucket not found", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", ColumnsFile: "columns.csv"}), - expGetBuckets(), - ), - expErr: `bucket "my-bucket" not found`, - }, - { - name: "update succeeds", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv"}), - withCols("columns.csv"), - - expGetBuckets("my-bucket"), - expGetMeasurementSchema(), - expUpdate(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Bucket ID$`, - `^1010\s+cpu\s+f00d$`, - ), - }, - { - name: "update succeeds extended output", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv", ExtendedOutput: true}), - withCols("columns.csv"), - expGetBuckets("my-bucket"), - expGetMeasurementSchema(), - expUpdate(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Column Name\s+Column Type\s+Column Data Type\s+Bucket ID$`, - `^1010\s+cpu\s+time\s+timestamp\s+f00d$`, - `^1010\s+cpu\s+host\s+tag\s+f00d$`, - `^1010\s+cpu\s+usage_user\s+field\s+float\s+f00d$`, - - ), - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - mockIO := mock.NewMockStdIO(ctrl) - writtenBytes := bytes.Buffer{} - mockIO.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes() - - args := &setupArgs{ - buckets: mock.NewMockBucketsApi(ctrl), - schemas: mock.NewMockBucketSchemasApi(ctrl), - stdio: mockIO, - cli: &internal.CLI{StdIO: mockIO}, - } - - for _, opt := range tc.opts { - opt(t, args) - } - - c := bucket_schema.Client{ - BucketApi: args.buckets, - BucketSchemasApi: args.schemas, - CLI: args.cli, - } - - ctx := context.Background() - err := c.Update(ctx, args.params) - if tc.expErr != "" { - assert.EqualError(t, err, tc.expErr) - } else { - require.NoError(t, err) - matchLines(t, tc.expLines, strings.Split(writtenBytes.String(), "\n")) - } - }) - } -} - -func TestClient_List(t *testing.T) { - t.Parallel() - - var ( - orgID = "dead" - bucketID = "f00d" - measurementID = "1010" - createdAt = time.Date(2004, 4, 9, 2, 15, 0, 0, time.UTC) - updatedAt = time.Date(2009, 9, 1, 2, 15, 0, 0, time.UTC) - ) - - type setupArgs struct { - buckets *mock.MockBucketsApi - schemas *mock.MockBucketSchemasApi - cli *internal.CLI - params bucket_schema.ListParams - cols []api.MeasurementSchemaColumn - stdio *mock.MockStdIO - } - - type optFn func(t *testing.T, a *setupArgs) - - type args struct { - OrgName string - BucketName string - Name string - ExtendedOutput bool - } - - withArgs := func(args args) optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - a.params = bucket_schema.ListParams{ - OrgBucketParams: internal.OrgBucketParams{ - OrgParams: internal.OrgParams{OrgName: args.OrgName}, - BucketParams: internal.BucketParams{BucketName: args.BucketName}, - }, - Name: args.Name, - ExtendedOutput: args.ExtendedOutput, - } - } - } - - expGetBuckets := func(n ...string) optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - require.True(t, len(n) <= 1, "either zero or one bucket name") - var buckets []api.Bucket - if len(n) == 1 { - bucket := api.NewBucket(n[0], nil) - bucket.SetOrgID(orgID) - bucket.SetId(bucketID) - bucket.SetName(n[0]) - buckets = []api.Bucket{*bucket} - } - - req := api.ApiGetBucketsRequest{ApiService: a.buckets} - - a.buckets.EXPECT(). - GetBuckets(gomock.Any()). - Return(req) - - a.buckets.EXPECT(). - GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { - return (in.GetOrg() != nil && *in.GetOrg() == a.params.OrgName) && - (in.GetName() != nil && *in.GetName() == a.params.BucketName) - })). - Return(api.Buckets{Buckets: &buckets}, nil) - } - } - - withCols := func(p string) optFn { - return func(t *testing.T, a *setupArgs) { - data, err := os.ReadFile(filepath.Join("testdata", p)) - require.NoError(t, err) - - var f bucket_schema.ColumnsFormat - decoder, err := f.DecoderFn(p) - require.NoError(t, err) - cols, err := decoder(bytes.NewReader(data)) - require.NoError(t, err) - a.cols = cols - } - } - - expGetMeasurementSchemas := func() optFn { - return func(t *testing.T, a *setupArgs) { - t.Helper() - - req := api.ApiGetMeasurementSchemasRequest{ApiService: a.schemas}.BucketID(bucketID) - - a.schemas.EXPECT(). - GetMeasurementSchemas(gomock.Any(), bucketID). - Return(req) - - a.schemas.EXPECT(). - GetMeasurementSchemasExecute(tmock.MatchedBy(func(in api.ApiGetMeasurementSchemasRequest) bool { - return (in.GetOrgID() != nil && *in.GetOrgID() == orgID) && - in.GetBucketID() == bucketID && - (in.GetName() != nil && *in.GetName() == a.params.Name) - })). - Return(api.MeasurementSchemaList{ - MeasurementSchemas: []api.MeasurementSchema{ - { - Id: measurementID, - Name: a.params.Name, - Columns: a.cols, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }, - }, - }, nil) - } - } - - opts := func(opts ...optFn) []optFn { return opts } - - lines := func(lines ...string) []string { return lines } - - cases := []struct { - name string - opts []optFn - expErr string - expLines []string - }{ - { - name: "org arg missing", - opts: opts( - withArgs(args{BucketName: "my-bucket"}), - ), - expErr: "org missing: specify org ID or org name", - }, - { - name: "bucket arg missing", - opts: opts( - withArgs(args{OrgName: "my-org"}), - ), - expErr: "bucket missing: specify bucket ID or bucket name", - }, - { - name: "bucket not found", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket"}), - expGetBuckets(), - ), - expErr: `bucket "my-bucket" not found`, - }, - { - name: "list succeeds", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu"}), - withCols("columns.csv"), - expGetBuckets("my-bucket"), - expGetMeasurementSchemas(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Bucket ID$`, - `^1010\s+cpu\s+f00d$`, - ), - }, - { - name: "list succeeds extended output", - opts: opts( - withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ExtendedOutput: true}), - withCols("columns.csv"), - expGetBuckets("my-bucket"), - expGetMeasurementSchemas(), - ), - expLines: lines( - `^ID\s+Measurement Name\s+Column Name\s+Column Type\s+Column Data Type\s+Bucket ID$`, - `^1010\s+cpu\s+time\s+timestamp\s+f00d$`, - `^1010\s+cpu\s+host\s+tag\s+f00d$`, - `^1010\s+cpu\s+usage_user\s+field\s+float\s+f00d$`, - ), - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - mockIO := mock.NewMockStdIO(ctrl) - writtenBytes := bytes.Buffer{} - mockIO.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes() - - args := &setupArgs{ - buckets: mock.NewMockBucketsApi(ctrl), - schemas: mock.NewMockBucketSchemasApi(ctrl), - stdio: mockIO, - cli: &internal.CLI{StdIO: mockIO}, - } - - for _, opt := range tc.opts { - opt(t, args) - } - - c := bucket_schema.Client{ - BucketApi: args.buckets, - BucketSchemasApi: args.schemas, - CLI: args.cli, - } - - ctx := context.Background() - err := c.List(ctx, args.params) - if tc.expErr != "" { - assert.EqualError(t, err, tc.expErr) - } else { - require.NoError(t, err) - matchLines(t, tc.expLines, strings.Split(writtenBytes.String(), "\n")) - } - }) - } -} diff --git a/internal/cmd/bucket_schema/create.go b/internal/cmd/bucket_schema/create.go new file mode 100644 index 0000000..79791bd --- /dev/null +++ b/internal/cmd/bucket_schema/create.go @@ -0,0 +1,45 @@ +package bucket_schema + +import ( + "context" + "fmt" + "io" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" +) + +type CreateParams struct { + cmd.OrgBucketParams + Name string + Stdin io.Reader + ColumnsFile string + ColumnsFormat ColumnsFormat + ExtendedOutput bool +} + +func (c Client) Create(ctx context.Context, params CreateParams) error { + cols, err := c.readColumns(params.Stdin, params.ColumnsFormat, params.ColumnsFile) + if err != nil { + return err + } + + ids, err := c.resolveOrgBucketIds(ctx, params.OrgBucketParams) + if err != nil { + return err + } + + res, err := c.CreateMeasurementSchema(ctx, ids.BucketID). + OrgID(ids.OrgID). + MeasurementSchemaCreateRequest(api.MeasurementSchemaCreateRequest{ + Name: params.Name, + Columns: cols, + }). + Execute() + + if err != nil { + return fmt.Errorf("failed to create measurement: %w", err) + } + + return c.printMeasurements(ids.BucketID, []api.MeasurementSchema{res}, params.ExtendedOutput) +} diff --git a/internal/cmd/bucket_schema/create_test.go b/internal/cmd/bucket_schema/create_test.go new file mode 100644 index 0000000..51ba391 --- /dev/null +++ b/internal/cmd/bucket_schema/create_test.go @@ -0,0 +1,266 @@ +package bucket_schema_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/bucket_schema" + "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/influxdata/influx-cli/v2/internal/testutils" + "github.com/stretchr/testify/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestClient_Create(t *testing.T) { + t.Parallel() + + var ( + orgID = "dead" + bucketID = "f00d" + measurementID = "1010" + createdAt = time.Date(2004, 4, 9, 2, 15, 0, 0, time.UTC) + ) + + type setupArgs struct { + buckets *mock.MockBucketsApi + schemas *mock.MockBucketSchemasApi + cli cmd.CLI + params bucket_schema.CreateParams + cols []api.MeasurementSchemaColumn + stdio *mock.MockStdIO + } + + type optFn func(t *testing.T, a *setupArgs) + + type args struct { + OrgName string + BucketName string + Name string + ColumnsFile string + ExtendedOutput bool + } + + withArgs := func(args args) optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + var colFile string + if args.ColumnsFile != "" { + colFile = filepath.Join("testdata", args.ColumnsFile) + } + + a.params = bucket_schema.CreateParams{ + OrgBucketParams: cmd.OrgBucketParams{ + OrgParams: cmd.OrgParams{OrgName: args.OrgName}, + BucketParams: cmd.BucketParams{BucketName: args.BucketName}, + }, + Name: args.Name, + ColumnsFile: colFile, + ExtendedOutput: args.ExtendedOutput, + } + } + } + + expGetBuckets := func(n ...string) optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + require.True(t, len(n) <= 1, "either zero or one bucket name") + var buckets []api.Bucket + if len(n) == 1 { + bucket := api.NewBucket(n[0], nil) + bucket.SetOrgID(orgID) + bucket.SetId(bucketID) + bucket.SetName(n[0]) + buckets = []api.Bucket{*bucket} + } + + req := api.ApiGetBucketsRequest{ApiService: a.buckets} + + a.buckets.EXPECT(). + GetBuckets(gomock.Any()). + Return(req) + + a.buckets.EXPECT(). + GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return cmp.Equal(in.GetOrg(), &a.params.OrgName) && cmp.Equal(in.GetName(), &a.params.BucketName) + })). + Return(api.Buckets{Buckets: &buckets}, nil) + } + } + + withCols := func(p string) optFn { + return func(t *testing.T, a *setupArgs) { + data, err := os.ReadFile(filepath.Join("testdata", p)) + require.NoError(t, err) + + var f bucket_schema.ColumnsFormat + decoder, err := f.DecoderFn(p) + require.NoError(t, err) + cols, err := decoder(bytes.NewReader(data)) + require.NoError(t, err) + a.cols = cols + } + } + + expCreate := func() optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + + req := api.ApiCreateMeasurementSchemaRequest{ApiService: a.schemas}.BucketID(bucketID) + + a.schemas.EXPECT(). + CreateMeasurementSchema(gomock.Any(), bucketID). + Return(req) + + a.schemas.EXPECT(). + CreateMeasurementSchemaExecute(tmock.MatchedBy(func(in api.ApiCreateMeasurementSchemaRequest) bool { + return cmp.Equal(in.GetOrgID(), &orgID) && cmp.Equal(in.GetBucketID(), bucketID) + })). + Return(api.MeasurementSchema{ + Id: measurementID, + Name: a.params.Name, + Columns: a.cols, + CreatedAt: createdAt, + UpdatedAt: createdAt, + }, nil) + } + } + + opts := func(opts ...optFn) []optFn { return opts } + + lines := func(lines ...string) []string { return lines } + + cases := []struct { + name string + opts []optFn + expErr string + expLines []string + }{ + { + name: "unable to guess from stdin", + expErr: `unable to guess format for file "stdin"`, + }, + { + name: "org arg missing", + opts: opts( + withArgs(args{BucketName: "my-bucket", ColumnsFile: "columns.csv"}), + ), + expErr: "org missing: specify org ID or org name", + }, + { + name: "bucket arg missing", + opts: opts( + withArgs(args{OrgName: "my-org", ColumnsFile: "columns.csv"}), + ), + expErr: "bucket missing: specify bucket ID or bucket name", + }, + { + name: "bucket not found", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", ColumnsFile: "columns.csv"}), + expGetBuckets(), + ), + expErr: `bucket "my-bucket" not found`, + }, + { + name: "create succeeds with csv", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv"}), + withCols("columns.csv"), + expGetBuckets("my-bucket"), + expCreate(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Bucket ID$`, + `^1010\s+cpu\s+f00d$`, + ), + }, + { + name: "create succeeds with json", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.json"}), + withCols("columns.csv"), + expGetBuckets("my-bucket"), + expCreate(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Bucket ID$`, + `^1010\s+cpu\s+f00d$`, + ), + }, + { + name: "create succeeds with ndjson", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.ndjson"}), + withCols("columns.csv"), + expGetBuckets("my-bucket"), + expCreate(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Bucket ID$`, + `^1010\s+cpu\s+f00d$`, + ), + }, + { + name: "create succeeds with extended output", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv", ExtendedOutput: true}), + withCols("columns.csv"), + expGetBuckets("my-bucket"), + expCreate(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Column Name\s+Column Type\s+Column Data Type\s+Bucket ID$`, + `^1010\s+cpu\s+time\s+timestamp\s+f00d$`, + `^1010\s+cpu\s+host\s+tag\s+f00d$`, + `^1010\s+cpu\s+usage_user\s+field\s+float\s+f00d$`, + ), + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockIO := mock.NewMockStdIO(ctrl) + writtenBytes := bytes.Buffer{} + mockIO.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes() + + args := &setupArgs{ + buckets: mock.NewMockBucketsApi(ctrl), + schemas: mock.NewMockBucketSchemasApi(ctrl), + stdio: mockIO, + cli: cmd.CLI{StdIO: mockIO}, + } + + for _, opt := range tc.opts { + opt(t, args) + } + + c := bucket_schema.Client{ + BucketsApi: args.buckets, + BucketSchemasApi: args.schemas, + CLI: args.cli, + } + + err := c.Create(context.Background(), args.params) + if tc.expErr != "" { + assert.EqualError(t, err, tc.expErr) + } else { + require.NoError(t, err) + testutils.MatchLines(t, tc.expLines, strings.Split(writtenBytes.String(), "\n")) + } + }) + } +} diff --git a/internal/cmd/bucket_schema/list.go b/internal/cmd/bucket_schema/list.go new file mode 100644 index 0000000..862e6db --- /dev/null +++ b/internal/cmd/bucket_schema/list.go @@ -0,0 +1,33 @@ +package bucket_schema + +import ( + "context" + "fmt" + + "github.com/influxdata/influx-cli/v2/internal/cmd" +) + +type ListParams struct { + cmd.OrgBucketParams + Name string + ExtendedOutput bool +} + +func (c Client) List(ctx context.Context, params ListParams) error { + ids, err := c.resolveOrgBucketIds(ctx, params.OrgBucketParams) + if err != nil { + return err + } + + req := c.GetMeasurementSchemas(ctx, ids.BucketID).OrgID(ids.OrgID) + + if params.Name != "" { + req = req.Name(params.Name) + } + + res, err := req.Execute() + if err != nil { + return fmt.Errorf("failed to list measurement schemas: %w", err) + } + return c.printMeasurements(ids.BucketID, res.MeasurementSchemas, params.ExtendedOutput) +} diff --git a/internal/cmd/bucket_schema/list_test.go b/internal/cmd/bucket_schema/list_test.go new file mode 100644 index 0000000..795ffdf --- /dev/null +++ b/internal/cmd/bucket_schema/list_test.go @@ -0,0 +1,236 @@ +package bucket_schema_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "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/bucket_schema" + "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/influxdata/influx-cli/v2/internal/testutils" + "github.com/stretchr/testify/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestClient_List(t *testing.T) { + t.Parallel() + + var ( + orgID = "dead" + bucketID = "f00d" + measurementID = "1010" + createdAt = time.Date(2004, 4, 9, 2, 15, 0, 0, time.UTC) + updatedAt = time.Date(2009, 9, 1, 2, 15, 0, 0, time.UTC) + ) + + type setupArgs struct { + buckets *mock.MockBucketsApi + schemas *mock.MockBucketSchemasApi + cli cmd.CLI + params bucket_schema.ListParams + cols []api.MeasurementSchemaColumn + stdio *mock.MockStdIO + } + + type optFn func(t *testing.T, a *setupArgs) + + type args struct { + OrgName string + BucketName string + Name string + ExtendedOutput bool + } + + withArgs := func(args args) optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + a.params = bucket_schema.ListParams{ + OrgBucketParams: cmd.OrgBucketParams{ + OrgParams: cmd.OrgParams{OrgName: args.OrgName}, + BucketParams: cmd.BucketParams{BucketName: args.BucketName}, + }, + Name: args.Name, + ExtendedOutput: args.ExtendedOutput, + } + } + } + + expGetBuckets := func(n ...string) optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + require.True(t, len(n) <= 1, "either zero or one bucket name") + var buckets []api.Bucket + if len(n) == 1 { + bucket := api.NewBucket(n[0], nil) + bucket.SetOrgID(orgID) + bucket.SetId(bucketID) + bucket.SetName(n[0]) + buckets = []api.Bucket{*bucket} + } + + req := api.ApiGetBucketsRequest{ApiService: a.buckets} + + a.buckets.EXPECT(). + GetBuckets(gomock.Any()). + Return(req) + + a.buckets.EXPECT(). + GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return (in.GetOrg() != nil && *in.GetOrg() == a.params.OrgName) && + (in.GetName() != nil && *in.GetName() == a.params.BucketName) + })). + Return(api.Buckets{Buckets: &buckets}, nil) + } + } + + withCols := func(p string) optFn { + return func(t *testing.T, a *setupArgs) { + data, err := os.ReadFile(filepath.Join("testdata", p)) + require.NoError(t, err) + + var f bucket_schema.ColumnsFormat + decoder, err := f.DecoderFn(p) + require.NoError(t, err) + cols, err := decoder(bytes.NewReader(data)) + require.NoError(t, err) + a.cols = cols + } + } + + expGetMeasurementSchemas := func() optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + + req := api.ApiGetMeasurementSchemasRequest{ApiService: a.schemas}.BucketID(bucketID) + + a.schemas.EXPECT(). + GetMeasurementSchemas(gomock.Any(), bucketID). + Return(req) + + a.schemas.EXPECT(). + GetMeasurementSchemasExecute(tmock.MatchedBy(func(in api.ApiGetMeasurementSchemasRequest) bool { + return (in.GetOrgID() != nil && *in.GetOrgID() == orgID) && + in.GetBucketID() == bucketID && + (in.GetName() != nil && *in.GetName() == a.params.Name) + })). + Return(api.MeasurementSchemaList{ + MeasurementSchemas: []api.MeasurementSchema{ + { + Id: measurementID, + Name: a.params.Name, + Columns: a.cols, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + }, + }, nil) + } + } + + opts := func(opts ...optFn) []optFn { return opts } + + lines := func(lines ...string) []string { return lines } + + cases := []struct { + name string + opts []optFn + expErr string + expLines []string + }{ + { + name: "org arg missing", + opts: opts( + withArgs(args{BucketName: "my-bucket"}), + ), + expErr: "org missing: specify org ID or org name", + }, + { + name: "bucket arg missing", + opts: opts( + withArgs(args{OrgName: "my-org"}), + ), + expErr: "bucket missing: specify bucket ID or bucket name", + }, + { + name: "bucket not found", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket"}), + expGetBuckets(), + ), + expErr: `bucket "my-bucket" not found`, + }, + { + name: "list succeeds", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu"}), + withCols("columns.csv"), + expGetBuckets("my-bucket"), + expGetMeasurementSchemas(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Bucket ID$`, + `^1010\s+cpu\s+f00d$`, + ), + }, + { + name: "list succeeds extended output", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ExtendedOutput: true}), + withCols("columns.csv"), + expGetBuckets("my-bucket"), + expGetMeasurementSchemas(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Column Name\s+Column Type\s+Column Data Type\s+Bucket ID$`, + `^1010\s+cpu\s+time\s+timestamp\s+f00d$`, + `^1010\s+cpu\s+host\s+tag\s+f00d$`, + `^1010\s+cpu\s+usage_user\s+field\s+float\s+f00d$`, + ), + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockIO := mock.NewMockStdIO(ctrl) + writtenBytes := bytes.Buffer{} + mockIO.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes() + + args := &setupArgs{ + buckets: mock.NewMockBucketsApi(ctrl), + schemas: mock.NewMockBucketSchemasApi(ctrl), + stdio: mockIO, + cli: cmd.CLI{StdIO: mockIO}, + } + + for _, opt := range tc.opts { + opt(t, args) + } + + c := bucket_schema.Client{ + BucketsApi: args.buckets, + BucketSchemasApi: args.schemas, + CLI: args.cli, + } + + err := c.List(context.Background(), args.params) + if tc.expErr != "" { + assert.EqualError(t, err, tc.expErr) + } else { + require.NoError(t, err) + testutils.MatchLines(t, tc.expLines, strings.Split(writtenBytes.String(), "\n")) + } + }) + } +} diff --git a/internal/cmd/bucket_schema/update.go b/internal/cmd/bucket_schema/update.go new file mode 100644 index 0000000..d8e6578 --- /dev/null +++ b/internal/cmd/bucket_schema/update.go @@ -0,0 +1,58 @@ +package bucket_schema + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" +) + +type UpdateParams struct { + cmd.OrgBucketParams + Name string + ID string + Stdin io.Reader + ColumnsFile string + ColumnsFormat ColumnsFormat + ExtendedOutput bool +} + +func (c Client) Update(ctx context.Context, params UpdateParams) error { + cols, err := c.readColumns(params.Stdin, params.ColumnsFormat, params.ColumnsFile) + if err != nil { + return err + } + + ids, err := c.resolveOrgBucketIds(ctx, params.OrgBucketParams) + if err != nil { + return err + } + + var id string + if params.ID == "" && params.Name == "" { + return errors.New("measurement id or name required") + } else if params.ID != "" { + id = params.ID + } else { + id, err = c.resolveMeasurement(ctx, *ids, params.Name) + if err != nil { + return err + } + } + + res, err := c.UpdateMeasurementSchema(ctx, ids.BucketID, id). + OrgID(ids.OrgID). + MeasurementSchemaUpdateRequest(api.MeasurementSchemaUpdateRequest{ + Columns: cols, + }). + Execute() + + if err != nil { + return fmt.Errorf("failed to update measurement schema: %w", err) + } + + return c.printMeasurements(ids.BucketID, []api.MeasurementSchema{res}, params.ExtendedOutput) +} diff --git a/internal/cmd/bucket_schema/update_test.go b/internal/cmd/bucket_schema/update_test.go new file mode 100644 index 0000000..e5b065a --- /dev/null +++ b/internal/cmd/bucket_schema/update_test.go @@ -0,0 +1,279 @@ +package bucket_schema_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/bucket_schema" + "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/influxdata/influx-cli/v2/internal/testutils" + "github.com/stretchr/testify/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestClient_Update(t *testing.T) { + t.Parallel() + + var ( + orgID = "dead" + bucketID = "f00d" + measurementID = "1010" + createdAt = time.Date(2004, 4, 9, 2, 15, 0, 0, time.UTC) + updatedAt = time.Date(2009, 9, 1, 2, 15, 0, 0, time.UTC) + ) + + type setupArgs struct { + buckets *mock.MockBucketsApi + schemas *mock.MockBucketSchemasApi + cli cmd.CLI + params bucket_schema.UpdateParams + cols []api.MeasurementSchemaColumn + stdio *mock.MockStdIO + } + + type optFn func(t *testing.T, a *setupArgs) + + type args struct { + OrgName string + BucketName string + Name string + ColumnsFile string + ExtendedOutput bool + } + + withArgs := func(args args) optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + var colFile string + if args.ColumnsFile != "" { + colFile = filepath.Join("testdata", args.ColumnsFile) + } + + a.params = bucket_schema.UpdateParams{ + OrgBucketParams: cmd.OrgBucketParams{ + OrgParams: cmd.OrgParams{OrgName: args.OrgName}, + BucketParams: cmd.BucketParams{BucketName: args.BucketName}, + }, + Name: args.Name, + ColumnsFile: colFile, + ExtendedOutput: args.ExtendedOutput, + } + } + } + + expGetBuckets := func(n ...string) optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + require.True(t, len(n) <= 1, "either zero or one bucket name") + var buckets []api.Bucket + if len(n) == 1 { + bucket := api.NewBucket(n[0], nil) + bucket.SetOrgID(orgID) + bucket.SetId(bucketID) + bucket.SetName(n[0]) + buckets = []api.Bucket{*bucket} + } + + req := api.ApiGetBucketsRequest{ApiService: a.buckets} + + a.buckets.EXPECT(). + GetBuckets(gomock.Any()). + Return(req) + + a.buckets.EXPECT(). + GetBucketsExecute(tmock.MatchedBy(func(in api.ApiGetBucketsRequest) bool { + return (in.GetOrg() != nil && *in.GetOrg() == a.params.OrgName) && + (in.GetName() != nil && *in.GetName() == a.params.BucketName) + })). + Return(api.Buckets{Buckets: &buckets}, nil) + } + } + + withCols := func(p string) optFn { + return func(t *testing.T, a *setupArgs) { + data, err := os.ReadFile(filepath.Join("testdata", p)) + require.NoError(t, err) + + var f bucket_schema.ColumnsFormat + decoder, err := f.DecoderFn(p) + require.NoError(t, err) + cols, err := decoder(bytes.NewReader(data)) + require.NoError(t, err) + a.cols = cols + } + } + + expGetMeasurementSchema := func() optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + + req := api.ApiGetMeasurementSchemasRequest{ApiService: a.schemas}.BucketID(bucketID) + + a.schemas.EXPECT(). + GetMeasurementSchemas(gomock.Any(), bucketID). + Return(req) + + a.schemas.EXPECT(). + GetMeasurementSchemasExecute(tmock.MatchedBy(func(in api.ApiGetMeasurementSchemasRequest) bool { + return (in.GetOrgID() != nil && *in.GetOrgID() == orgID) && + in.GetBucketID() == bucketID && + (in.GetName() != nil && *in.GetName() == a.params.Name) + })). + Return(api.MeasurementSchemaList{ + MeasurementSchemas: []api.MeasurementSchema{ + { + Id: measurementID, + Name: a.params.Name, + Columns: a.cols, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + }, + }, nil) + } + } + + expUpdate := func() optFn { + return func(t *testing.T, a *setupArgs) { + t.Helper() + + req := api.ApiUpdateMeasurementSchemaRequest{ApiService: a.schemas}.BucketID(bucketID).MeasurementID(measurementID) + + a.schemas.EXPECT(). + UpdateMeasurementSchema(gomock.Any(), bucketID, measurementID). + Return(req) + + a.schemas.EXPECT(). + UpdateMeasurementSchemaExecute(tmock.MatchedBy(func(in api.ApiUpdateMeasurementSchemaRequest) bool { + return cmp.Equal(in.GetOrgID(), &orgID) && + cmp.Equal(in.GetBucketID(), bucketID) && + cmp.Equal(in.GetMeasurementID(), measurementID) && + cmp.Equal(in.GetMeasurementSchemaUpdateRequest().Columns, a.cols) + })). + Return(api.MeasurementSchema{ + Id: measurementID, + Name: a.params.Name, + Columns: a.cols, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, nil) + } + } + + opts := func(opts ...optFn) []optFn { return opts } + + lines := func(lines ...string) []string { return lines } + + cases := []struct { + name string + opts []optFn + expErr string + expLines []string + }{ + { + name: "unable to guess from stdin", + expErr: `unable to guess format for file "stdin"`, + }, + { + name: "org arg missing", + opts: opts( + withArgs(args{BucketName: "my-bucket", ColumnsFile: "columns.csv"}), + ), + expErr: "org missing: specify org ID or org name", + }, + { + name: "bucket arg missing", + opts: opts( + withArgs(args{OrgName: "my-org", ColumnsFile: "columns.csv"}), + ), + expErr: "bucket missing: specify bucket ID or bucket name", + }, + { + name: "bucket not found", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", ColumnsFile: "columns.csv"}), + expGetBuckets(), + ), + expErr: `bucket "my-bucket" not found`, + }, + { + name: "update succeeds", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv"}), + withCols("columns.csv"), + + expGetBuckets("my-bucket"), + expGetMeasurementSchema(), + expUpdate(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Bucket ID$`, + `^1010\s+cpu\s+f00d$`, + ), + }, + { + name: "update succeeds extended output", + opts: opts( + withArgs(args{OrgName: "my-org", BucketName: "my-bucket", Name: "cpu", ColumnsFile: "columns.csv", ExtendedOutput: true}), + withCols("columns.csv"), + expGetBuckets("my-bucket"), + expGetMeasurementSchema(), + expUpdate(), + ), + expLines: lines( + `^ID\s+Measurement Name\s+Column Name\s+Column Type\s+Column Data Type\s+Bucket ID$`, + `^1010\s+cpu\s+time\s+timestamp\s+f00d$`, + `^1010\s+cpu\s+host\s+tag\s+f00d$`, + `^1010\s+cpu\s+usage_user\s+field\s+float\s+f00d$`, + + ), + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockIO := mock.NewMockStdIO(ctrl) + writtenBytes := bytes.Buffer{} + mockIO.EXPECT().Write(gomock.Any()).DoAndReturn(writtenBytes.Write).AnyTimes() + + args := &setupArgs{ + buckets: mock.NewMockBucketsApi(ctrl), + schemas: mock.NewMockBucketSchemasApi(ctrl), + stdio: mockIO, + cli: cmd.CLI{StdIO: mockIO}, + } + + for _, opt := range tc.opts { + opt(t, args) + } + + c := bucket_schema.Client{ + BucketsApi: args.buckets, + BucketSchemasApi: args.schemas, + CLI: args.cli, + } + + err := c.Update(context.Background(), args.params) + if tc.expErr != "" { + assert.EqualError(t, err, tc.expErr) + } else { + require.NoError(t, err) + testutils.MatchLines(t, tc.expLines, strings.Split(writtenBytes.String(), "\n")) + } + }) + } +} diff --git a/internal/cli.go b/internal/cmd/cli.go similarity index 98% rename from internal/cli.go rename to internal/cmd/cli.go index 8f4c9f4..1c92c79 100644 --- a/internal/cli.go +++ b/internal/cmd/cli.go @@ -1,4 +1,4 @@ -package internal +package cmd import ( "encoding/json" diff --git a/internal/params.go b/internal/cmd/params.go similarity index 93% rename from internal/params.go rename to internal/cmd/params.go index 81f0440..848b565 100644 --- a/internal/params.go +++ b/internal/cmd/params.go @@ -1,4 +1,4 @@ -package internal +package cmd import ( "github.com/influxdata/influx-cli/v2/pkg/influxid" diff --git a/internal/cmd/ping/ping.go b/internal/cmd/ping/ping.go new file mode 100644 index 0000000..ec2d719 --- /dev/null +++ b/internal/cmd/ping/ping.go @@ -0,0 +1,22 @@ +package ping + +import ( + "context" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" +) + +type Client struct { + cmd.CLI + api.HealthApi +} + +// Ping checks the health of a remote InfluxDB instance. +func (c Client) Ping(ctx context.Context) error { + if _, err := c.GetHealth(ctx).Execute(); err != nil { + return err + } + _, err := c.StdIO.Write([]byte("OK\n")) + return err +} diff --git a/internal/ping_test.go b/internal/cmd/ping/ping_test.go similarity index 80% rename from internal/ping_test.go rename to internal/cmd/ping/ping_test.go index 75b292c..5043724 100644 --- a/internal/ping_test.go +++ b/internal/cmd/ping/ping_test.go @@ -1,4 +1,4 @@ -package internal_test +package ping_test import ( "bytes" @@ -7,8 +7,9 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/influxdata/influx-cli/v2/internal" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/ping" "github.com/influxdata/influx-cli/v2/internal/mock" "github.com/stretchr/testify/require" ) @@ -24,9 +25,12 @@ func Test_PingSuccess(t *testing.T) { stdio := mock.NewMockStdIO(ctrl) bytesWritten := bytes.Buffer{} stdio.EXPECT().Write(gomock.Any()).DoAndReturn(bytesWritten.Write).AnyTimes() - cli := &internal.CLI{StdIO: stdio} + cli := ping.Client{ + CLI: cmd.CLI{StdIO: stdio}, + HealthApi: client, + } - require.NoError(t, cli.Ping(context.Background(), client)) + require.NoError(t, cli.Ping(context.Background())) require.Equal(t, "OK\n", bytesWritten.String()) } @@ -38,9 +42,11 @@ func Test_PingFailedRequest(t *testing.T) { client := mock.NewMockHealthApi(ctrl) client.EXPECT().GetHealth(gomock.Any()).Return(api.ApiGetHealthRequest{ApiService: client}) client.EXPECT().GetHealthExecute(gomock.Any()).Return(api.HealthCheck{}, errors.New(e)) + cli := ping.Client{ + HealthApi: client, + } - cli := &internal.CLI{} - err := cli.Ping(context.Background(), client) + err := cli.Ping(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), e) } @@ -54,9 +60,10 @@ func Test_PingFailedStatus(t *testing.T) { client.EXPECT().GetHealth(gomock.Any()).Return(api.ApiGetHealthRequest{ApiService: client}) client.EXPECT().GetHealthExecute(gomock.Any()). Return(api.HealthCheck{}, &api.HealthCheck{Status: api.HEALTHCHECKSTATUS_FAIL, Message: &e}) - - cli := &internal.CLI{} - err := cli.Ping(context.Background(), client) + cli := ping.Client{ + HealthApi: client, + } + err := cli.Ping(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), e) } @@ -71,8 +78,10 @@ func Test_PingFailedStatusNoMessage(t *testing.T) { client.EXPECT().GetHealthExecute(gomock.Any()). Return(api.HealthCheck{}, &api.HealthCheck{Status: api.HEALTHCHECKSTATUS_FAIL, Name: name}) - cli := &internal.CLI{} - err := cli.Ping(context.Background(), client) + cli := ping.Client{ + HealthApi: client, + } + err := cli.Ping(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), name) } diff --git a/internal/setup.go b/internal/cmd/setup/setup.go similarity index 90% rename from internal/setup.go rename to internal/cmd/setup/setup.go index 26a5d84..99e87ba 100644 --- a/internal/setup.go +++ b/internal/cmd/setup/setup.go @@ -1,4 +1,4 @@ -package internal +package setup import ( "context" @@ -9,21 +9,12 @@ import ( "time" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/bucket" "github.com/influxdata/influx-cli/v2/internal/config" "github.com/influxdata/influx-cli/v2/internal/duration" ) -type SetupParams struct { - Username string - Password string - AuthToken string - Org string - Bucket string - Retention string - Force bool - ConfigName string -} - var ( ErrPasswordIsTooShort = errors.New("password is too short") ErrAlreadySetUp = errors.New("instance has already been set up") @@ -33,9 +24,25 @@ var ( const MinPasswordLen = 8 -func (c *CLI) Setup(ctx context.Context, client api.SetupApi, params *SetupParams) error { +type Client struct { + cmd.CLI + api.SetupApi +} + +type Params struct { + Username string + Password string + AuthToken string + Org string + Bucket string + Retention string + Force bool + ConfigName string +} + +func (c Client) Setup(ctx context.Context, params *Params) error { // Check if setup is even allowed. - checkResp, err := client.GetSetup(ctx).Execute() + checkResp, err := c.GetSetup(ctx).Execute() if err != nil { return fmt.Errorf("failed to check if already set up: %w", err) } @@ -54,7 +61,7 @@ func (c *CLI) Setup(ctx context.Context, client api.SetupApi, params *SetupParam if err != nil { return err } - resp, err := client.PostSetup(ctx).OnboardingRequest(setupBody).Execute() + resp, err := c.PostSetup(ctx).OnboardingRequest(setupBody).Execute() if err != nil { return fmt.Errorf("failed to setup instance: %w", err) } @@ -91,7 +98,7 @@ func (c *CLI) Setup(ctx context.Context, client api.SetupApi, params *SetupParam // validateNoNameCollision checks that we will be able to write onboarding results to local config: // - If a custom name was given, check that it doesn't collide with existing config // - If no custom name was given, check that we don't already have configs -func (c *CLI) validateNoNameCollision(configName string) error { +func (c Client) validateNoNameCollision(configName string) error { existingConfigs, err := c.ConfigService.ListConfigs() if err != nil { return fmt.Errorf("error checking existing configs: %w", err) @@ -116,7 +123,7 @@ func (c *CLI) validateNoNameCollision(configName string) error { // onboardingRequest constructs a request body for the onboarding API. // Unless the 'force' parameter is set, the user will be prompted to enter any missing information // and to confirm the final request parameters. -func (c *CLI) onboardingRequest(params *SetupParams) (req api.OnboardingRequest, err error) { +func (c Client) onboardingRequest(params *Params) (req api.OnboardingRequest, err error) { if (params.Force || params.Password != "") && len(params.Password) < MinPasswordLen { return req, ErrPasswordIsTooShort } @@ -131,7 +138,7 @@ func (c *CLI) onboardingRequest(params *SetupParams) (req api.OnboardingRequest, if params.AuthToken != "" { req.Token = ¶ms.AuthToken } - rpSecs := int64(InfiniteRetention) + rpSecs := int64(bucket.InfiniteRetention) if params.Retention != "" { dur, err := duration.RawDurationToTimeDuration(params.Retention) if err != nil { @@ -192,7 +199,7 @@ func (c *CLI) onboardingRequest(params *SetupParams) (req api.OnboardingRequest, } } if params.Retention == "" { - infiniteStr := strconv.Itoa(InfiniteRetention) + infiniteStr := strconv.Itoa(bucket.InfiniteRetention) for { rpStr, err := c.StdIO.GetStringInput("Please type your retention period in hours, or 0 for infinite", infiniteStr) if err != nil { diff --git a/internal/setup_test.go b/internal/cmd/setup/setup_test.go similarity index 79% rename from internal/setup_test.go rename to internal/cmd/setup/setup_test.go index b2fa001..90ca6d6 100644 --- a/internal/setup_test.go +++ b/internal/cmd/setup/setup_test.go @@ -1,4 +1,4 @@ -package internal_test +package setup_test import ( "bytes" @@ -10,11 +10,13 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/influxdata/influx-cli/v2/internal" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/setup" "github.com/influxdata/influx-cli/v2/internal/config" "github.com/influxdata/influx-cli/v2/internal/duration" "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/influxdata/influx-cli/v2/internal/testutils" "github.com/stretchr/testify/assert" tmock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -31,9 +33,12 @@ func Test_SetupConfigNameCollision(t *testing.T) { cfg := "foo" configSvc := mock.NewMockConfigService(ctrl) configSvc.EXPECT().ListConfigs().Return(map[string]config.Config{cfg: {}}, nil) - cli := &internal.CLI{ConfigService: configSvc} + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc}, + SetupApi: client, + } - err := cli.Setup(context.Background(), client, &internal.SetupParams{ConfigName: cfg}) + err := cli.Setup(context.Background(), &setup.Params{ConfigName: cfg}) require.Error(t, err) require.Contains(t, err.Error(), cfg) require.Contains(t, err.Error(), "already exists") @@ -49,11 +54,14 @@ func Test_SetupConfigNameRequired(t *testing.T) { configSvc := mock.NewMockConfigService(ctrl) configSvc.EXPECT().ListConfigs().Return(map[string]config.Config{"foo": {}}, nil) - cli := &internal.CLI{ConfigService: configSvc} + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc}, + SetupApi: client, + } - err := cli.Setup(context.Background(), client, &internal.SetupParams{}) + err := cli.Setup(context.Background(), &setup.Params{}) require.Error(t, err) - require.Equal(t, internal.ErrConfigNameRequired, err) + require.Equal(t, setup.ErrConfigNameRequired, err) } func Test_SetupAlreadySetup(t *testing.T) { @@ -64,11 +72,14 @@ func Test_SetupAlreadySetup(t *testing.T) { client.EXPECT().GetSetupExecute(gomock.Any()).Return(api.InlineResponse200{Allowed: api.PtrBool(false)}, nil) configSvc := mock.NewMockConfigService(ctrl) - cli := &internal.CLI{ConfigService: configSvc} + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc}, + SetupApi: client, + } - err := cli.Setup(context.Background(), client, &internal.SetupParams{}) + err := cli.Setup(context.Background(), &setup.Params{}) require.Error(t, err) - require.Equal(t, internal.ErrAlreadySetUp, err) + require.Equal(t, setup.ErrAlreadySetUp, err) } func Test_SetupCheckFailed(t *testing.T) { @@ -81,9 +92,12 @@ func Test_SetupCheckFailed(t *testing.T) { client.EXPECT().GetSetupExecute(gomock.Any()).Return(api.InlineResponse200{}, errors.New(e)) configSvc := mock.NewMockConfigService(ctrl) - cli := &internal.CLI{ConfigService: configSvc} + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc}, + SetupApi: client, + } - err := cli.Setup(context.Background(), client, &internal.SetupParams{}) + err := cli.Setup(context.Background(), &setup.Params{}) require.Error(t, err) require.Contains(t, err.Error(), e) } @@ -92,7 +106,7 @@ func Test_SetupSuccessNoninteractive(t *testing.T) { t.Parallel() retentionSecs := int64(duration.Week.Seconds()) - params := internal.SetupParams{ + params := setup.Params{ Username: "user", Password: "mysecretpassword", AuthToken: "mytoken", @@ -140,14 +154,15 @@ func Test_SetupSuccessNoninteractive(t *testing.T) { stdio := mock.NewMockStdIO(ctrl) bytesWritten := bytes.Buffer{} stdio.EXPECT().Write(gomock.Any()).DoAndReturn(bytesWritten.Write).AnyTimes() - cli := &internal.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio} - require.NoError(t, cli.Setup(context.Background(), client, ¶ms)) - - outLines := strings.Split(strings.TrimSpace(bytesWritten.String()), "\n") - require.Len(t, outLines, 2) - header, data := outLines[0], outLines[1] - require.Regexp(t, "User\\s+Organization\\s+Bucket", header) - require.Regexp(t, fmt.Sprintf("%s\\s+%s\\s+%s", params.Username, params.Org, params.Bucket), data) + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio}, + SetupApi: client, + } + require.NoError(t, cli.Setup(context.Background(), ¶ms)) + testutils.MatchLines(t, []string{ + `User\s+Organization\s+Bucket`, + fmt.Sprintf(`%s\s+%s\s+%s`, params.Username, params.Org, params.Bucket), + }, strings.Split(bytesWritten.String(), "\n")) } func Test_SetupSuccessInteractive(t *testing.T) { @@ -206,21 +221,22 @@ func Test_SetupSuccessInteractive(t *testing.T) { stdio.EXPECT().GetStringInput("Please type your primary bucket name", gomock.Any()).Return(bucket, nil) stdio.EXPECT().GetStringInput("Please type your retention period in hours, or 0 for infinite", gomock.Any()).Return(strconv.Itoa(retentionHrs), nil) stdio.EXPECT().GetConfirm(gomock.Any()).Return(true) - cli := &internal.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio} - require.NoError(t, cli.Setup(context.Background(), client, &internal.SetupParams{})) - - outLines := strings.Split(strings.TrimSpace(bytesWritten.String()), "\n") - require.Len(t, outLines, 2) - header, data := outLines[0], outLines[1] - require.Regexp(t, "User\\s+Organization\\s+Bucket", header) - require.Regexp(t, fmt.Sprintf("%s\\s+%s\\s+%s", username, org, bucket), data) + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio}, + SetupApi: client, + } + require.NoError(t, cli.Setup(context.Background(), &setup.Params{})) + testutils.MatchLines(t, []string{ + `User\s+Organization\s+Bucket`, + fmt.Sprintf(`%s\s+%s\s+%s`, username, org, bucket), + }, strings.Split(bytesWritten.String(), "\n")) } func Test_SetupPasswordParamToShort(t *testing.T) { t.Parallel() retentionSecs := int64(duration.Week.Seconds()) - params := internal.SetupParams{ + params := setup.Params{ Username: "user", Password: "2short", AuthToken: "mytoken", @@ -240,16 +256,19 @@ func Test_SetupPasswordParamToShort(t *testing.T) { configSvc.EXPECT().ListConfigs().Return(nil, nil) stdio := mock.NewMockStdIO(ctrl) - cli := &internal.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio} - err := cli.Setup(context.Background(), client, ¶ms) - require.Equal(t, internal.ErrPasswordIsTooShort, err) + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio}, + SetupApi: client, + } + err := cli.Setup(context.Background(), ¶ms) + require.Equal(t, setup.ErrPasswordIsTooShort, err) } func Test_SetupCancelAtConfirmation(t *testing.T) { t.Parallel() retentionSecs := int64(duration.Week.Seconds()) - params := internal.SetupParams{ + params := setup.Params{ Username: "user", Password: "mysecretpassword", AuthToken: "mytoken", @@ -272,7 +291,10 @@ func Test_SetupCancelAtConfirmation(t *testing.T) { stdio.EXPECT().Banner(gomock.Any()) stdio.EXPECT().GetConfirm(gomock.Any()).Return(false) - cli := &internal.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio} - err := cli.Setup(context.Background(), client, ¶ms) - require.Equal(t, internal.ErrSetupCanceled, err) + cli := setup.Client{ + CLI: cmd.CLI{ConfigService: configSvc, ActiveConfig: config.Config{Host: host}, StdIO: stdio}, + SetupApi: client, + } + err := cli.Setup(context.Background(), ¶ms) + require.Equal(t, setup.ErrSetupCanceled, err) } diff --git a/internal/batcher/buffer_batcher.go b/internal/cmd/write/buffer_batcher.go similarity index 99% rename from internal/batcher/buffer_batcher.go rename to internal/cmd/write/buffer_batcher.go index bd0a471..cf2ebed 100644 --- a/internal/batcher/buffer_batcher.go +++ b/internal/cmd/write/buffer_batcher.go @@ -1,4 +1,4 @@ -package batcher +package write import ( "bufio" diff --git a/internal/batcher/buffer_batcher_internal_test.go b/internal/cmd/write/buffer_batcher_internal_test.go similarity index 99% rename from internal/batcher/buffer_batcher_internal_test.go rename to internal/cmd/write/buffer_batcher_internal_test.go index 8f601b7..cedaf3b 100644 --- a/internal/batcher/buffer_batcher_internal_test.go +++ b/internal/cmd/write/buffer_batcher_internal_test.go @@ -1,4 +1,4 @@ -package batcher +package write import ( "bufio" diff --git a/internal/batcher/buffer_batcher_test.go b/internal/cmd/write/buffer_batcher_test.go similarity index 95% rename from internal/batcher/buffer_batcher_test.go rename to internal/cmd/write/buffer_batcher_test.go index 2758654..135a980 100644 --- a/internal/batcher/buffer_batcher_test.go +++ b/internal/cmd/write/buffer_batcher_test.go @@ -1,4 +1,4 @@ -package batcher_test +package write_test import ( "bufio" @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/influxdata/influx-cli/v2/internal/batcher" + "github.com/influxdata/influx-cli/v2/internal/cmd/write" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -45,7 +45,7 @@ func TestScanLines(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { scanner := bufio.NewScanner(strings.NewReader(tt.input)) - scanner.Split(batcher.ScanLines) + scanner.Split(write.ScanLines) got := []string{} for scanner.Scan() { got = append(got, scanner.Text()) @@ -129,7 +129,7 @@ func TestBatcher_WriteTo(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := &batcher.BufferBatcher{ + b := &write.BufferBatcher{ MaxFlushBytes: tt.fields.MaxFlushBytes, MaxFlushInterval: tt.fields.MaxFlushInterval, } @@ -151,7 +151,7 @@ func TestBatcher_WriteTo(t *testing.T) { }) // test the same data, but now with WriteBatches function t.Run("WriteTo_"+tt.name, func(t *testing.T) { - b := &batcher.BufferBatcher{ + b := &write.BufferBatcher{ MaxFlushBytes: tt.fields.MaxFlushBytes, MaxFlushInterval: tt.fields.MaxFlushInterval, } @@ -175,7 +175,7 @@ func TestBatcher_WriteTo(t *testing.T) { } func TestBatcher_WriteTimeout(t *testing.T) { - b := &batcher.BufferBatcher{} + b := &write.BufferBatcher{} // this mimics a reader like stdin that may never return data. r, _ := io.Pipe() diff --git a/internal/cmd/write/dryrun.go b/internal/cmd/write/dryrun.go new file mode 100644 index 0000000..07a2127 --- /dev/null +++ b/internal/cmd/write/dryrun.go @@ -0,0 +1,29 @@ +package write + +import ( + "context" + "io" + + "github.com/influxdata/influx-cli/v2/internal/cmd" +) + +type DryRunClient struct { + cmd.CLI + LineReader +} + +func (c DryRunClient) WriteDryRun(ctx context.Context) error { + r, closer, err := c.LineReader.Open(ctx) + if closer != nil { + defer closer.Close() + } + if err != nil { + return err + } + + if _, err := io.Copy(c.StdIO, r); err != nil { + return err + } + + return nil +} diff --git a/internal/cmd/write/dryrun_test.go b/internal/cmd/write/dryrun_test.go new file mode 100644 index 0000000..fa6bfeb --- /dev/null +++ b/internal/cmd/write/dryrun_test.go @@ -0,0 +1,42 @@ +package write_test + +import ( + "bytes" + "context" + "io" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/write" + "github.com/influxdata/influx-cli/v2/internal/config" + "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/stretchr/testify/require" +) + +func TestWriteDryRun(t *testing.T) { + t.Parallel() + + inLines := ` +fake line protocol 1 +fake line protocol 2 +fake line protocol 3 +` + mockReader := bufferReader{} + _, err := io.Copy(&mockReader.buf, strings.NewReader(inLines)) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + stdio := mock.NewMockStdIO(ctrl) + bytesWritten := bytes.Buffer{} + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(bytesWritten.Write).AnyTimes() + + cli := write.DryRunClient{ + CLI: cmd.CLI{ActiveConfig: config.Config{Org: "my-default-org"}, StdIO: stdio}, + LineReader: &mockReader, + } + + require.NoError(t, cli.WriteDryRun(context.Background())) + require.Equal(t, inLines, bytesWritten.String()) +} diff --git a/internal/linereader/multi_reader.go b/internal/cmd/write/multi_reader.go similarity index 99% rename from internal/linereader/multi_reader.go rename to internal/cmd/write/multi_reader.go index 2b43186..c7f9086 100644 --- a/internal/linereader/multi_reader.go +++ b/internal/cmd/write/multi_reader.go @@ -1,4 +1,4 @@ -package linereader +package write import ( "compress/gzip" diff --git a/internal/linereader/multi_reader_test.go b/internal/cmd/write/multi_reader_test.go similarity index 92% rename from internal/linereader/multi_reader_test.go rename to internal/cmd/write/multi_reader_test.go index c2b6ec3..9c32d41 100644 --- a/internal/linereader/multi_reader_test.go +++ b/internal/cmd/write/multi_reader_test.go @@ -1,4 +1,4 @@ -package linereader_test +package write_test import ( "bufio" @@ -14,7 +14,7 @@ import ( "strings" "testing" - "github.com/influxdata/influx-cli/v2/internal/linereader" + "github.com/influxdata/influx-cli/v2/internal/cmd/write" "github.com/stretchr/testify/require" ) @@ -117,8 +117,8 @@ func TestLineReader(t *testing.T) { args []string files []string urls []string - format linereader.InputFormat - compression linereader.InputCompression + format write.InputFormat + compression write.InputCompression encoding string headers []string skipHeader int @@ -148,7 +148,7 @@ func TestLineReader(t *testing.T) { { name: "read compressed LP data from file", files: []string{gzipLpFileNoExt}, - compression: linereader.InputCompressionGZIP, + compression: write.InputCompressionGZIP, firstLineCorrection: 0, lines: []string{ lpContents, @@ -157,7 +157,7 @@ func TestLineReader(t *testing.T) { { name: "read compressed data from LP file using non-UTF encoding", files: []string{gzipLpFileNoExt}, - compression: linereader.InputCompressionGZIP, + compression: write.InputCompressionGZIP, encoding: "ISO_8859-1", firstLineCorrection: 0, lines: []string{ @@ -190,7 +190,7 @@ func TestLineReader(t *testing.T) { }, { name: "read compressed LP data from stdin", - compression: linereader.InputCompressionGZIP, + compression: write.InputCompressionGZIP, stdIn: gzipStdin(stdInLpContents), lines: []string{ stdInLpContents, @@ -206,7 +206,7 @@ func TestLineReader(t *testing.T) { }, { name: "read compressed LP data from stdin using '-' argument", - compression: linereader.InputCompressionGZIP, + compression: write.InputCompressionGZIP, args: []string{"-"}, stdIn: gzipStdin(stdInLpContents), lines: []string{ @@ -230,7 +230,7 @@ func TestLineReader(t *testing.T) { { name: "read compressed LP data from URL", urls: []string{fmt.Sprintf("/a?data=%s&compress=true", url.QueryEscape(lpContents))}, - compression: linereader.InputCompressionGZIP, + compression: write.InputCompressionGZIP, lines: []string{ lpContents, }, @@ -260,7 +260,7 @@ func TestLineReader(t *testing.T) { { name: "read compressed CSV data from file + transform to line protocol", files: []string{gzipCsvFileNoExt}, - compression: linereader.InputCompressionGZIP, + compression: write.InputCompressionGZIP, firstLineCorrection: 0, lines: []string{ lpContents, @@ -296,7 +296,7 @@ func TestLineReader(t *testing.T) { }, { name: "read CSV data from stdin + transform to line protocol", - format: linereader.InputFormatCSV, + format: write.InputFormatCSV, stdIn: strings.NewReader(stdInCsvContents), lines: []string{ stdInLpContents, @@ -304,8 +304,8 @@ func TestLineReader(t *testing.T) { }, { name: "read compressed CSV data from stdin + transform to line protocol", - format: linereader.InputFormatCSV, - compression: linereader.InputCompressionGZIP, + format: write.InputFormatCSV, + compression: write.InputCompressionGZIP, stdIn: gzipStdin(stdInCsvContents), lines: []string{ stdInLpContents, @@ -313,7 +313,7 @@ func TestLineReader(t *testing.T) { }, { name: "read CSV data from stdin using '-' argument + transform to line protocol", - format: linereader.InputFormatCSV, + format: write.InputFormatCSV, args: []string{"-"}, stdIn: strings.NewReader(stdInCsvContents), lines: []string{ @@ -322,8 +322,8 @@ func TestLineReader(t *testing.T) { }, { name: "read compressed CSV data from stdin using '-' argument + transform to line protocol", - format: linereader.InputFormatCSV, - compression: linereader.InputCompressionGZIP, + format: write.InputFormatCSV, + compression: write.InputCompressionGZIP, args: []string{"-"}, stdIn: gzipStdin(stdInCsvContents), lines: []string{ @@ -332,7 +332,7 @@ func TestLineReader(t *testing.T) { }, { name: "read CSV data from 1st argument + transform to line protocol", - format: linereader.InputFormatCSV, + format: write.InputFormatCSV, args: []string{stdInCsvContents}, lines: []string{ stdInLpContents, @@ -348,7 +348,7 @@ func TestLineReader(t *testing.T) { { name: "read compressed CSV data from URL + transform to line protocol", urls: []string{fmt.Sprintf("/a.csv?data=%s&compress=true", url.QueryEscape(csvContents))}, - compression: linereader.InputCompressionGZIP, + compression: write.InputCompressionGZIP, lines: []string{ lpContents, }, @@ -394,7 +394,7 @@ func TestLineReader(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - r := &linereader.MultiInputLineReader{ + r := &write.MultiInputLineReader{ StdIn: test.stdIn, HttpClient: &mockClient{t: t}, Args: test.args, @@ -456,7 +456,7 @@ func TestLineReaderErrors(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - r := linereader.MultiInputLineReader{ + r := write.MultiInputLineReader{ HttpClient: &mockClient{t: t, fail: true}, Files: test.files, URLs: test.urls, @@ -475,10 +475,10 @@ func TestLineReaderErrorOut(t *testing.T) { stdInContents := "_measurement,a|long:strict\nm,1\nm,1.1" errorOut := bytes.Buffer{} - r := linereader.MultiInputLineReader{ + r := write.MultiInputLineReader{ StdIn: strings.NewReader(stdInContents), ErrorOut: &errorOut, - Format: linereader.InputFormatCSV, + Format: write.InputFormatCSV, } reader, closer, err := r.Open(context.Background()) require.NoError(t, err) diff --git a/internal/throttler/throttler.go b/internal/cmd/write/throttler.go similarity index 99% rename from internal/throttler/throttler.go rename to internal/cmd/write/throttler.go index 48a702d..fd4163b 100644 --- a/internal/throttler/throttler.go +++ b/internal/cmd/write/throttler.go @@ -1,4 +1,4 @@ -package throttler +package write import ( "context" diff --git a/internal/throttler/throttler_test.go b/internal/cmd/write/throttler_test.go similarity index 88% rename from internal/throttler/throttler_test.go rename to internal/cmd/write/throttler_test.go index 2accfaa..db9991a 100644 --- a/internal/throttler/throttler_test.go +++ b/internal/cmd/write/throttler_test.go @@ -1,4 +1,4 @@ -package throttler_test +package write_test import ( "bytes" @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/influxdata/influx-cli/v2/internal/throttler" + "github.com/influxdata/influx-cli/v2/internal/cmd/write" "github.com/stretchr/testify/require" ) @@ -14,9 +14,9 @@ func TestThrottlerPassthrough(t *testing.T) { // Hard to test that rate-limiting actually works, so we just check // that no data is lost. in := "Hello world!" - bps, err := throttler.ToBytesPerSecond("1B/s") + bps, err := write.ToBytesPerSecond("1B/s") require.NoError(t, err) - throttler := throttler.NewThrottler(bps) + throttler := write.NewThrottler(bps) r := throttler.Throttle(context.Background(), strings.NewReader(in)) out := bytes.Buffer{} @@ -71,7 +71,7 @@ func TestToBytesPerSecond(t *testing.T) { } for _, test := range tests { t.Run(test.in, func(t *testing.T) { - bytesPerSec, err := throttler.ToBytesPerSecond(test.in) + bytesPerSec, err := write.ToBytesPerSecond(test.in) if len(test.error) == 0 { require.Equal(t, test.out, float64(bytesPerSec)) require.Nil(t, err) diff --git a/internal/write.go b/internal/cmd/write/write.go similarity index 62% rename from internal/write.go rename to internal/cmd/write/write.go index 2251478..ad22d95 100644 --- a/internal/write.go +++ b/internal/cmd/write/write.go @@ -1,4 +1,4 @@ -package internal +package write import ( "context" @@ -7,28 +7,30 @@ import ( "io" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" ) type LineReader interface { Open(ctx context.Context) (io.Reader, io.Closer, error) } -type Throttler interface { +type RateLimiter interface { Throttle(ctx context.Context, in io.Reader) io.Reader } -type Batcher interface { +type BatchWriter interface { WriteBatches(ctx context.Context, r io.Reader, writeFn func(batch []byte) error) error } -type WriteClients struct { - Reader LineReader - Throttler Throttler - Writer Batcher - Client api.WriteApi +type Client struct { + cmd.CLI + api.WriteApi + LineReader + RateLimiter + BatchWriter } -type WriteParams struct { +type Params struct { BucketID string BucketName string OrgID string @@ -38,7 +40,7 @@ type WriteParams struct { var ErrWriteCanceled = errors.New("write canceled") -func (c *CLI) Write(ctx context.Context, clients *WriteClients, params *WriteParams) error { +func (c Client) Write(ctx context.Context, params *Params) error { if params.OrgID == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { return errors.New("must specify org ID or org name") } @@ -46,7 +48,7 @@ func (c *CLI) Write(ctx context.Context, clients *WriteClients, params *WritePar return errors.New("must specify bucket ID or bucket name") } - r, closer, err := clients.Reader.Open(ctx) + r, closer, err := c.LineReader.Open(ctx) if closer != nil { defer closer.Close() } @@ -55,7 +57,7 @@ func (c *CLI) Write(ctx context.Context, clients *WriteClients, params *WritePar } writeBatch := func(batch []byte) error { - req := clients.Client.PostWrite(ctx).Body(batch).ContentEncoding("gzip").Precision(params.Precision) + req := c.PostWrite(ctx).Body(batch).ContentEncoding("gzip").Precision(params.Precision) if params.BucketID != "" { req = req.Bucket(params.BucketID) } else { @@ -76,7 +78,7 @@ func (c *CLI) Write(ctx context.Context, clients *WriteClients, params *WritePar return nil } - if err := clients.Writer.WriteBatches(ctx, clients.Throttler.Throttle(ctx, r), writeBatch); err == context.Canceled { + if err := c.BatchWriter.WriteBatches(ctx, c.RateLimiter.Throttle(ctx, r), writeBatch); err == context.Canceled { return ErrWriteCanceled } else if err != nil { return fmt.Errorf("failed to write data: %w", err) @@ -84,19 +86,3 @@ func (c *CLI) Write(ctx context.Context, clients *WriteClients, params *WritePar return nil } - -func (c *CLI) WriteDryRun(ctx context.Context, reader LineReader) error { - r, closer, err := reader.Open(ctx) - if closer != nil { - defer closer.Close() - } - if err != nil { - return err - } - - if _, err := io.Copy(c.StdIO, r); err != nil { - return err - } - - return nil -} diff --git a/internal/write_test.go b/internal/cmd/write/write_test.go similarity index 73% rename from internal/write_test.go rename to internal/cmd/write/write_test.go index 7557ce2..75ed9dd 100644 --- a/internal/write_test.go +++ b/internal/cmd/write/write_test.go @@ -1,4 +1,4 @@ -package internal_test +package write_test import ( "bytes" @@ -9,8 +9,9 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/influxdata/influx-cli/v2/internal" "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/write" "github.com/influxdata/influx-cli/v2/internal/config" "github.com/influxdata/influx-cli/v2/internal/mock" "github.com/stretchr/testify/assert" @@ -64,12 +65,11 @@ func TestWriteByIDs(t *testing.T) { mockThrottler := noopThrottler{} mockBatcher := lineBatcher{} - params := internal.WriteParams{ + params := write.Params{ OrgID: "12345", BucketID: "98765", Precision: api.WRITEPRECISION_S, } - cli := internal.CLI{ActiveConfig: config.Config{Org: "my-default-org"}} ctrl := gomock.NewController(t) client := mock.NewMockWriteApi(ctrl) @@ -85,14 +85,15 @@ func TestWriteByIDs(t *testing.T) { return nil }).Times(len(inLines)) - clients := internal.WriteClients{ - Reader: &mockReader, - Throttler: &mockThrottler, - Writer: &mockBatcher, - Client: client, + cli := write.Client{ + CLI: cmd.CLI{ActiveConfig: config.Config{Org: "my-default-org"}}, + LineReader: &mockReader, + RateLimiter: &mockThrottler, + BatchWriter: &mockBatcher, + WriteApi: client, } - require.NoError(t, cli.Write(context.Background(), &clients, ¶ms)) + require.NoError(t, cli.Write(context.Background(), ¶ms)) require.Equal(t, inLines, writtenLines) require.True(t, mockThrottler.used) } @@ -109,12 +110,11 @@ func TestWriteByNames(t *testing.T) { mockThrottler := noopThrottler{} mockBatcher := lineBatcher{} - params := internal.WriteParams{ + params := write.Params{ OrgName: "my-org", BucketName: "my-bucket", Precision: api.WRITEPRECISION_US, } - cli := internal.CLI{ActiveConfig: config.Config{Org: "my-default-org"}} ctrl := gomock.NewController(t) client := mock.NewMockWriteApi(ctrl) @@ -130,14 +130,15 @@ func TestWriteByNames(t *testing.T) { return nil }).Times(len(inLines)) - clients := internal.WriteClients{ - Reader: &mockReader, - Throttler: &mockThrottler, - Writer: &mockBatcher, - Client: client, + cli := write.Client{ + CLI: cmd.CLI{ActiveConfig: config.Config{Org: "my-default-org"}}, + LineReader: &mockReader, + RateLimiter: &mockThrottler, + BatchWriter: &mockBatcher, + WriteApi: client, } - require.NoError(t, cli.Write(context.Background(), &clients, ¶ms)) + require.NoError(t, cli.Write(context.Background(), ¶ms)) require.Equal(t, inLines, writtenLines) require.True(t, mockThrottler.used) } @@ -154,18 +155,18 @@ func TestWriteOrgFromConfig(t *testing.T) { mockThrottler := noopThrottler{} mockBatcher := lineBatcher{} - params := internal.WriteParams{ + params := write.Params{ BucketName: "my-bucket", Precision: api.WRITEPRECISION_US, } - cli := internal.CLI{ActiveConfig: config.Config{Org: "my-default-org"}} + defaultOrg := "my-default-org" ctrl := gomock.NewController(t) client := mock.NewMockWriteApi(ctrl) var writtenLines []string client.EXPECT().PostWrite(gomock.Any()).Return(api.ApiPostWriteRequest{ApiService: client}).Times(len(inLines)) client.EXPECT().PostWriteExecute(tmock.MatchedBy(func(in api.ApiPostWriteRequest) bool { - return assert.Equal(t, cli.ActiveConfig.Org, *in.GetOrg()) && + return assert.Equal(t, defaultOrg, *in.GetOrg()) && assert.Equal(t, params.BucketName, *in.GetBucket()) && assert.Equal(t, params.Precision, *in.GetPrecision()) && assert.Equal(t, "gzip", *in.GetContentEncoding()) // Make sure the body is properly marked for compression. @@ -174,37 +175,15 @@ func TestWriteOrgFromConfig(t *testing.T) { return nil }).Times(len(inLines)) - clients := internal.WriteClients{ - Reader: &mockReader, - Throttler: &mockThrottler, - Writer: &mockBatcher, - Client: client, + cli := write.Client{ + CLI: cmd.CLI{ActiveConfig: config.Config{Org: defaultOrg}}, + LineReader: &mockReader, + RateLimiter: &mockThrottler, + BatchWriter: &mockBatcher, + WriteApi: client, } - require.NoError(t, cli.Write(context.Background(), &clients, ¶ms)) + require.NoError(t, cli.Write(context.Background(), ¶ms)) require.Equal(t, inLines, writtenLines) require.True(t, mockThrottler.used) } - -func TestWriteDryRun(t *testing.T) { - t.Parallel() - - inLines := ` -fake line protocol 1 -fake line protocol 2 -fake line protocol 3 -` - mockReader := bufferReader{} - _, err := io.Copy(&mockReader.buf, strings.NewReader(inLines)) - require.NoError(t, err) - - ctrl := gomock.NewController(t) - stdio := mock.NewMockStdIO(ctrl) - bytesWritten := bytes.Buffer{} - stdio.EXPECT().Write(gomock.Any()).DoAndReturn(bytesWritten.Write).AnyTimes() - - cli := internal.CLI{ActiveConfig: config.Config{Org: "my-default-org"}, StdIO: stdio} - - require.NoError(t, cli.WriteDryRun(context.Background(), &mockReader)) - require.Equal(t, inLines, bytesWritten.String()) -} diff --git a/internal/ping.go b/internal/ping.go deleted file mode 100644 index 4ccccfe..0000000 --- a/internal/ping.go +++ /dev/null @@ -1,16 +0,0 @@ -package internal - -import ( - "context" - - "github.com/influxdata/influx-cli/v2/internal/api" -) - -// Ping checks the health of a remote InfluxDB instance. -func (c *CLI) Ping(ctx context.Context, client api.HealthApi) error { - if _, err := client.GetHealth(ctx).Execute(); err != nil { - return err - } - _, err := c.StdIO.Write([]byte("OK\n")) - return err -} diff --git a/internal/testutils/utils.go b/internal/testutils/utils.go new file mode 100644 index 0000000..8ee000d --- /dev/null +++ b/internal/testutils/utils.go @@ -0,0 +1,20 @@ +package testutils + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func MatchLines(t *testing.T, expectedLines []string, lines []string) { + var nonEmptyLines []string + for _, l := range lines { + if l != "" { + nonEmptyLines = append(nonEmptyLines, l) + } + } + require.Equal(t, len(expectedLines), len(nonEmptyLines)) + for i, expected := range expectedLines { + require.Regexp(t, expected, nonEmptyLines[i]) + } +}