diff --git a/cmd/cluster/cluster.go b/cmd/cluster/cluster.go index 57f9e6fdc..67b0943e3 100644 --- a/cmd/cluster/cluster.go +++ b/cmd/cluster/cluster.go @@ -43,7 +43,7 @@ func NewClusterCommand(cfgFactory config.Factory) *cobra.Command { clusterRootCmd.AddCommand(NewStatusCommand(cfgFactory)) clusterRootCmd.AddCommand(resetsatoken.NewResetCommand(cfgFactory)) clusterRootCmd.AddCommand(checkexpiration.NewCheckCommand(cfgFactory)) - clusterRootCmd.AddCommand(NewGetKubeconfigCommand()) + clusterRootCmd.AddCommand(NewGetKubeconfigCommand(cfgFactory)) return clusterRootCmd } diff --git a/cmd/cluster/get_kubeconfig.go b/cmd/cluster/get_kubeconfig.go index 2c579ba0e..b8d2fec9e 100644 --- a/cmd/cluster/get_kubeconfig.go +++ b/cmd/cluster/get_kubeconfig.go @@ -17,36 +17,66 @@ package cluster import ( "github.com/spf13/cobra" - "opendev.org/airship/airshipctl/pkg/errors" + "opendev.org/airship/airshipctl/pkg/clusterctl/client" + clusterctlcmd "opendev.org/airship/airshipctl/pkg/clusterctl/cmd" + "opendev.org/airship/airshipctl/pkg/config" ) const ( getKubeconfigLong = ` -Retrieve cluster kubeconfig and save it to file or stdout. +Retrieve cluster kubeconfig and print it to stdout ` getKubeconfigExample = ` -# Retrieve target-cluster kubeconfig and print it to stdout -airshipctl cluster get-kubeconfig target-cluster +# Retrieve target-cluster kubeconfig +airshipctl cluster get-kubeconfig target-cluster --kubeconfig /tmp/kubeconfig ` ) // NewGetKubeconfigCommand creates a command which retrieves cluster kubeconfig -func NewGetKubeconfigCommand() *cobra.Command { +func NewGetKubeconfigCommand(cfgFactory config.Factory) *cobra.Command { + o := &client.GetKubeconfigOptions{} cmd := &cobra.Command{ Use: "get-kubeconfig [cluster_name]", Short: "Retrieve kubeconfig for a desired cluster", Long: getKubeconfigLong[1:], Example: getKubeconfigExample[1:], Args: cobra.ExactArgs(1), - RunE: getKubeconfigRunE(), + RunE: getKubeconfigRunE(cfgFactory, o), } + initFlags(o, cmd) + return cmd } +func initFlags(o *client.GetKubeconfigOptions, cmd *cobra.Command) { + flags := cmd.Flags() + + flags.StringVar( + &o.ParentKubeconfigPath, + "kubeconfig", + "", + "path to kubeconfig associated with parental cluster") + + flags.StringVarP( + &o.ManagedClusterNamespace, + "namespace", + "n", + "default", + "namespace where cluster is located, if not specified default one will be used") + + flags.StringVar( + &o.ParentKubeconfigContext, + "context", + "", + "specify context within the kubeconfig file") +} + // getKubeconfigRunE returns a function to cobra command to be executed in runtime -func getKubeconfigRunE() func(cmd *cobra.Command, args []string) error { +func getKubeconfigRunE(cfgFactory config.Factory, o *client.GetKubeconfigOptions) func( + cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - return errors.ErrNotImplemented{What: "cluster get-kubeconfig is not implemented yet"} + o.ManagedClusterName = args[0] + return clusterctlcmd.GetKubeconfig(cfgFactory, o, cmd.OutOrStdout()) } } diff --git a/cmd/cluster/get_kubeconfig_test.go b/cmd/cluster/get_kubeconfig_test.go index aacef11cf..0ad252961 100644 --- a/cmd/cluster/get_kubeconfig_test.go +++ b/cmd/cluster/get_kubeconfig_test.go @@ -26,7 +26,7 @@ func TestNewKubeConfigCommandCmd(t *testing.T) { { Name: "cluster-get-kubeconfig-cmd-with-help", CmdLine: "--help", - Cmd: cluster.NewGetKubeconfigCommand(), + Cmd: cluster.NewGetKubeconfigCommand(nil), }, } for _, testcase := range tests { diff --git a/cmd/cluster/testdata/TestNewKubeConfigCommandCmdGoldenOutput/cluster-get-kubeconfig-cmd-with-help.golden b/cmd/cluster/testdata/TestNewKubeConfigCommandCmdGoldenOutput/cluster-get-kubeconfig-cmd-with-help.golden index 5aa351486..0ee32b3a4 100644 --- a/cmd/cluster/testdata/TestNewKubeConfigCommandCmdGoldenOutput/cluster-get-kubeconfig-cmd-with-help.golden +++ b/cmd/cluster/testdata/TestNewKubeConfigCommandCmdGoldenOutput/cluster-get-kubeconfig-cmd-with-help.golden @@ -1,12 +1,15 @@ -Retrieve cluster kubeconfig and save it to file or stdout. +Retrieve cluster kubeconfig and print it to stdout Usage: get-kubeconfig [cluster_name] [flags] Examples: -# Retrieve target-cluster kubeconfig and print it to stdout -airshipctl cluster get-kubeconfig target-cluster +# Retrieve target-cluster kubeconfig +airshipctl cluster get-kubeconfig target-cluster --kubeconfig /tmp/kubeconfig Flags: - -h, --help help for get-kubeconfig + --context string specify context within the kubeconfig file + -h, --help help for get-kubeconfig + --kubeconfig string path to kubeconfig associated with parental cluster + -n, --namespace string namespace where cluster is located, if not specified default one will be used (default "default") diff --git a/docs/source/cli/airshipctl_cluster_get-kubeconfig.md b/docs/source/cli/airshipctl_cluster_get-kubeconfig.md index 0c7906184..3a33477e4 100644 --- a/docs/source/cli/airshipctl_cluster_get-kubeconfig.md +++ b/docs/source/cli/airshipctl_cluster_get-kubeconfig.md @@ -4,7 +4,7 @@ Retrieve kubeconfig for a desired cluster ### Synopsis -Retrieve cluster kubeconfig and save it to file or stdout. +Retrieve cluster kubeconfig and print it to stdout ``` @@ -14,15 +14,18 @@ airshipctl cluster get-kubeconfig [cluster_name] [flags] ### Examples ``` -# Retrieve target-cluster kubeconfig and print it to stdout -airshipctl cluster get-kubeconfig target-cluster +# Retrieve target-cluster kubeconfig +airshipctl cluster get-kubeconfig target-cluster --kubeconfig /tmp/kubeconfig ``` ### Options ``` - -h, --help help for get-kubeconfig + --context string specify context within the kubeconfig file + -h, --help help for get-kubeconfig + --kubeconfig string path to kubeconfig associated with parental cluster + -n, --namespace string namespace where cluster is located, if not specified default one will be used (default "default") ``` ### Options inherited from parent commands diff --git a/pkg/clusterctl/client/client.go b/pkg/clusterctl/client/client.go index a250e14c0..3dc8c62c0 100644 --- a/pkg/clusterctl/client/client.go +++ b/pkg/clusterctl/client/client.go @@ -30,7 +30,7 @@ var _ Interface = &Client{} type Interface interface { Init(kubeconfigPath, kubeconfigContext string) error Move(fromKubeconfigPath, fromKubeconfigContext, toKubeconfigPath, toKubeconfigContext, namespace string) error - GetKubeconfig(options GetKubeconfigOptions) (string, error) + GetKubeconfig(options *GetKubeconfigOptions) (string, error) } // Client Implements interface to Clusterctl @@ -124,7 +124,7 @@ func newClusterctlClient(root string, options *airshipv1.Clusterctl) (clusterctl } // GetKubeconfig is a wrapper for related cluster-api function -func (c *Client) GetKubeconfig(options GetKubeconfigOptions) (string, error) { +func (c *Client) GetKubeconfig(options *GetKubeconfigOptions) (string, error) { return c.clusterctlClient.GetKubeconfig(clusterctlclient.GetKubeconfigOptions{ Kubeconfig: clusterctlclient.Kubeconfig{ Path: options.ParentKubeconfigPath, diff --git a/pkg/clusterctl/cmd/command.go b/pkg/clusterctl/cmd/command.go index a3b653f2c..c26ec1bfb 100644 --- a/pkg/clusterctl/cmd/command.go +++ b/pkg/clusterctl/cmd/command.go @@ -15,10 +15,13 @@ package cmd import ( + "io" + airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/clusterctl/client" "opendev.org/airship/airshipctl/pkg/config" "opendev.org/airship/airshipctl/pkg/document" + "opendev.org/airship/airshipctl/pkg/k8s/kubeconfig" "opendev.org/airship/airshipctl/pkg/log" "opendev.org/airship/airshipctl/pkg/phase" ) @@ -105,3 +108,22 @@ func (c *Command) Move(toKubeconfigContext string) error { } return c.client.Move(c.kubeconfigPath, c.kubeconfigContext, c.kubeconfigPath, toKubeconfigContext, "") } + +// GetKubeconfig creates new kubeconfig interface object from secret and prints its content to writer +func GetKubeconfig(cfgFactory config.Factory, options *client.GetKubeconfigOptions, writer io.Writer) error { + cfg, err := cfgFactory() + if err != nil { + return err + } + + targetPath, err := cfg.CurrentContextTargetPath() + if err != nil { + return err + } + + client, err := client.NewClient(targetPath, log.DebugEnabled(), &airshipv1.Clusterctl{}) + if err != nil { + return err + } + return kubeconfig.NewKubeConfig(kubeconfig.FromSecret(client, options)).Write(writer) +} diff --git a/pkg/k8s/kubeconfig/errors.go b/pkg/k8s/kubeconfig/errors.go index 8e9a1aa37..8f65b9576 100644 --- a/pkg/k8s/kubeconfig/errors.go +++ b/pkg/k8s/kubeconfig/errors.go @@ -14,10 +14,6 @@ package kubeconfig -import ( - "fmt" -) - // ErrKubeConfigPathEmpty returned when kubeconfig path is not specified type ErrKubeConfigPathEmpty struct { } @@ -25,27 +21,3 @@ type ErrKubeConfigPathEmpty struct { func (e *ErrKubeConfigPathEmpty) Error() string { return "kubeconfig path is not defined" } - -// ErrClusterNameEmpty returned when cluster name is not provided -type ErrClusterNameEmpty struct { -} - -func (e ErrClusterNameEmpty) Error() string { - return "cluster name is not defined" -} - -// ErrMalformedSecret error returned if secret data value is lost or empty -type ErrMalformedSecret struct { - ClusterName string - Namespace string - SecretName string -} - -func (e ErrMalformedSecret) Error() string { - return fmt.Sprintf( - "can't retrieve data from secret %s in cluster %s(namespace: %s)", - e.SecretName, - e.ClusterName, - e.Namespace, - ) -} diff --git a/pkg/k8s/kubeconfig/kubeconfig.go b/pkg/k8s/kubeconfig/kubeconfig.go index a63fa869d..e0be4d159 100644 --- a/pkg/k8s/kubeconfig/kubeconfig.go +++ b/pkg/k8s/kubeconfig/kubeconfig.go @@ -21,6 +21,7 @@ import ( "sigs.k8s.io/yaml" "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/clusterctl/client" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/pkg/fs" ) @@ -90,9 +91,13 @@ func FromAPIalphaV1(apiObj *v1alpha1.KubeConfig) KubeSourceFunc { } // FromSecret returns KubeSource type, uses client interface to kubernetes cluster -func FromSecret(kubeOpts *FromClusterOptions) KubeSourceFunc { +func FromSecret(c client.Interface, o *client.GetKubeconfigOptions) KubeSourceFunc { return func() ([]byte, error) { - return GetKubeconfigFromSecret(kubeOpts) + data, err := c.GetKubeconfig(o) + if err != nil { + return nil, err + } + return []byte(data), nil } } diff --git a/pkg/k8s/kubeconfig/kubeconfig_test.go b/pkg/k8s/kubeconfig/kubeconfig_test.go index 97f41dba9..f7dd85c11 100644 --- a/pkg/k8s/kubeconfig/kubeconfig_test.go +++ b/pkg/k8s/kubeconfig/kubeconfig_test.go @@ -16,6 +16,7 @@ package kubeconfig_test import ( "bytes" + "errors" "fmt" "io" "io/ioutil" @@ -23,22 +24,17 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - coreV1 "k8s.io/api/core/v1" - metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/client-go/tools/clientcmd/api/v1" kustfs "sigs.k8s.io/kustomize/api/filesys" "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/clusterctl/client" "opendev.org/airship/airshipctl/pkg/fs" - "opendev.org/airship/airshipctl/pkg/k8s/client/fake" "opendev.org/airship/airshipctl/pkg/k8s/kubeconfig" testfs "opendev.org/airship/airshipctl/testutil/fs" ) const ( - testClusterName = "dummy_target_cluster" - testSecretName = testClusterName + "-kubeconfig" - testNamespace = "default" testValidKubeconfig = `apiVersion: v1 clusters: - cluster: @@ -136,166 +132,46 @@ func TestKubeconfigContent(t *testing.T) { assert.Equal(t, expectedData, actualData) } +type MockClientInterface struct { + MockGetKubeconfig func(options *client.GetKubeconfigOptions) (string, error) + client.Interface +} + +func (c MockClientInterface) GetKubeconfig(o *client.GetKubeconfigOptions) (string, error) { + return c.MockGetKubeconfig(o) +} + func TestFromSecret(t *testing.T) { tests := []struct { name string - opts *kubeconfig.FromClusterOptions - acc fake.ResourceAccumulator - expectedData []byte + expectedData string err error }{ { - name: "valid kubeconfig", - opts: &kubeconfig.FromClusterOptions{ - ClusterName: testClusterName, - Namespace: testNamespace, - }, - acc: fake.WithTypedObjects(&coreV1.Secret{ - TypeMeta: metaV1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metaV1.ObjectMeta{ - Name: testSecretName, - Namespace: testNamespace, - }, - Data: map[string][]byte{ - "value": []byte(testValidKubeconfig), - }, - }), - expectedData: []byte(testValidKubeconfig), + name: "valid kubeconfig", + expectedData: testValidKubeconfig, err: nil, }, { - name: "no cluster name", - opts: &kubeconfig.FromClusterOptions{ - ClusterName: "", - Namespace: testNamespace, - }, - acc: fake.WithTypedObjects(&coreV1.Secret{ - TypeMeta: metaV1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metaV1.ObjectMeta{ - Name: testSecretName, - Namespace: testNamespace, - }, - Data: map[string][]byte{ - "value": []byte(testValidKubeconfig), - }, - }), - expectedData: nil, - err: kubeconfig.ErrClusterNameEmpty{}, - }, - { - name: "default namespace", - opts: &kubeconfig.FromClusterOptions{ - ClusterName: testClusterName, - Namespace: "", - }, - acc: fake.WithTypedObjects(&coreV1.Secret{ - TypeMeta: metaV1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metaV1.ObjectMeta{ - Name: testSecretName, - Namespace: testNamespace, - }, - Data: map[string][]byte{ - "value": []byte(testValidKubeconfig), - }, - }), - expectedData: []byte(testValidKubeconfig), - err: nil, - }, - { - name: "no data in secret", - opts: &kubeconfig.FromClusterOptions{ - ClusterName: testClusterName, - Namespace: testNamespace, - }, - acc: fake.WithTypedObjects(&coreV1.Secret{ - TypeMeta: metaV1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metaV1.ObjectMeta{ - Name: testSecretName, - Namespace: testNamespace, - }, - }), - expectedData: nil, - err: kubeconfig.ErrMalformedSecret{ - ClusterName: testClusterName, - Namespace: testNamespace, - SecretName: testSecretName, - }, - }, - { - name: "empty data in secret", - opts: &kubeconfig.FromClusterOptions{ - ClusterName: testClusterName, - Namespace: testNamespace, - }, - acc: fake.WithTypedObjects(&coreV1.Secret{ - TypeMeta: metaV1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metaV1.ObjectMeta{ - Name: testSecretName, - Namespace: testNamespace, - }, - Data: map[string][]byte{}, - }), - expectedData: nil, - err: kubeconfig.ErrMalformedSecret{ - ClusterName: testClusterName, - Namespace: testNamespace, - SecretName: testSecretName, - }, - }, - { - name: "empty value in data in secret", - opts: &kubeconfig.FromClusterOptions{ - ClusterName: testClusterName, - Namespace: testNamespace, - }, - acc: fake.WithTypedObjects(&coreV1.Secret{ - TypeMeta: metaV1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metaV1.ObjectMeta{ - Name: testSecretName, - Namespace: testNamespace, - }, - Data: map[string][]byte{ - "value": []byte(""), - }, - }), - expectedData: nil, - err: kubeconfig.ErrMalformedSecret{ - ClusterName: testClusterName, - Namespace: testNamespace, - SecretName: testSecretName, - }, + name: "failed to get kubeconfig", + expectedData: "", + err: errors.New("error"), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - tt.opts.Client = fake.NewClient(tt.acc) - kubeconf, err := kubeconfig.FromSecret(tt.opts)() + cl := MockClientInterface{ + MockGetKubeconfig: func(_ *client.GetKubeconfigOptions) (string, error) { return tt.expectedData, tt.err }, + } + kubeconf, err := kubeconfig.FromSecret(cl, nil)() if tt.err != nil { - assert.Equal(t, tt.err, err) + require.Error(t, err) assert.Nil(t, kubeconf) } else { require.NoError(t, err) - assert.Equal(t, tt.expectedData, kubeconf) + assert.Equal(t, []byte(tt.expectedData), kubeconf) } }) } diff --git a/pkg/k8s/kubeconfig/secret.go b/pkg/k8s/kubeconfig/secret.go deleted file mode 100644 index 4c28bfa2e..000000000 --- a/pkg/k8s/kubeconfig/secret.go +++ /dev/null @@ -1,72 +0,0 @@ -/* - 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 - - https://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 kubeconfig - -import ( - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "opendev.org/airship/airshipctl/pkg/k8s/client" - "opendev.org/airship/airshipctl/pkg/log" -) - -// FromClusterOptions holds all configurable options for kubeconfig extraction -type FromClusterOptions struct { - ClusterName string - Namespace string - Client client.Interface -} - -// GetKubeconfigFromSecret extracts kubeconfig from secret data structure -func GetKubeconfigFromSecret(o *FromClusterOptions) ([]byte, error) { - const defaultNamespace = "default" - - if o.ClusterName == "" { - return nil, ErrClusterNameEmpty{} - } - if o.Namespace == "" { - log.Printf("Namespace is not provided, using default one") - o.Namespace = defaultNamespace - } - - log.Debugf("Extracting kubeconfig from secret in cluster %s(namespace: %s)", o.ClusterName, o.Namespace) - secretName := fmt.Sprintf("%s-kubeconfig", o.ClusterName) - kubeCore := o.Client.ClientSet().CoreV1() - - secret, err := kubeCore.Secrets(o.Namespace).Get(secretName, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - if secret.Data == nil { - return nil, ErrMalformedSecret{ - ClusterName: o.ClusterName, - Namespace: o.Namespace, - SecretName: secretName, - } - } - - val, exist := secret.Data["value"] - if !exist || len(val) == 0 { - return nil, ErrMalformedSecret{ - ClusterName: o.ClusterName, - Namespace: o.Namespace, - SecretName: secretName, - } - } - - return val, nil -}