refactor: split flat internal/ module into modules-per-cmd (#64)

This commit is contained in:
Daniel Moran 2021-05-06 13:47:03 -04:00 committed by GitHub
parent d995f7d182
commit ead7e63d41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2580 additions and 2330 deletions

View File

@ -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, &params)
api := getAPI(ctx)
client := bucket.Client{
CLI: getCLI(ctx),
BucketsApi: api.BucketsApi,
OrganizationsApi: api.OrganizationsApi,
}
return client.Create(ctx.Context, &params)
},
}
}
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, &params)
api := getAPI(ctx)
client := bucket.Client{
CLI: getCLI(ctx),
BucketsApi: api.BucketsApi,
OrganizationsApi: api.OrganizationsApi,
}
return client.Delete(ctx.Context, &params)
},
}
}
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, &params)
api := getAPI(ctx)
client := bucket.Client{
CLI: getCLI(ctx),
BucketsApi: api.BucketsApi,
OrganizationsApi: api.OrganizationsApi,
}
return client.List(ctx.Context, &params)
},
}
}
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, &params)
api := getAPI(ctx)
client := bucket.Client{
CLI: getCLI(ctx),
BucketsApi: api.BucketsApi,
OrganizationsApi: api.OrganizationsApi,
}
return client.Update(ctx.Context, &params)
},
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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",

View File

@ -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)
},
}
}

View File

@ -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, &params)
client := setup.Client{
CLI: getCLI(ctx),
SetupApi: getAPINoToken(ctx).SetupApi,
}
return client.Setup(ctx.Context, &params)
},
}
}

View File

@ -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, &params.WriteParams)
return client.Write(ctx.Context, &params.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)
},
}
}

View File

@ -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: &params.SchemaType,
}
if params.Description != "" {
reqBody.Description = &params.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...)
}

View File

@ -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])
})
}
}

View File

@ -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...)
}

View File

@ -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: &params.SchemaType,
}
if params.Description != "" {
reqBody.Description = &params.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})
}

View File

@ -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"))
})
}
}

View File

@ -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})
}

View File

@ -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"))
})
}
}

View File

@ -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)
}

View File

@ -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"))
})
}
}

View File

@ -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})
}

View File

@ -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"))
})
}
}

View File

@ -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{}

View File

@ -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"))
}
})
}
}

View File

@ -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)
}

View File

@ -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"))
}
})
}
}

View File

@ -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)
}

View File

@ -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"))
}
})
}
}

View File

@ -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)
}

View File

@ -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"))
}
})
}
}

View File

@ -1,4 +1,4 @@
package internal
package cmd
import (
"encoding/json"

View File

@ -1,4 +1,4 @@
package internal
package cmd
import (
"github.com/influxdata/influx-cli/v2/pkg/influxid"

22
internal/cmd/ping/ping.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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 = &params.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 {

View File

@ -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, &params))
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(), &params))
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, &params)
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(), &params)
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, &params)
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(), &params)
require.Equal(t, setup.ErrSetupCanceled, err)
}

View File

@ -1,4 +1,4 @@
package batcher
package write
import (
"bufio"

View File

@ -1,4 +1,4 @@
package batcher
package write
import (
"bufio"

View File

@ -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()

View File

@ -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
}

View File

@ -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())
}

View File

@ -1,4 +1,4 @@
package linereader
package write
import (
"compress/gzip"

View File

@ -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)

View File

@ -1,4 +1,4 @@
package throttler
package write
import (
"context"

View File

@ -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)

View File

@ -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
}

View File

@ -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, &params))
require.NoError(t, cli.Write(context.Background(), &params))
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, &params))
require.NoError(t, cli.Write(context.Background(), &params))
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, &params))
require.NoError(t, cli.Write(context.Background(), &params))
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())
}

View File

@ -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
}

View File

@ -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])
}
}