Merge "Link cluster get-kubeconfig cmd with appropriate functionality"
This commit is contained in:
commit
621b7758e0
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user