Extended kubeconfig interface
Added kubeconfig extraction from secret stored in kubernetes cluster Change-Id: I4399d718857614c6d6dfec2174092cd84c4ee22f
This commit is contained in:
parent
eaa66fded7
commit
f0df007945
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
72
pkg/k8s/kubeconfig/secret.go
Normal file
72
pkg/k8s/kubeconfig/secret.go
Normal file
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user