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
|
package kubeconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// ErrKubeConfigPathEmpty returned when kubeconfig path is not specified
|
// ErrKubeConfigPathEmpty returned when kubeconfig path is not specified
|
||||||
type ErrKubeConfigPathEmpty struct {
|
type ErrKubeConfigPathEmpty struct {
|
||||||
}
|
}
|
||||||
@ -21,3 +25,27 @@ type ErrKubeConfigPathEmpty struct {
|
|||||||
func (e *ErrKubeConfigPathEmpty) Error() string {
|
func (e *ErrKubeConfigPathEmpty) Error() string {
|
||||||
return "kubeconfig path is not defined"
|
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
|
// FromFile returns KubeSource type, uses path to kubeconfig on FS as source to construct kubeconfig object
|
||||||
func FromFile(path string, fs document.FileSystem) KubeSourceFunc {
|
func FromFile(path string, fs document.FileSystem) KubeSourceFunc {
|
||||||
return func() ([]byte, error) {
|
return func() ([]byte, error) {
|
||||||
|
@ -23,16 +23,22 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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"
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
kustfs "sigs.k8s.io/kustomize/api/filesys"
|
kustfs "sigs.k8s.io/kustomize/api/filesys"
|
||||||
|
|
||||||
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
|
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
|
||||||
"opendev.org/airship/airshipctl/pkg/document"
|
"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/pkg/k8s/kubeconfig"
|
||||||
"opendev.org/airship/airshipctl/testutil/fs"
|
"opendev.org/airship/airshipctl/testutil/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
testClusterName = "dummy_target_cluster"
|
||||||
|
testSecretName = testClusterName + "-kubeconfig"
|
||||||
|
testNamespace = "default"
|
||||||
testValidKubeconfig = `apiVersion: v1
|
testValidKubeconfig = `apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
- cluster:
|
- cluster:
|
||||||
@ -130,6 +136,171 @@ func TestKubeconfigContent(t *testing.T) {
|
|||||||
assert.Equal(t, expectedData, actualData)
|
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) {
|
func TestFromBundle(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -171,6 +342,7 @@ func TestFromBundle(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewKubeConfig(t *testing.T) {
|
func TestNewKubeConfig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
shouldPanic bool
|
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