diff --git a/cmd/influx/main.go b/cmd/influx/main.go index 68118b4..87eb1b5 100644 --- a/cmd/influx/main.go +++ b/cmd/influx/main.go @@ -40,6 +40,7 @@ var app = cli.App{ newBucketSchemaCmd(), newQueryCmd(), newConfigCmd(), + newOrgCmd(), }, } diff --git a/cmd/influx/org.go b/cmd/influx/org.go new file mode 100644 index 0000000..7d1e1ae --- /dev/null +++ b/cmd/influx/org.go @@ -0,0 +1,155 @@ +package main + +import ( + "github.com/influxdata/influx-cli/v2/internal/cmd/org" + "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" + "github.com/influxdata/influx-cli/v2/pkg/influxid" + "github.com/urfave/cli/v2" +) + +func newOrgCmd() *cli.Command { + return &cli.Command{ + Name: "org", + Aliases: []string{"organization"}, + Usage: "Organization management commands", + Subcommands: []*cli.Command{ + newOrgCreateCmd(), + newOrgDeleteCmd(), + newOrgListCmd(), + newOrgUpdateCmd(), + }, + } +} + +func newOrgCreateCmd() *cli.Command { + var params org.CreateParams + return &cli.Command{ + Name: "create", + Usage: "Create organization", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.StringFlag{ + Name: "name", + Usage: "Name to set on the new organization", + Aliases: []string{"n"}, + Required: true, + Destination: ¶ms.Name, + }, + &cli.StringFlag{ + Name: "description", + Usage: "Description to set on the new organization", + Aliases: []string{"d"}, + Destination: ¶ms.Description, + }, + ), + Action: func(ctx *cli.Context) error { + client := org.Client{ + CLI: getCLI(ctx), + OrganizationsApi: getAPI(ctx).OrganizationsApi, + } + return client.Create(ctx.Context, ¶ms) + }, + } +} + +func newOrgDeleteCmd() *cli.Command { + var id influxid.ID + return &cli.Command{ + Name: "delete", + Usage: "Delete organization", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.GenericFlag{ + Name: "id", + Usage: "The organization ID", + Aliases: []string{"i"}, + EnvVars: []string{"INFLUX_ORG_ID"}, + Value: &id, + }, + ), + Action: func(ctx *cli.Context) error { + client := org.Client{ + CLI: getCLI(ctx), + OrganizationsApi: getAPI(ctx).OrganizationsApi, + } + return client.Delete(ctx.Context, id) + }, + } +} + +func newOrgListCmd() *cli.Command { + var params org.ListParams + return &cli.Command{ + Name: "list", + Aliases: []string{"find", "ls"}, + Usage: "List organizations", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.StringFlag{ + Name: "name", + Usage: "The organization name", + Aliases: []string{"n"}, + EnvVars: []string{"INFLUX_ORG"}, + Destination: ¶ms.Name, + }, + &cli.GenericFlag{ + Name: "id", + Usage: "The organization ID", + Aliases: []string{"i"}, + EnvVars: []string{"INFLUX_ORG_ID"}, + Value: ¶ms.ID, + }, + ), + Action: func(ctx *cli.Context) error { + client := org.Client{ + CLI: getCLI(ctx), + OrganizationsApi: getAPI(ctx).OrganizationsApi, + } + return client.List(ctx.Context, ¶ms) + }, + } +} + +func newOrgUpdateCmd() *cli.Command { + var params org.UpdateParams + return &cli.Command{ + Name: "update", + Usage: "Update organization", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.GenericFlag{ + Name: "id", + Usage: "The organization ID", + Aliases: []string{"i"}, + EnvVars: []string{"INFLUX_ORG_ID"}, + Required: true, + Value: ¶ms.ID, + }, + &cli.StringFlag{ + Name: "name", + Usage: "New name to set on the organization", + Aliases: []string{"n"}, + EnvVars: []string{"INFLUX_ORG"}, + Destination: ¶ms.Name, + }, + &cli.StringFlag{ + Name: "description", + Usage: "New description to set on the organization", + Aliases: []string{"d"}, + EnvVars: []string{"INFLUX_ORG_DESCRIPTION"}, + Destination: ¶ms.Description, + }, + ), + Action: func(ctx *cli.Context) error { + client := org.Client{ + CLI: getCLI(ctx), + OrganizationsApi: getAPI(ctx).OrganizationsApi, + } + return client.Update(ctx.Context, ¶ms) + }, + } +} diff --git a/etc/generate-openapi.sh b/etc/generate-openapi.sh index f55724a..aba0c2e 100755 --- a/etc/generate-openapi.sh +++ b/etc/generate-openapi.sh @@ -10,7 +10,7 @@ declare -r MERGE_DOCKER_IMG=quay.io/influxdb/swagger-cli declare -r GENERATOR_DOCKER_IMG=openapitools/openapi-generator-cli:v5.1.0 # Clean up all the generated files in the target directory. -rm $(grep -Elr "${GENERATED_PATTERN}" "${API_DIR}") +rm -f $(grep -Elr "${GENERATED_PATTERN}" "${API_DIR}") # Merge all API contracts into a single file to drive codegen. docker run --rm -it -u "$(id -u):$(id -g)" \ diff --git a/internal/cmd/org/client.go b/internal/cmd/org/client.go new file mode 100644 index 0000000..195acdf --- /dev/null +++ b/internal/cmd/org/client.go @@ -0,0 +1,52 @@ +package org + +import ( + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" +) + +type Client struct { + cmd.CLI + api.OrganizationsApi +} + +type printOrgOpts struct { + org *api.Organization + orgs []api.Organization + deleted bool +} + +func (c Client) printOrgs(opts printOrgOpts) error { + if c.PrintAsJSON { + var v interface{} + if opts.org != nil { + v = opts.org + } else { + v = opts.orgs + } + return c.PrintJSON(v) + } + + headers := []string{"ID", "Name"} + if opts.deleted { + headers = append(headers, "Deleted") + } + + if opts.org != nil { + opts.orgs = append(opts.orgs, *opts.org) + } + + var rows []map[string]interface{} + for _, o := range opts.orgs { + row := map[string]interface{}{ + "ID": o.GetId(), + "Name": o.GetName(), + } + if opts.deleted { + row["Deleted"] = true + } + rows = append(rows, row) + } + + return c.PrintTable(headers, rows...) +} diff --git a/internal/cmd/org/org.go b/internal/cmd/org/org.go new file mode 100644 index 0000000..415f6c2 --- /dev/null +++ b/internal/cmd/org/org.go @@ -0,0 +1,84 @@ +package org + +import ( + "context" + "fmt" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/pkg/influxid" +) + +type CreateParams struct { + Name string + Description string +} + +func (c Client) Create(ctx context.Context, params *CreateParams) error { + body := api.PostOrganizationRequest{Name: params.Name} + if params.Description != "" { + body.Description = ¶ms.Description + } + res, err := c.PostOrgs(ctx).PostOrganizationRequest(body).Execute() + if err != nil { + return fmt.Errorf("failed to create org %q: %w", params.Name, err) + } + return c.printOrgs(printOrgOpts{org: &res}) +} + +func (c Client) Delete(ctx context.Context, id influxid.ID) error { + org, err := c.GetOrgsID(ctx, id.String()).Execute() + if err != nil { + return fmt.Errorf("org %q not found: %w", id, err) + + } + if err := c.DeleteOrgsID(ctx, id.String()).Execute(); err != nil { + return fmt.Errorf("failed to delete org %q: %w", id, err) + } + return c.printOrgs(printOrgOpts{org: &org, deleted: true}) +} + +type ListParams struct { + Name string + ID influxid.ID +} + +func (c Client) List(ctx context.Context, params *ListParams) error { + req := c.GetOrgs(ctx) + if params.Name != "" { + req = req.Org(params.Name) + } + if params.ID.Valid() { + req = req.OrgID(params.ID.String()) + } + orgs, err := req.Execute() + if err != nil { + return fmt.Errorf("failed to list orgs: %w", err) + } + printOpts := printOrgOpts{} + if orgs.Orgs != nil { + printOpts.orgs = *orgs.Orgs + } + return c.printOrgs(printOpts) +} + +type UpdateParams struct { + ID influxid.ID + Name string + Description string +} + +func (c Client) Update(ctx context.Context, params *UpdateParams) error { + body := api.PatchOrganizationRequest{} + if params.Name != "" { + body.Name = ¶ms.Name + } + if params.Description != "" { + body.Description = ¶ms.Description + } + + res, err := c.PatchOrgsID(ctx, params.ID.String()).PatchOrganizationRequest(body).Execute() + if err != nil { + return fmt.Errorf("failed to update org %q: %w", params.ID, err) + } + return c.printOrgs(printOrgOpts{org: &res}) +} diff --git a/internal/cmd/org/org_test.go b/internal/cmd/org/org_test.go new file mode 100644 index 0000000..602b3d7 --- /dev/null +++ b/internal/cmd/org/org_test.go @@ -0,0 +1,297 @@ +package org_test + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/org" + "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/influxdata/influx-cli/v2/internal/testutils" + "github.com/influxdata/influx-cli/v2/pkg/influxid" + "github.com/stretchr/testify/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +var id, _ = influxid.IDFromString("1111111111111111") + +func TestClient_Create(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params org.CreateParams + registerExpectations func(*testing.T, *mock.MockOrganizationsApi) + outLine string + }{ + { + name: "name only", + params: org.CreateParams{ + Name: "my-org", + }, + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().PostOrgs(gomock.Any()).Return(api.ApiPostOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().PostOrgsExecute(tmock.MatchedBy(func(in api.ApiPostOrgsRequest) bool { + body := in.GetPostOrganizationRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "my-org", body.GetName()) && + assert.Nil(t, body.Description) + })).Return(api.Organization{Name: "my-org", Id: api.PtrString("123")}, nil) + }, + outLine: `123\s+my-org`, + }, + { + name: "with description", + params: org.CreateParams{ + Name: "my-org", + Description: "my cool new org", + }, + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().PostOrgs(gomock.Any()).Return(api.ApiPostOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().PostOrgsExecute(tmock.MatchedBy(func(in api.ApiPostOrgsRequest) bool { + body := in.GetPostOrganizationRequest() + return assert.NotNil(t, body) && + assert.Equal(t, "my-org", body.GetName()) && + assert.Equal(t, "my cool new org", body.GetDescription()) + })).Return(api.Organization{ + Name: "my-org", + Id: api.PtrString("123"), + Description: api.PtrString("my cool new org"), + }, nil) + }, + outLine: `123\s+my-org`, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + api := mock.NewMockOrganizationsApi(ctrl) + if tc.registerExpectations != nil { + tc.registerExpectations(t, api) + } + + stdout := bytes.Buffer{} + stdio := mock.NewMockStdIO(ctrl) + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(stdout.Write).AnyTimes() + + cli := org.Client{CLI: cmd.CLI{StdIO: stdio}, OrganizationsApi: api} + require.NoError(t, cli.Create(context.Background(), &tc.params)) + testutils.MatchLines(t, []string{`ID\s+Name`, tc.outLine}, strings.Split(stdout.String(), "\n")) + }) + } +} + +func TestClient_Delete(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + notFound bool + }{ + { + name: "delete existing", + }, + { + name: "delete non-existing", + notFound: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + orgApi := mock.NewMockOrganizationsApi(ctrl) + + stdout := bytes.Buffer{} + stdio := mock.NewMockStdIO(ctrl) + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(stdout.Write).AnyTimes() + + cli := org.Client{CLI: cmd.CLI{StdIO: stdio}, OrganizationsApi: orgApi} + + getReq := api.ApiGetOrgsIDRequest{ApiService: orgApi}.OrgID(id.String()) + orgApi.EXPECT().GetOrgsID(gomock.Any(), gomock.Eq(id.String())).Return(getReq) + orgApi.EXPECT().GetOrgsIDExecute(gomock.Eq(getReq)). + DoAndReturn(func(request api.ApiGetOrgsIDRequest) (api.Organization, error) { + if tc.notFound { + return api.Organization{}, &api.Error{Code: api.ERRORCODE_NOT_FOUND} + } + return api.Organization{Id: api.PtrString(id.String()), Name: "my-org"}, nil + }) + + if tc.notFound { + require.Error(t, cli.Delete(context.Background(), id)) + require.Empty(t, stdout.String()) + return + } + + delReq := api.ApiDeleteOrgsIDRequest{ApiService: orgApi}.OrgID(id.String()) + orgApi.EXPECT().DeleteOrgsID(gomock.Any(), gomock.Eq(id.String())).Return(delReq) + orgApi.EXPECT().DeleteOrgsIDExecute(gomock.Eq(delReq)).Return(nil) + + require.NoError(t, cli.Delete(context.Background(), id)) + testutils.MatchLines(t, []string{ + `ID\s+Name\s+Deleted`, + `1111111111111111\s+my-org\s+true`, + }, strings.Split(stdout.String(), "\n")) + }) + } +} + +func TestClient_List(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params org.ListParams + registerExpectations func(*testing.T, *mock.MockOrganizationsApi) + outLines []string + }{ + { + name: "no results", + registerExpectations: 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) + }, + }, + { + name: "many results", + registerExpectations: 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{ + Orgs: &[]api.Organization{ + {Id: api.PtrString("123"), Name: "org1"}, + {Id: api.PtrString("456"), Name: "org2"}, + }, + }, nil) + }, + outLines: []string{`123\s+org1`, `456\s+org2`}, + }, + { + name: "by name", + params: org.ListParams{Name: "org1"}, + registerExpectations: 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, "org1", *in.GetOrg()) && assert.Nil(t, in.GetOrgID()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{ + {Id: api.PtrString("123"), Name: "org1"}, + }, + }, nil) + }, + outLines: []string{`123\s+org1`}, + }, + { + name: "by ID", + params: org.ListParams{ID: id}, + registerExpectations: 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.Nil(t, in.GetOrg()) && assert.Equal(t, id.String(), *in.GetOrgID()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{ + {Id: api.PtrString(id.String()), Name: "org3"}, + }, + }, nil) + }, + outLines: []string{fmt.Sprintf(`%s\s+org3`, id)}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + orgApi := mock.NewMockOrganizationsApi(ctrl) + if tc.registerExpectations != nil { + tc.registerExpectations(t, orgApi) + } + stdout := bytes.Buffer{} + stdio := mock.NewMockStdIO(ctrl) + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(stdout.Write).AnyTimes() + + cli := org.Client{CLI: cmd.CLI{StdIO: stdio}, OrganizationsApi: orgApi} + require.NoError(t, cli.List(context.Background(), &tc.params)) + testutils.MatchLines(t, append([]string{`ID\s+Name`}, tc.outLines...), strings.Split(stdout.String(), "\n")) + }) + } +} + +func TestClient_Update(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params org.UpdateParams + registerExpectations func(*testing.T, *mock.MockOrganizationsApi) + outLine string + }{ + { + name: "name", + params: org.UpdateParams{ID: id, Name: "my-org"}, + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().PatchOrgsID(gomock.Any(), gomock.Eq(id.String())). + Return(api.ApiPatchOrgsIDRequest{ApiService: orgApi}.OrgID(id.String())) + orgApi.EXPECT().PatchOrgsIDExecute(tmock.MatchedBy(func(in api.ApiPatchOrgsIDRequest) bool { + body := in.GetPatchOrganizationRequest() + return assert.Equal(t, id.String(), in.GetOrgID()) && + assert.NotNil(t, body) && + assert.Equal(t, "my-org", body.GetName()) && + assert.Nil(t, body.Description) + })).Return(api.Organization{Id: api.PtrString(id.String()), Name: "my-org"}, nil) + }, + outLine: fmt.Sprintf(`%s\s+my-org`, id.String()), + }, + { + name: "description", + params: org.UpdateParams{ID: id, Description: "my cool org"}, + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().PatchOrgsID(gomock.Any(), gomock.Eq(id.String())). + Return(api.ApiPatchOrgsIDRequest{ApiService: orgApi}.OrgID(id.String())) + orgApi.EXPECT().PatchOrgsIDExecute(tmock.MatchedBy(func(in api.ApiPatchOrgsIDRequest) bool { + body := in.GetPatchOrganizationRequest() + return assert.Equal(t, id.String(), in.GetOrgID()) && + assert.NotNil(t, body) && + assert.Nil(t, body.Name) && + assert.Equal(t, "my cool org", body.GetDescription()) + })).Return(api.Organization{Id: api.PtrString(id.String()), Name: "my-org"}, nil) + }, + outLine: fmt.Sprintf(`%s\s+my-org`, id.String()), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + orgApi := mock.NewMockOrganizationsApi(ctrl) + if tc.registerExpectations != nil { + tc.registerExpectations(t, orgApi) + } + stdout := bytes.Buffer{} + stdio := mock.NewMockStdIO(ctrl) + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(stdout.Write).AnyTimes() + + cli := org.Client{CLI: cmd.CLI{StdIO: stdio}, OrganizationsApi: orgApi} + require.NoError(t, cli.Update(context.Background(), &tc.params)) + testutils.MatchLines(t, []string{`ID\s+Name`, tc.outLine}, strings.Split(stdout.String(), "\n")) + }) + } +}