Skip to content

Commit

Permalink
feat(k8s): extend k8s provider to fetch configmap (#191)
Browse files Browse the repository at this point in the history
The k8s provider now supports fetching values from ConfigMaps.

Signed-off-by: Bernardo Salazar <[email protected]>
  • Loading branch information
bersalazar authored Dec 23, 2023
1 parent 81c4042 commit 669351c
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 45 deletions.
44 changes: 31 additions & 13 deletions pkg/providers/k8s/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,8 @@ func (p *provider) GetString(path string) (string, error) {
if apiVersion != "v1" {
return "", fmt.Errorf("Invalid apiVersion %s. Only apiVersion v1 is supported at this time.", apiVersion)
}
if kind != "Secret" {
return "", fmt.Errorf("Invalid kind %s. Only kind Secret is supported at this time.", kind)
}

//TODO:
// At this time, only Secret kind with v1 apiVersion version is supported.
// getObject() should be extended to support both ConfigMap and Secrets kind in other apiVersions.
objectData, err := getObject(namespace, name, p.KubeConfigPath, p.KubeContext, context.Background())
objectData, err := getObject(kind, namespace, name, p.KubeConfigPath, p.KubeContext, context.Background())
if err != nil {
return "", fmt.Errorf("Unable to get %s %s/%s: %s", kind, namespace, name, err)
}
Expand All @@ -110,7 +104,7 @@ func (p *provider) GetString(path string) (string, error) {
}
p.log.Debugf(message)

return string(object), nil
return object, nil
}

func (p *provider) GetStringMap(path string) (map[string]interface{}, error) {
Expand All @@ -135,7 +129,7 @@ func buildConfigWithContextFromFlags(context string, kubeconfigPath string) (*re
}

// Fetch the object from the Kubernetes cluster
func getObject(namespace string, name string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string][]byte, error) {
func getObject(kind string, namespace string, name string, kubeConfigPath string, kubeContext string, ctx context.Context) (map[string]string, error) {
if kubeContext == "" {
fmt.Printf("vals-k8s: kubeContext was not provided. Using current context.\n")
}
Expand All @@ -151,10 +145,34 @@ func getObject(namespace string, name string, kubeConfigPath string, kubeContext
return nil, fmt.Errorf("Unable to create the Kubernetes client: %s", err)
}

object, err := clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("Unable to get the object from Kubernetes: %s", err)
var object map[string]string

switch kind {
case "Secret":
secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("Unable to get the Secret object from Kubernetes: %s", err)
}
object = convertByteMapToStringMap(secret.Data)
case "ConfigMap":
configmap, err := clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("Unable to get the ConfigMap object from Kubernetes: %s", err)
}
object = configmap.Data
default:
return nil, fmt.Errorf("The specified kind is not valid. Valid kinds: Secret, ConfigMap")
}

return object, nil
}

func convertByteMapToStringMap(byteMap map[string][]byte) map[string]string {
stringMap := make(map[string]string)

for key, value := range byteMap {
stringMap[key] = string(value)
}

return object.Data, nil
return stringMap
}
122 changes: 99 additions & 23 deletions pkg/providers/k8s/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,54 +19,106 @@ import (
// kubectl create namespace test-namespace
// create a secret:
// kubectl create secret generic mysecret -n test-namespace --from-literal=key=p4ssw0rd
// create a configmap:
// kubectl create configmap myconfigmap -n test-namespace --from-literal=key=configValue

func Test_getObject(t *testing.T) {
homeDir, _ := os.UserHomeDir()
testcases := []struct {
namespace string
kind string
name string
kubeConfigPath string
want map[string][]uint8
want map[string]string
wantErr string
}{
// valid kubeConfigPath is specified
// (secret) valid kubeConfigPath is specified
{
namespace: "test-namespace",
kind: "Secret",
name: "mysecret",
kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir),
want: map[string][]uint8{"key": []uint8("p4ssw0rd")},
want: map[string]string{"key": "p4ssw0rd"},
wantErr: "",
},
// kubeConfigPath does not exist
// (secret) kubeConfigPath does not exist
{
namespace: "test-namespace",
kind: "Secret",
name: "mysecret",
kubeConfigPath: "/tmp/does-not-exist",
want: nil,
wantErr: "Unable to build Kubeconfig from vals configuration: stat /tmp/does-not-exist: no such file or directory",
},
// namespace does not exist
// (secret) namespace does not exist
{
namespace: "non-existent-namespace",
kind: "Secret",
name: "mysecret",
kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir),
want: nil,
wantErr: "Unable to get the object from Kubernetes: secrets \"mysecret\" not found",
wantErr: "Unable to get the Secret object from Kubernetes: secrets \"mysecret\" not found",
},
// secret does not exist
// (secret) secret does not exist
{
namespace: "test-namespace",
kind: "Secret",
name: "non-existent-secret",
kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir),
want: nil,
wantErr: "Unable to get the object from Kubernetes: secrets \"non-existent-secret\" not found",
wantErr: "Unable to get the Secret object from Kubernetes: secrets \"non-existent-secret\" not found",
},
// (configmap) valid kubeConfigPath is specified
{
namespace: "test-namespace",
kind: "ConfigMap",
name: "myconfigmap",
kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir),
want: map[string]string{"key": "configValue"},
wantErr: "",
},
// (configmap) kubeConfigPath does not exist
{
namespace: "test-namespace",
kind: "ConfigMap",
name: "myconfigmap",
kubeConfigPath: "/tmp/does-not-exist",
want: nil,
wantErr: "Unable to build Kubeconfig from vals configuration: stat /tmp/does-not-exist: no such file or directory",
},
// (configmap) namespace does not exist
{
namespace: "non-existent-namespace",
kind: "ConfigMap",
name: "myconfigmap",
kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir),
want: nil,
wantErr: "Unable to get the ConfigMap object from Kubernetes: configmaps \"myconfigmap\" not found",
},
// (configmap) configmap does not exist
{
namespace: "test-namespace",
kind: "ConfigMap",
name: "non-existent-configmap",
kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir),
want: nil,
wantErr: "Unable to get the ConfigMap object from Kubernetes: configmaps \"non-existent-configmap\" not found",
},
// unsupported kind
{
namespace: "test-namespace",
kind: "UnsupportedKind",
name: "myconfigmap",
kubeConfigPath: fmt.Sprintf("%s/.kube/config", homeDir),
want: nil,
wantErr: "The specified kind is not valid. Valid kinds: Secret, ConfigMap",
},
}

