diff --git a/pkg/k8s/kubeconfig/errors.go b/pkg/k8s/kubeconfig/errors.go index 8f65b9576..8e9a1aa37 100644 --- a/pkg/k8s/kubeconfig/errors.go +++ b/pkg/k8s/kubeconfig/errors.go @@ -14,6 +14,10 @@ package kubeconfig +import ( + "fmt" +) + // ErrKubeConfigPathEmpty returned when kubeconfig path is not specified type ErrKubeConfigPathEmpty struct { } @@ -21,3 +25,27 @@ 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 505014a0a..0caa04fba 100644 --- a/pkg/k8s/kubeconfig/kubeconfig.go +++ b/pkg/k8s/kubeconfig/kubeconfig.go @@ -88,6 +88,13 @@ func FromAPIalphaV1(apiObj *v1alpha1.KubeConfig) KubeSourceFunc { } } +// FromSecret returns KubeSource type, uses client interface to kubernetes cluster +func FromSecret(kubeOpts *FromClusterOptions) KubeSourceFunc { + return func() ([]byte, error) { + return GetKubeconfigFromSecret(kubeOpts) + } +} + // FromFile returns KubeSource type, uses path to kubeconfig on FS as source to construct kubeconfig object func FromFile(path string, fs document.FileSystem) KubeSourceFunc { return func() ([]byte, error) { diff --git a/pkg/k8s/kubeconfig/kubeconfig_test.go b/pkg/k8s/kubeconfig/kubeconfig_test.go index 170558cce..28cab6071 100644 --- a/pkg/k8s/kubeconfig/kubeconfig_test.go +++ b/pkg/k8s/kubeconfig/kubeconfig_test.go @@ -23,16 +23,22 @@ 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/document" + "opendev.org/airship/airshipctl/pkg/k8s/client/fake" "opendev.org/airship/airshipctl/pkg/k8s/kubeconfig" "opendev.org/airship/airshipctl/testutil/fs" ) const ( + testClusterName = "dummy_target_cluster" + testSecretName = testClusterName + "-kubeconfig" + testNamespace = "default" testValidKubeconfig = `apiVersion: v1 clusters: - cluster: @@ -130,6 +136,171 @@ func TestKubeconfigContent(t *testing.T) { assert.Equal(t, expectedData, actualData) } +func TestFromSecret(t *testing.T) { + tests := []struct { + name string + opts *kubeconfig.FromClusterOptions + acc fake.ResourceAccumulator + expectedData []byte + 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), + 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, + }, + }, + } + + 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)() + if tt.err != nil { + assert.Equal(t, tt.err, err) + assert.Nil(t, kubeconf) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedData, kubeconf) + } + }) + } +} + func TestFromBundle(t *testing.T) { tests := []struct { name string @@ -171,6 +342,7 @@ func TestFromBundle(t *testing.T) { }) } } + func TestNewKubeConfig(t *testing.T) { tests := []struct { shouldPanic bool diff --git a/pkg/k8s/kubeconfig/secret.go b/pkg/k8s/kubeconfig/secret.go new file mode 100644 index 000000000..4c28bfa2e --- /dev/null +++ b/pkg/k8s/kubeconfig/secret.go @@ -0,0 +1,72 @@ +/* + 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 +}