diff --git a/cmd/meroxa/builder/builder.go b/cmd/meroxa/builder/builder.go index f17da7ecc..07aff31e8 100644 --- a/cmd/meroxa/builder/builder.go +++ b/cmd/meroxa/builder/builder.go @@ -634,7 +634,7 @@ func buildCommandWithFeatureFlag(cmd *cobra.Command, c Command) { userFeatureFlags := global.Config.GetStringSlice(global.UserFeatureFlagsEnv) if !hasFeatureFlag(userFeatureFlags, flagRequired) { - return fmt.Errorf("your account does not have access to the %q feature."+ + return fmt.Errorf("your account does not have access to the %q feature. "+ "Reach out to support@meroxa.com for more information", flagRequired) } diff --git a/cmd/meroxa/root/environments/environments.go b/cmd/meroxa/root/environments/environments.go index 230a1304f..e84222ca6 100644 --- a/cmd/meroxa/root/environments/environments.go +++ b/cmd/meroxa/root/environments/environments.go @@ -17,8 +17,7 @@ limitations under the License. package environments import ( - "context" - "fmt" + "github.com/spf13/cobra" "github.com/meroxa/cli/cmd/meroxa/builder" "github.com/meroxa/cli/log" @@ -31,8 +30,8 @@ type Environments struct { var ( _ builder.CommandWithAliases = (*Environments)(nil) _ builder.CommandWithDocs = (*Environments)(nil) - _ builder.CommandWithExecute = (*Environments)(nil) _ builder.CommandWithFeatureFlag = (*Environments)(nil) + _ builder.CommandWithSubCommands = (*Environments)(nil) ) func (*Environments) Usage() string { @@ -57,7 +56,8 @@ func (e *Environments) Logger(logger log.Logger) { e.logger = logger } -func (e *Environments) Execute(ctx context.Context) error { - fmt.Println("Welcome to a new world") - return nil +func (*Environments) SubCommands() []*cobra.Command { + return []*cobra.Command{ + builder.BuildCobraCommand(&List{}), + } } diff --git a/cmd/meroxa/root/environments/list.go b/cmd/meroxa/root/environments/list.go new file mode 100644 index 000000000..339883216 --- /dev/null +++ b/cmd/meroxa/root/environments/list.go @@ -0,0 +1,84 @@ +/* +Copyright © 2021 Meroxa Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package environments + +import ( + "context" + + "github.com/meroxa/cli/cmd/meroxa/builder" + "github.com/meroxa/cli/log" + "github.com/meroxa/cli/utils" + "github.com/meroxa/meroxa-go" +) + +var ( + _ builder.CommandWithDocs = (*List)(nil) + _ builder.CommandWithClient = (*List)(nil) + _ builder.CommandWithLogger = (*List)(nil) + _ builder.CommandWithExecute = (*List)(nil) + _ builder.CommandWithAliases = (*List)(nil) + _ builder.CommandWithNoHeaders = (*List)(nil) +) + +type listEnvironmentsClient interface { + ListEnvironments(ctx context.Context) ([]*meroxa.Environment, error) +} + +type List struct { + client listEnvironmentsClient + logger log.Logger + hideHeaders bool +} + +func (l *List) Usage() string { + return "list" +} + +func (l *List) Docs() builder.Docs { + return builder.Docs{ + Short: "List environments", + } +} + +func (l *List) Aliases() []string { + return []string{"ls"} +} + +func (l *List) Execute(ctx context.Context) error { + var err error + environments, err := l.client.ListEnvironments(ctx) + if err != nil { + return err + } + + l.logger.JSON(ctx, environments) + l.logger.Info(ctx, utils.EnvironmentsTable(environments, l.hideHeaders)) + + return nil +} + +func (l *List) Logger(logger log.Logger) { + l.logger = logger +} + +func (l *List) Client(client *meroxa.Client) { + l.client = client +} + +func (l *List) HideHeaders(hide bool) { + l.hideHeaders = hide +} diff --git a/cmd/meroxa/root/environments/list_test.go b/cmd/meroxa/root/environments/list_test.go new file mode 100644 index 000000000..f4efb593e --- /dev/null +++ b/cmd/meroxa/root/environments/list_test.go @@ -0,0 +1,90 @@ +/* +Copyright © 2021 Meroxa Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package environments + +import ( + "context" + "encoding/json" + "reflect" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/meroxa/cli/log" + mock "github.com/meroxa/cli/mock-cmd" + "github.com/meroxa/cli/utils" + "github.com/meroxa/meroxa-go" +) + +func TestListEnvironmentsExecution(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + client := mock.NewMockListEnvironmentsClient(ctrl) + logger := log.NewTestLogger() + + ee := &meroxa.Environment{ + Type: "dedicated", + Name: "environment-1234", + Provider: "aws", + Region: "aws:us-east", + Status: meroxa.EnvironmentStatus{State: "provisioned"}, + UUID: "531428f7-4e86-4094-8514-d397d49026f7", + } + + environments := []*meroxa.Environment{ee} + + client. + EXPECT(). + ListEnvironments(ctx). + Return(environments, nil) + + l := &List{ + client: client, + logger: logger, + } + + err := l.Execute(ctx) + + if err != nil { + t.Fatalf("not expected error, got \"%s\"", err.Error()) + } + + gotLeveledOutput := logger.LeveledOutput() + wantLeveledOutput := utils.EnvironmentsTable(environments, false) + + if !strings.Contains(gotLeveledOutput, wantLeveledOutput) { + t.Fatalf("expected output:\n%s\ngot:\n%s", wantLeveledOutput, gotLeveledOutput) + } + + gotJSONOutput := logger.JSONOutput() + var gotEnvironments []meroxa.Environment + err = json.Unmarshal([]byte(gotJSONOutput), &gotEnvironments) + + var lp []meroxa.Environment + + for _, p := range environments { + lp = append(lp, *p) + } + + if err != nil { + t.Fatalf("not expected error, got %q", err.Error()) + } + + if !reflect.DeepEqual(gotEnvironments, lp) { + t.Fatalf("expected \"%v\", got \"%v\"", environments, gotEnvironments) + } +} diff --git a/go.mod b/go.mod index a028f11e3..5c2a65149 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/mux v1.7.3 github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-runewidth v0.0.10 // indirect - github.com/meroxa/meroxa-go v0.0.0-20210928081857-bcbce33ea9f4 + github.com/meroxa/meroxa-go v0.0.0-20211013160423-7447f282edb1 github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 github.com/rivo/uniseg v0.2.0 // indirect @@ -20,7 +20,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.8.1 golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect - golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f + golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 ) require ( diff --git a/go.sum b/go.sum index 83fa74099..8ae003789 100644 --- a/go.sum +++ b/go.sum @@ -210,8 +210,8 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/meroxa/meroxa-go v0.0.0-20210928081857-bcbce33ea9f4 h1:7jpUh3WZ9r3vZ0mYdCENg1/F15wkWi/AqkXmZYLsAPk= -github.com/meroxa/meroxa-go v0.0.0-20210928081857-bcbce33ea9f4/go.mod h1:gGKULnbPeFk//HW3XRxkZwZ3jgMuoMB+43EiGZ7QoGU= +github.com/meroxa/meroxa-go v0.0.0-20211013160423-7447f282edb1 h1:NNyXbCGgxkLkeV9GSVFoSGU1pu1+A78inUhLcooSbOM= +github.com/meroxa/meroxa-go v0.0.0-20211013160423-7447f282edb1/go.mod h1:gGKULnbPeFk//HW3XRxkZwZ3jgMuoMB+43EiGZ7QoGU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -388,8 +388,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/mock-cmd/list_environments.go b/mock-cmd/list_environments.go new file mode 100644 index 000000000..8557afe72 --- /dev/null +++ b/mock-cmd/list_environments.go @@ -0,0 +1,51 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: cmd/meroxa/root/environments/list.go + +// Package mock_environments is a generated GoMock package. +package mock_cmd + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + meroxa "github.com/meroxa/meroxa-go" +) + +// MockListEnvironmentsClient is a mock of listEnvironmentsClient interface. +type MockListEnvironmentsClient struct { + ctrl *gomock.Controller + recorder *MockListEnvironmentsClientMockRecorder +} + +// MockListEnvironmentsClientMockRecorder is the mock recorder for MockListEnvironmentsClient. +type MockListEnvironmentsClientMockRecorder struct { + mock *MockListEnvironmentsClient +} + +// NewMockListEnvironmentsClient creates a new mock instance. +func NewMockListEnvironmentsClient(ctrl *gomock.Controller) *MockListEnvironmentsClient { + mock := &MockListEnvironmentsClient{ctrl: ctrl} + mock.recorder = &MockListEnvironmentsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockListEnvironmentsClient) EXPECT() *MockListEnvironmentsClientMockRecorder { + return m.recorder +} + +// ListEnvironments mocks base method. +func (m *MockListEnvironmentsClient) ListEnvironments(ctx context.Context) ([]*meroxa.Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListEnvironments", ctx) + ret0, _ := ret[0].([]*meroxa.Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListEnvironments indicates an expected call of ListEnvironments. +func (mr *MockListEnvironmentsClientMockRecorder) ListEnvironments(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnvironments", reflect.TypeOf((*MockListEnvironmentsClient)(nil).ListEnvironments), ctx) +} diff --git a/utils/display.go b/utils/display.go index 47564ca39..561c6586c 100644 --- a/utils/display.go +++ b/utils/display.go @@ -361,6 +361,45 @@ func PrintPipelinesTable(pipelines []*meroxa.Pipeline, hideHeaders bool) { fmt.Println(PipelinesTable(pipelines, hideHeaders)) } +func EnvironmentsTable(environments []*meroxa.Environment, hideHeaders bool) string { + if len(environments) != 0 { + table := simpletable.New() + + if !hideHeaders { + table.Header = &simpletable.Header{ + Cells: []*simpletable.Cell{ + {Align: simpletable.AlignCenter, Text: "UUID"}, + {Align: simpletable.AlignCenter, Text: "NAME"}, + {Align: simpletable.AlignCenter, Text: "TYPE"}, + {Align: simpletable.AlignCenter, Text: "PROVIDER"}, + {Align: simpletable.AlignCenter, Text: "REGION"}, + {Align: simpletable.AlignCenter, Text: "STATE"}, + }, + } + } + + for _, p := range environments { + r := []*simpletable.Cell{ + {Align: simpletable.AlignRight, Text: p.UUID}, + {Align: simpletable.AlignCenter, Text: p.Name}, + {Align: simpletable.AlignCenter, Text: p.Type}, + {Align: simpletable.AlignCenter, Text: p.Provider}, + {Align: simpletable.AlignCenter, Text: p.Region}, + {Align: simpletable.AlignCenter, Text: p.Status.State}, + } + + table.Body.Cells = append(table.Body.Cells, r) + } + table.SetStyle(simpletable.StyleCompact) + return table.String() + } + return "" +} + +func PrintEnvironmentsTable(environments []*meroxa.Environment, hideHeaders bool) { + fmt.Println(EnvironmentsTable(environments, hideHeaders)) +} + func truncateString(oldString string, l int) string { str := oldString diff --git a/utils/display_test.go b/utils/display_test.go index 964877af1..ea3b6652d 100644 --- a/utils/display_test.go +++ b/utils/display_test.go @@ -139,7 +139,6 @@ func TestEmptyTables(t *testing.T) { } var emptyPipelinesList []*meroxa.Pipeline - out = CaptureOutput(func() { PrintPipelinesTable(emptyPipelinesList, true) }) @@ -147,6 +146,15 @@ func TestEmptyTables(t *testing.T) { if out != "\n" { t.Errorf("Output for pipelines should be blank") } + + var emptyEnvironmentsList []*meroxa.Environment + out = CaptureOutput(func() { + PrintEnvironmentsTable(emptyEnvironmentsList, true) + }) + + if out != "\n" { + t.Errorf("Output for pipelines should be blank") + } } func TestResourceTypesTable(t *testing.T) { types := []string{"postgres", "s3", "redshift", "mysql", "jdbc", "url", "mongodb"} @@ -447,6 +455,103 @@ func TestPipelinesTableWithoutHeaders(t *testing.T) { } } +func TestEnvironmentsTable(t *testing.T) { + e := &meroxa.Environment{ + Type: "dedicated", + Name: "environment-1234", + Provider: "aws", + Region: "aws:us-east", + Status: meroxa.EnvironmentStatus{State: "provisioned"}, + UUID: "531428f7-4e86-4094-8514-d397d49026f7", + } + + tests := map[string][]*meroxa.Environment{ + "Base": {e}, + } + + tableHeaders := []string{"ID", "NAME", "TYPE", "PROVIDER", "REGION", "STATE"} + + for name, environments := range tests { + t.Run(name, func(t *testing.T) { + out := CaptureOutput(func() { + PrintEnvironmentsTable(environments, false) + }) + + for _, header := range tableHeaders { + if !strings.Contains(out, header) { + t.Errorf("%s header is missing", header) + } + } + + if !strings.Contains(out, e.UUID) { + t.Errorf("%s, not found", e.UUID) + } + if !strings.Contains(out, e.Name) { + t.Errorf("%s, not found", e.Name) + } + if !strings.Contains(out, e.Type) { + t.Errorf("%s, not found", e.Type) + } + if !strings.Contains(out, e.Region) { + t.Errorf("%s, not found", e.Region) + } + if !strings.Contains(out, e.Status.State) { + t.Errorf("%s, not found", e.Status.State) + } + if !strings.Contains(out, e.UUID) { + t.Errorf("%s, not found", e.UUID) + } + + fmt.Println(out) + }) + } +} + +func TestEnvironmentsTableWithoutHeaders(t *testing.T) { + e := &meroxa.Environment{ + Type: "dedicated", + Name: "environment-1234", + Provider: "aws", + Region: "aws:us-east", + Status: meroxa.EnvironmentStatus{State: "provisioned"}, + UUID: "531428f7-4e86-4094-8514-d397d49026f7", + } + + var environments []*meroxa.Environment + tableHeaders := []string{"ID", "NAME", "TYPE", "PROVIDER", "REGION", "STATE"} + + environments = append(environments, e) + + out := CaptureOutput(func() { + PrintEnvironmentsTable(environments, true) + }) + + for _, header := range tableHeaders { + if strings.Contains(out, header) { + t.Errorf("%s header should not be displayed", header) + } + } + + if !strings.Contains(out, e.UUID) { + t.Errorf("%s, not found", e.UUID) + } + if !strings.Contains(out, e.Name) { + t.Errorf("%s, not found", e.Name) + } + if !strings.Contains(out, e.Type) { + t.Errorf("%s, not found", e.Type) + } + if !strings.Contains(out, e.Region) { + t.Errorf("%s, not found", e.Region) + } + if !strings.Contains(out, e.Status.State) { + t.Errorf("%s, not found", e.Status.State) + } + if !strings.Contains(out, e.UUID) { + t.Errorf("%s, not found", e.UUID) + } +} + func deepCopy(a, b interface{}) { byt, _ := json.Marshal(a) _ = json.Unmarshal(byt, b) diff --git a/vendor/github.com/meroxa/meroxa-go/environment.go b/vendor/github.com/meroxa/meroxa-go/environment.go new file mode 100644 index 000000000..812e6ea68 --- /dev/null +++ b/vendor/github.com/meroxa/meroxa-go/environment.go @@ -0,0 +1,49 @@ +package meroxa + +import ( + "context" + "encoding/json" + "net/http" + "time" +) + +const environmentsBasePath = "/v1/environments" + +type EnvironmentStatus struct { + State string `json:"state"` + Details string `json:"details,omitempty"` +} + +// Environment represents the Meroxa Environment type within the Meroxa API +type Environment struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Provider string `json:"provider"` + Region string `json:"region"` + Type string `json:"type"` + Configuration map[string]interface{} `json:"config,omitempty"` + Status EnvironmentStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ListEnvironments returns an array of Environments (scoped to the calling user) +func (c *Client) ListEnvironments(ctx context.Context) ([]*Environment, error) { + resp, err := c.MakeRequest(ctx, http.MethodGet, environmentsBasePath, nil, nil) + if err != nil { + return nil, err + } + + err = handleAPIErrors(resp) + if err != nil { + return nil, err + } + + var ee []*Environment + err = json.NewDecoder(resp.Body).Decode(&ee) + if err != nil { + return nil, err + } + + return ee, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 6a7e68b6a..860a50cc2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -66,7 +66,7 @@ github.com/mattn/go-isatty # github.com/mattn/go-runewidth v0.0.10 ## explicit; go 1.9 github.com/mattn/go-runewidth -# github.com/meroxa/meroxa-go v0.0.0-20210928081857-bcbce33ea9f4 +# github.com/meroxa/meroxa-go v0.0.0-20211013160423-7447f282edb1 ## explicit; go 1.17 github.com/meroxa/meroxa-go # github.com/mitchellh/mapstructure v1.4.1 @@ -120,7 +120,7 @@ github.com/subosito/gotenv ## explicit; go 1.17 golang.org/x/net/context golang.org/x/net/context/ctxhttp -# golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f +# golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 ## explicit; go 1.11 golang.org/x/oauth2 golang.org/x/oauth2/internal