From a5e8383da10d4010390c77fa219805ac053c96e3 Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Fri, 30 Apr 2021 18:05:38 +0200 Subject: [PATCH 01/10] rpk/cloud: cloud api client interface --- src/go/rpk/pkg/vcloud/yak/api.go | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/go/rpk/pkg/vcloud/yak/api.go diff --git a/src/go/rpk/pkg/vcloud/yak/api.go b/src/go/rpk/pkg/vcloud/yak/api.go new file mode 100644 index 000000000000..81e8ce28cfab --- /dev/null +++ b/src/go/rpk/pkg/vcloud/yak/api.go @@ -0,0 +1,40 @@ +// Copyright 2021 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package yak + +import ( + "errors" + "fmt" +) +type CloudApiClient interface { + // GetNamespaces returns list of all namespaces available in the user's + // organization. Returns `ErrLoginTokenMissing` if token cannot be + // retrieved. Returns `ErrNotAuthorized` if user is not authorized to list + // namespaces. + GetNamespaces() ([]*Namespace, error) +} + +type Namespace struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + ClusterIds []string `json:"clusterIds,omitempty"` +} + +type ErrLoginTokenMissing struct { + InnerError error +} + +func (e ErrLoginTokenMissing) Error() string { + return fmt.Sprintf("Error retrieving login token: %v", e.InnerError) +} + +var ( + ErrNotAuthorized = errors.New("User is not authorized to view this resource") +) From e5dc93d9c556c80d08eb1053d0d66c76756f9080 Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Fri, 30 Apr 2021 18:05:59 +0200 Subject: [PATCH 02/10] rpk/cloud: cloud api client implementation --- src/go/rpk/pkg/vcloud/yak/client.go | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/go/rpk/pkg/vcloud/yak/client.go diff --git a/src/go/rpk/pkg/vcloud/yak/client.go b/src/go/rpk/pkg/vcloud/yak/client.go new file mode 100644 index 000000000000..6b5f4c411184 --- /dev/null +++ b/src/go/rpk/pkg/vcloud/yak/client.go @@ -0,0 +1,75 @@ +// Copyright 2021 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package yak + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + log "github.com/sirupsen/logrus" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/config" +) + +type YakClient struct { + conf config.ConfigReaderWriter +} + +const ( + namespaceRoute = "namespace" + + // TODO(av) make this configurable and point it to production + DefaultYakUrl = "https://backend.dev.vectorized.cloud" +) + +func NewYakClient(conf config.ConfigReaderWriter) CloudApiClient { + return &YakClient{ + conf: conf, + } +} + +func (yc *YakClient) GetNamespaces() ([]*Namespace, error) { + token, err := yc.conf.ReadToken() + if err != nil { + return nil, ErrLoginTokenMissing{err} + } + url := fmt.Sprintf("%s/%s", DefaultYakUrl, namespaceRoute) + req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{})) + if err != nil { + return nil, fmt.Errorf("error creating new request. %w", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error calling api: %w", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + // targetting HTTP codes 401, 403 which are used by yak + if resp.StatusCode > 400 && resp.StatusCode < 404 { + log.Debug(body) + return nil, ErrNotAuthorized + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("error retrieving resource, http code %d. %s", resp.StatusCode, body) + } + var namespaces []*Namespace + err = json.Unmarshal(body, &namespaces) + if err != nil { + return nil, fmt.Errorf("error unmarshaling response. %w", err) + } + return namespaces, nil +} From 33818627357a507cbbf538c9bab5848efa6c633a Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Mon, 3 May 2021 10:09:11 +0200 Subject: [PATCH 03/10] rpk/cloud: duplicate ui table view This is to keep vcloud independent of rpk. This just duplicates the current rpk table. --- src/go/rpk/pkg/vcloud/ui/tables.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/go/rpk/pkg/vcloud/ui/tables.go diff --git a/src/go/rpk/pkg/vcloud/ui/tables.go b/src/go/rpk/pkg/vcloud/ui/tables.go new file mode 100644 index 000000000000..3b8c963eddbd --- /dev/null +++ b/src/go/rpk/pkg/vcloud/ui/tables.go @@ -0,0 +1,28 @@ +// Copyright 2020 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package ui + +import ( + "io" + + "github.com/olekukonko/tablewriter" +) + +func NewVcloudTable(writer io.Writer) *tablewriter.Table { + table := tablewriter.NewWriter(writer) + table.SetBorder(false) + table.SetColumnSeparator("") + table.SetHeaderLine(false) + table.SetColWidth(80) + table.SetAutoWrapText(true) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + return table +} From 94bc4c68dce6b9fc6c4ed98ac85fb9a32627c2ce Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Fri, 30 Apr 2021 18:06:31 +0200 Subject: [PATCH 04/10] rpk/cloud: command to get list of namespaces --- src/go/rpk/pkg/cli/cmd/cloud/namespaces.go | 63 +++++++++++++ .../rpk/pkg/cli/cmd/cloud/namespaces_test.go | 90 +++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/go/rpk/pkg/cli/cmd/cloud/namespaces.go create mode 100644 src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go diff --git a/src/go/rpk/pkg/cli/cmd/cloud/namespaces.go b/src/go/rpk/pkg/cli/cmd/cloud/namespaces.go new file mode 100644 index 000000000000..c2e57222f4db --- /dev/null +++ b/src/go/rpk/pkg/cli/cmd/cloud/namespaces.go @@ -0,0 +1,63 @@ +// Copyright 2021 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package cloud + +import ( + "fmt" + "io" + + "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/config" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/ui" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/yak" +) + +func NewNamespacesCommand(fs afero.Fs) *cobra.Command { + return &cobra.Command{ + Use: "namespaces", + Aliases: []string{"ns"}, + Short: "Get namespaces in your vectorized cloud", + Long: `List namespaces that you have created in your vectorized cloud organization.`, + RunE: func(cmd *cobra.Command, args []string) error { + yakClient := yak.NewYakClient(config.NewVCloudConfigReaderWriter(fs)) + return GetNamespaces(yakClient, logrus.StandardLogger().Out) + }, + } +} + +func GetNamespaces(c yak.CloudApiClient, out io.Writer) error { + ns, err := c.GetNamespaces() + if _, ok := err.(yak.ErrLoginTokenMissing); ok { + log.Info("Please run `rpk cloud login` first. ") + return err + } + if err != nil { + return err + } + + printFormatted(ns, out) + return nil +} + +func printFormatted(ns []*yak.Namespace, out io.Writer) { + t := ui.NewVcloudTable(out) + t.SetHeader([]string{"id", "name", "clusters"}) + for _, n := range ns { + t.Append([]string{ + n.Id, + n.Name, + fmt.Sprint(len(n.ClusterIds)), + }) + } + t.Render() +} diff --git a/src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go b/src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go new file mode 100644 index 000000000000..41a6bb0b7945 --- /dev/null +++ b/src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go @@ -0,0 +1,90 @@ +// Copyright 2021 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package cloud_test + +import ( + "bytes" + "errors" + "strings" + "testing" + + "github.com/sirupsen/logrus" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/cli/cmd/cloud" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/yak" +) + +func TestNamespaces(t *testing.T) { + tests := []struct { + name string + client yak.CloudApiClient + expectedOutput []string + expectedError string + }{ + { + name: "success", + client: &mockYakClient{}, + expectedOutput: []string{"test", "2"}, + expectedError: "", + }, + { + name: "not logged in", + client: &erroredYakClient{yak.ErrLoginTokenMissing{errors.New("inner")}}, + expectedOutput: []string{"rpk cloud login"}, + expectedError: "retrieving login token", + }, + { + name: "generic client error", + client: &erroredYakClient{errors.New("other error")}, + expectedOutput: []string{}, + expectedError: "other error", + }, + } + + for _, tt := range tests { + var buf bytes.Buffer + logrus.SetOutput(&buf) + err := cloud.GetNamespaces(tt.client, &buf) + if len(tt.expectedOutput) > 0 { + for _, s := range tt.expectedOutput { + if !strings.Contains(buf.String(), s) { + t.Errorf("%s: expecting string %s in output %s", tt.name, s, buf.String()) + } + } + } + if tt.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("%s: expecting error %s got %v", tt.name, tt.expectedError, err) + } + } + } +} + +// Yak client returning fixed mocked responses +type mockYakClient struct { +} + +func (yc *mockYakClient) GetNamespaces() ([]*yak.Namespace, error) { + return []*yak.Namespace{ + { + Id: "test", + Name: "test", + ClusterIds: []string{"1", "2"}, + }, + }, nil +} + +// Yak client returning error provided on creation +type erroredYakClient struct { + err error +} + +func (yc *erroredYakClient) GetNamespaces() ([]*yak.Namespace, error) { + return nil, yc.err +} From 532f1f32a8e036de9d802c14939de357a2d4ee3d Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Fri, 30 Apr 2021 18:06:59 +0200 Subject: [PATCH 05/10] rpk/cloud: generic get command introduced --- src/go/rpk/pkg/cli/cmd/cloud/get.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/go/rpk/pkg/cli/cmd/cloud/get.go diff --git a/src/go/rpk/pkg/cli/cmd/cloud/get.go b/src/go/rpk/pkg/cli/cmd/cloud/get.go new file mode 100644 index 000000000000..17a9eeedbda5 --- /dev/null +++ b/src/go/rpk/pkg/cli/cmd/cloud/get.go @@ -0,0 +1,27 @@ +// Copyright 2021 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package cloud + +import ( + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func NewGetCommand(fs afero.Fs) *cobra.Command { + command := &cobra.Command{ + Use: "get", + Short: "Get resource from Vectorized cloud", + Hidden: true, + } + + command.AddCommand(NewNamespacesCommand(fs)) + + return command +} From e3c1ca79bb93e1733b1743a262c8ade8cf44c1bd Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Fri, 30 Apr 2021 18:07:24 +0200 Subject: [PATCH 06/10] rpk/cloud: cloud command pointing to get command --- src/go/rpk/pkg/cli/cmd/cloud.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/go/rpk/pkg/cli/cmd/cloud.go b/src/go/rpk/pkg/cli/cmd/cloud.go index 662c2c2e56bf..8700a19fe882 100644 --- a/src/go/rpk/pkg/cli/cmd/cloud.go +++ b/src/go/rpk/pkg/cli/cmd/cloud.go @@ -24,6 +24,7 @@ func NewCloudCommand(fs afero.Fs) *cobra.Command { command.AddCommand(cloud.NewLoginCommand(fs)) command.AddCommand(cloud.NewLogoutCommand(fs)) + command.AddCommand(cloud.NewGetCommand(fs)) return command } From b2350c4355e1f99d55fdbb53ab88b7bfe8ac2963 Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Mon, 3 May 2021 13:35:48 +0200 Subject: [PATCH 07/10] rpk/cloud: extract common logic in yak client --- src/go/rpk/pkg/vcloud/yak/api.go | 1 + src/go/rpk/pkg/vcloud/yak/client.go | 46 +++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/go/rpk/pkg/vcloud/yak/api.go b/src/go/rpk/pkg/vcloud/yak/api.go index 81e8ce28cfab..7714cc2ad12d 100644 --- a/src/go/rpk/pkg/vcloud/yak/api.go +++ b/src/go/rpk/pkg/vcloud/yak/api.go @@ -13,6 +13,7 @@ import ( "errors" "fmt" ) + type CloudApiClient interface { // GetNamespaces returns list of all namespaces available in the user's // organization. Returns `ErrLoginTokenMissing` if token cannot be diff --git a/src/go/rpk/pkg/vcloud/yak/client.go b/src/go/rpk/pkg/vcloud/yak/client.go index 6b5f4c411184..d8432d238dd8 100644 --- a/src/go/rpk/pkg/vcloud/yak/client.go +++ b/src/go/rpk/pkg/vcloud/yak/client.go @@ -43,33 +43,53 @@ func (yc *YakClient) GetNamespaces() ([]*Namespace, error) { return nil, ErrLoginTokenMissing{err} } url := fmt.Sprintf("%s/%s", DefaultYakUrl, namespaceRoute) - req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{})) + _, body, err := yc.get(token, url) if err != nil { - return nil, fmt.Errorf("error creating new request. %w", err) + return nil, fmt.Errorf("error calling api: %w", err) + } + + var namespaces []*Namespace + err = json.Unmarshal(body, &namespaces) + if err != nil { + return nil, fmt.Errorf("error unmarshaling response. %w", err) + } + return namespaces, nil +} + +func (yc *YakClient) get(token, url string) (*http.Response, []byte, error) { + log.Debugf("Calling yak api on url %s", url) + req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer(nil)) + if err != nil { + return nil, nil, fmt.Errorf("error creating new request. %w", err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, fmt.Errorf("error calling api: %w", err) + return nil, nil, err } + defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("error reading response body: %w", err) + return nil, nil, fmt.Errorf("error reading response body: %w", err) + } + err = yc.handleErrResponseCodes(resp, body) + if err != nil { + return resp, body, err } + return resp, body, nil +} +func (yc *YakClient) handleErrResponseCodes( + resp *http.Response, body []byte, +) error { // targetting HTTP codes 401, 403 which are used by yak if resp.StatusCode > 400 && resp.StatusCode < 404 { - log.Debug(body) - return nil, ErrNotAuthorized + log.Debug(string(body)) + return ErrNotAuthorized } if resp.StatusCode != 200 { - return nil, fmt.Errorf("error retrieving resource, http code %d. %s", resp.StatusCode, body) + return fmt.Errorf("error retrieving resource, http code %d. %s", resp.StatusCode, body) } - var namespaces []*Namespace - err = json.Unmarshal(body, &namespaces) - if err != nil { - return nil, fmt.Errorf("error unmarshaling response. %w", err) - } - return namespaces, nil + return nil } From 53fd012083e746bf03bdb162882d4329f339e5d1 Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Mon, 3 May 2021 13:50:19 +0200 Subject: [PATCH 08/10] rpk/cloud: add client method to retrieve clusters --- .../rpk/pkg/cli/cmd/cloud/namespaces_test.go | 18 +++++++++ src/go/rpk/pkg/vcloud/yak/api.go | 24 +++++++++++ src/go/rpk/pkg/vcloud/yak/client.go | 40 +++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go b/src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go index 41a6bb0b7945..25612f987512 100644 --- a/src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go +++ b/src/go/rpk/pkg/cli/cmd/cloud/namespaces_test.go @@ -80,6 +80,18 @@ func (yc *mockYakClient) GetNamespaces() ([]*yak.Namespace, error) { }, nil } +func (yc *mockYakClient) GetClusters( + namespaceName string, +) ([]*yak.Cluster, error) { + return []*yak.Cluster{ + { + Id: "notready", + Name: "notready", + Ready: false, + }, + }, nil +} + // Yak client returning error provided on creation type erroredYakClient struct { err error @@ -88,3 +100,9 @@ type erroredYakClient struct { func (yc *erroredYakClient) GetNamespaces() ([]*yak.Namespace, error) { return nil, yc.err } + +func (yc *erroredYakClient) GetClusters( + namespaceName string, +) ([]*yak.Cluster, error) { + return nil, yc.err +} diff --git a/src/go/rpk/pkg/vcloud/yak/api.go b/src/go/rpk/pkg/vcloud/yak/api.go index 7714cc2ad12d..1f28a3e8e958 100644 --- a/src/go/rpk/pkg/vcloud/yak/api.go +++ b/src/go/rpk/pkg/vcloud/yak/api.go @@ -20,6 +20,12 @@ type CloudApiClient interface { // retrieved. Returns `ErrNotAuthorized` if user is not authorized to list // namespaces. GetNamespaces() ([]*Namespace, error) + // GetClusters lists all redpanda clusters available in given namespace. + // Returns `ErrLoginTokenMissing` if token cannot be retrieved. Returns + // `ErrNotAuthorized` if user is not authorized to list clusters. Returns + // `ErrNamespaceDoesNotExists` if namespace of given name was not + // found. + GetClusters(namespaceName string) ([]*Cluster, error) } type Namespace struct { @@ -28,6 +34,16 @@ type Namespace struct { ClusterIds []string `json:"clusterIds,omitempty"` } +// Cluster is definition of redpanda cluster +type Cluster struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Hosts []string `json:"hosts,omitempty"` + Ready bool `json:"ready,omitempty"` + Provider string `json:"provider,omitempty"` + Region string `json:"region,omitempty"` +} + type ErrLoginTokenMissing struct { InnerError error } @@ -36,6 +52,14 @@ func (e ErrLoginTokenMissing) Error() string { return fmt.Sprintf("Error retrieving login token: %v", e.InnerError) } +type ErrNamespaceDoesNotExist struct { + Name string +} + +func (e ErrNamespaceDoesNotExist) Error() string { + return fmt.Sprintf("Namespace %s does not exist", e.Name) +} + var ( ErrNotAuthorized = errors.New("User is not authorized to view this resource") ) diff --git a/src/go/rpk/pkg/vcloud/yak/client.go b/src/go/rpk/pkg/vcloud/yak/client.go index d8432d238dd8..986720e170b8 100644 --- a/src/go/rpk/pkg/vcloud/yak/client.go +++ b/src/go/rpk/pkg/vcloud/yak/client.go @@ -26,6 +26,8 @@ type YakClient struct { const ( namespaceRoute = "namespace" + // requires namespaceId + getClustersRouteTemplate = namespaceRoute + "/%s/cluster" // TODO(av) make this configurable and point it to production DefaultYakUrl = "https://backend.dev.vectorized.cloud" @@ -56,6 +58,31 @@ func (yc *YakClient) GetNamespaces() ([]*Namespace, error) { return namespaces, nil } +func (yc *YakClient) GetClusters(namespaceName string) ([]*Cluster, error) { + token, err := yc.conf.ReadToken() + if err != nil { + return nil, ErrLoginTokenMissing{err} + } + // map namespace name to namespace id + namespaceId, err := yc.getNamespaceId(namespaceName) + if err != nil { + return nil, err + } + // retrieve clusters + url := fmt.Sprintf("%s/%s", DefaultYakUrl, fmt.Sprintf(getClustersRouteTemplate, namespaceId)) + _, body, err := yc.get(token, url) + if err != nil { + return nil, fmt.Errorf("error calling api: %w", err) + } + + var clusters []*Cluster + err = json.Unmarshal(body, &clusters) + if err != nil { + return nil, fmt.Errorf("error unmarshaling response. %w", err) + } + return clusters, nil +} + func (yc *YakClient) get(token, url string) (*http.Response, []byte, error) { log.Debugf("Calling yak api on url %s", url) req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer(nil)) @@ -93,3 +120,16 @@ func (yc *YakClient) handleErrResponseCodes( } return nil } + +func (yc *YakClient) getNamespaceId(namespaceName string) (string, error) { + ns, err := yc.GetNamespaces() + if err != nil { + return "", err + } + for _, n := range ns { + if n.Name == namespaceName { + return n.Id, nil + } + } + return "", ErrNamespaceDoesNotExist{namespaceName} +} From 0a9e16b265442380046c382a28004c42b93170bb Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Mon, 3 May 2021 13:50:48 +0200 Subject: [PATCH 09/10] rpk/cloud: introduce clusters command --- src/go/rpk/pkg/cli/cmd/cloud/clusters.go | 81 +++++++++++++++++++ src/go/rpk/pkg/cli/cmd/cloud/clusters_test.go | 67 +++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 src/go/rpk/pkg/cli/cmd/cloud/clusters.go create mode 100644 src/go/rpk/pkg/cli/cmd/cloud/clusters_test.go diff --git a/src/go/rpk/pkg/cli/cmd/cloud/clusters.go b/src/go/rpk/pkg/cli/cmd/cloud/clusters.go new file mode 100644 index 000000000000..2d2a50d6a4f5 --- /dev/null +++ b/src/go/rpk/pkg/cli/cmd/cloud/clusters.go @@ -0,0 +1,81 @@ +// Copyright 2021 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package cloud + +import ( + "errors" + "io" + "strconv" + + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/config" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/ui" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/yak" +) + +func NewClustersCommand(fs afero.Fs) *cobra.Command { + var ( + namespaceName string + ) + command := &cobra.Command{ + Use: "clusters", + Short: "Get clusters in given namespace", + Long: `List clusters that you have created in your namespace in your vectorized cloud organization.`, + RunE: func(cmd *cobra.Command, args []string) error { + if namespaceName == "" { + return errors.New("please provide --namespace flag") + } + yakClient := yak.NewYakClient(config.NewVCloudConfigReaderWriter(fs)) + return GetClusters(yakClient, log.StandardLogger().Out, namespaceName) + }, + } + + command.Flags().StringVarP( + &namespaceName, + "namespace", + "n", + "", + "Namespace name from your vectorized cloud organization", + ) + + return command +} + +func GetClusters( + c yak.CloudApiClient, out io.Writer, namespaceName string, +) error { + clusters, err := c.GetClusters(namespaceName) + if _, ok := err.(yak.ErrLoginTokenMissing); ok { + log.Info("Please run `rpk cloud login` first. ") + return err + } + + if err != nil { + return err + } + + printFormattedClusters(clusters, out) + return nil +} + +func printFormattedClusters(clusters []*yak.Cluster, out io.Writer) { + t := ui.NewVcloudTable(out) + t.SetHeader([]string{"id", "name", "ready"}) + for _, c := range clusters { + t.Append([]string{ + c.Id, + c.Name, + strconv.FormatBool(c.Ready), + }) + } + t.Render() +} diff --git a/src/go/rpk/pkg/cli/cmd/cloud/clusters_test.go b/src/go/rpk/pkg/cli/cmd/cloud/clusters_test.go new file mode 100644 index 000000000000..80a0b9cf0a17 --- /dev/null +++ b/src/go/rpk/pkg/cli/cmd/cloud/clusters_test.go @@ -0,0 +1,67 @@ +// Copyright 2021 Vectorized, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package cloud_test + +import ( + "bytes" + "errors" + "strings" + "testing" + + "github.com/sirupsen/logrus" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/cli/cmd/cloud" + "github.com/vectorizedio/redpanda/src/go/rpk/pkg/vcloud/yak" +) + +func TestClusters(t *testing.T) { + tests := []struct { + name string + client yak.CloudApiClient + expectedOutput []string + expectedError string + }{ + { + "success", + &mockYakClient{}, + []string{"notready", "false"}, + "", + }, + { + "not logged in", + &erroredYakClient{yak.ErrLoginTokenMissing{errors.New("inner")}}, + []string{"rpk cloud login"}, + "retrieving login token", + }, + { + "generic client error", + &erroredYakClient{errors.New("other error")}, + []string{}, + "other error", + }, + } + + for _, tt := range tests { + var buf bytes.Buffer + logrus.SetOutput(&buf) + err := cloud.GetClusters(tt.client, &buf, "ns") + if len(tt.expectedOutput) > 0 { + for _, s := range tt.expectedOutput { + if !strings.Contains(buf.String(), s) { + t.Errorf("%s: expecting string %s in output %s", tt.name, s, buf.String()) + } + } + } + if tt.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("%s: expecting error %s got %v", tt.name, tt.expectedError, err) + } + } + } +} From 91a2df1bcdecd5494ee82c7117e0ad3795c441ec Mon Sep 17 00:00:00 2001 From: Alena Varkockova Date: Mon, 3 May 2021 13:51:51 +0200 Subject: [PATCH 10/10] rpk/cloud: introduce clusters command into get --- src/go/rpk/pkg/cli/cmd/cloud/get.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/go/rpk/pkg/cli/cmd/cloud/get.go b/src/go/rpk/pkg/cli/cmd/cloud/get.go index 17a9eeedbda5..4409f7e15bdf 100644 --- a/src/go/rpk/pkg/cli/cmd/cloud/get.go +++ b/src/go/rpk/pkg/cli/cmd/cloud/get.go @@ -22,6 +22,7 @@ func NewGetCommand(fs afero.Fs) *cobra.Command { } command.AddCommand(NewNamespacesCommand(fs)) + command.AddCommand(NewClustersCommand(fs)) return command }