Link cluster get-kubeconfig cmd with appropriate functionality

This patch links cluster get-kubeconfig command call with
appropriate clusterctl functionality, and also replaces old
callbacks and removes outdated implementation.

Change-Id: Ibd0d981985f94497db250c8df3f5675fdec1d2ca
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Relates-To: #374
This commit is contained in:
Ruslan Aliev 2020-12-07 16:58:09 -06:00
parent 63421c8fcc
commit 899bbdbe07
11 changed files with 108 additions and 269 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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