for i := range testcases {
tc := testcases[i]
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
got, err := getObject(tc.namespace, tc.name, tc.kubeConfigPath, "", context.Background())
got, err := getObject(tc.kind, tc.namespace, tc.name, tc.kubeConfigPath, "", context.Background())
if err != nil {
if err.Error() != tc.wantErr {
t.Fatalf("unexpected error: want %q, got %q", tc.wantErr, err.Error())
Expand Down Expand Up @@ -205,53 +257,77 @@ func Test_GetString(t *testing.T) {
want string
wantErr string
}{
// Valid path is specified
// (secret) Valid path is specified
{
path: "v1/Secret/test-namespace/mysecret/key",
want: "p4ssw0rd",
wantErr: "",
},
// Invalid path is specified
// (configmap) Valid path is specified
{
path: "v1/ConfigMap/test-namespace/myconfigmap/key",
want: "configValue",
wantErr: "",
},
// (secret) Invalid path is specified
{
path: "v1/Secret/test-namespace/mysecret/key/more/path",
want: "",
wantErr: "Invalid path v1/Secret/test-namespace/mysecret/key/more/path. Path must be in the format <apiVersion>/<kind>/<namespace>/<name>/<key>",
},
// Bad path is specified
// (configmap) Invalid path is specified
{
path: "bad/data/path",
path: "v1/ConfigMap/test-namespace/myconfigmap/key/more/path",
want: "",
wantErr: "Invalid path bad/data/path. Path must be in the format <apiVersion>/<kind>/<namespace>/<name>/<key>",
wantErr: "Invalid path v1/ConfigMap/test-namespace/myconfigmap/key/more/path. Path must be in the format <apiVersion>/<kind>/<namespace>/<name>/<key>",
},
// Non-existent namespace is specified
// (secret) Non-existent namespace is specified
{
path: "v1/Secret/badnamespace/secret/key",
want: "",
wantErr: "Unable to get Secret badnamespace/secret: Unable to get the object from Kubernetes: secrets \"secret\" not found",
wantErr: "Unable to get Secret badnamespace/secret: Unable to get the Secret object from Kubernetes: secrets \"secret\" not found",
},
// Non-existent secret is specified
// (configmap) Non-existent secret is specified
{
path: "v1/Secret/test-namespace/badsecret/key",
want: "",
wantErr: "Unable to get Secret test-namespace/badsecret: Unable to get the object from Kubernetes: secrets \"badsecret\" not found",
wantErr: "Unable to get Secret test-namespace/badsecret: Unable to get the Secret object from Kubernetes: secrets \"badsecret\" not found",
},
// Non-existent key is requested
// (secret) Non-existent key is requested
{
path: "v1/Secret/test-namespace/mysecret/non-existent-key",
want: "",
wantErr: "Key non-existent-key does not exist in test-namespace/mysecret",
},
// Invalid apiVersion specified
// (configmap) Non-existent key is requested
{
path: "v1/ConfigMap/test-namespace/myconfigmap/non-existent-key",
want: "",
wantErr: "Key non-existent-key does not exist in test-namespace/myconfigmap",
},
// (secret) Invalid apiVersion specified
{
path: "v2/Secret/test-namespace/mysecret/non-existent-key",
want: "",
wantErr: "Invalid apiVersion v2. Only apiVersion v1 is supported at this time.",
},
// Invalid kind specified
// (configmap) Invalid apiVersion specified
{
path: "v2/ConfigMap/test-namespace/myconfigmap/non-existent-key",
want: "",
wantErr: "Invalid apiVersion v2. Only apiVersion v1 is supported at this time.",
},
// Incorrect path is specified
{
path: "bad/data/path",
want: "",
wantErr: "Invalid path bad/data/path. Path must be in the format <apiVersion>/<kind>/<namespace>/<name>/<key>",
},
// Unsupported kind is specified
{
path: "v1/ConfigMap/test-namespace/mysecret/non-existent-key",
path: "v1/UnsupportedKind/test-namespace/myconfigmap/key",
want: "",
wantErr: "Invalid kind ConfigMap. Only kind Secret is supported at this time.",
wantErr: "Unable to get UnsupportedKind test-namespace/myconfigmap: The specified kind is not valid. Valid kinds: Secret, ConfigMap",
},
}
for _, tc := range tests {
Expand Down
Loading

0 comments on commit 669351c

Please sign in to comment.