From f7fde3860a7f64d7cb6ce5a176e6b2ef3936b644 Mon Sep 17 00:00:00 2001 From: Jonas-Taha El Sesiy Date: Thu, 2 Jan 2025 18:18:59 +0100 Subject: [PATCH] Add support for helm secrets Prior to this change, the plugin always assumed secrets to be of type `Opaque`. This change introduces the secret type for (de)serialization and adds a small interface that is used to keep the core logic mostly unchanged but handle different types of secrets. Only support for helm was added for now but expanding to more types should be straight-forward Fixes #50 --- pkg/cmd/decode.go | 43 ++++++++++++++++++++++ pkg/cmd/decode_test.go | 72 +++++++++++++++++++++++++++++++++++++ pkg/cmd/types.go | 30 ++++++++++++++++ pkg/cmd/types_test.go | 36 ++++++++++++++++--- pkg/cmd/view-secret.go | 13 ++++--- pkg/cmd/view-secret_test.go | 2 +- 6 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 pkg/cmd/decode.go create mode 100644 pkg/cmd/decode_test.go diff --git a/pkg/cmd/decode.go b/pkg/cmd/decode.go new file mode 100644 index 0000000..3a99af8 --- /dev/null +++ b/pkg/cmd/decode.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "compress/gzip" + "encoding/base64" + "fmt" + "io" + "strings" +) + +type SecretDecoder interface { + Decode(input string) (string, error) +} + +func (s Secret) Decode(input string) (string, error) { + switch s.Type { + // TODO handle all secret types + case Opaque: + b64d, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return "", nil + } + return string(b64d), nil + case Helm: + b64dk8s, _ := base64.StdEncoding.DecodeString(input) + b64dhelm, _ := base64.StdEncoding.DecodeString(string(b64dk8s)) + + gz, err := gzip.NewReader(strings.NewReader(string(b64dhelm))) + if err != nil { + return "", err + } + defer gz.Close() + + s, err := io.ReadAll(gz) + if err != nil { + return "", err + } + + return string(s), nil + } + + return "", fmt.Errorf("couldn't decode unknown secret type %q", s.Type) +} diff --git a/pkg/cmd/decode_test.go b/pkg/cmd/decode_test.go new file mode 100644 index 0000000..d81e365 --- /dev/null +++ b/pkg/cmd/decode_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecode(t *testing.T) { + tests := map[string]struct { + data func() Secret + want string + }{ + // base64 encoded + "opaque": { + func() Secret { + return Secret{ + Data: map[string]string{ + "key": "dGVzdAo=", + }, + Type: Opaque, + } + }, + "test\n", + }, + // double base64 encoded + gzip'd + "helm": { + func() Secret { + res := Secret{ + Type: Helm, + } + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write([]byte("test\n")); err != nil { + return res + } + if err := gz.Close(); err != nil { + return res + } + + b64k8s := base64.StdEncoding.EncodeToString(buf.Bytes()) + b64helm := base64.StdEncoding.EncodeToString([]byte(b64k8s)) + + res.Data = SecretData{ + "key": b64helm, + } + + return res + }, + "test\n", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + data := tt.data() + want := tt.want + + got, err := data.Decode(data.Data["key"]) + if err != nil { + t.Errorf("got %v, want %v", got, want) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/cmd/types.go b/pkg/cmd/types.go index 4eecb8d..37f4d00 100644 --- a/pkg/cmd/types.go +++ b/pkg/cmd/types.go @@ -7,6 +7,7 @@ type SecretList struct { type Secret struct { Data SecretData `json:"data"` Metadata Metadata `json:"metadata"` + Type SecretType `json:"type"` } type SecretData map[string]string @@ -15,3 +16,32 @@ type Metadata struct { Name string `json:"name"` Namespace string `json:"namespace"` } + +// SecretType represents the type of a secret +// +// Opaque arbitrary user-defined data +// kubernetes.io/service-account-token ServiceAccount token +// kubernetes.io/dockercfg serialized ~/.dockercfg file +// kubernetes.io/dockerconfigjson serialized ~/.docker/config.json file +// kubernetes.io/basic-auth credentials for basic authentication +// kubernetes.io/ssh-auth credentials for SSH authentication +// kubernetes.io/tls data for a TLS client or server +// bootstrap.kubernetes.io/token bootstrap token data +// helm.sh/release.v1 Helm v3 release data +// +// refs: +// - https://kubernetes.io/docs/concepts/configuration/secret/#secret-types +// - https://gist.github.com/DzeryCZ/c4adf39d4a1a99ae6e594a183628eaee +type SecretType string + +const ( + BasicAuth SecretType = "kubernetes.io/basic-auth" + DockerCfg SecretType = "kubernetes.io/dockercfg" + DockerConfigJson SecretType = "kubernetes.io/dockerconfigjson" + Helm SecretType = "helm.sh/release.v1" + Opaque SecretType = "Opaque" + ServiceAccountToken SecretType = "kubernetes.io/service-account-token" + SshAuth SecretType = "kubernetes.io/ssh-auth" + Tls SecretType = "kubernetes.io/tls" + Token SecretType = "bootstrap.kubernetes.io/token" +) diff --git a/pkg/cmd/types_test.go b/pkg/cmd/types_test.go index 508bc29..9ee3132 100644 --- a/pkg/cmd/types_test.go +++ b/pkg/cmd/types_test.go @@ -27,7 +27,20 @@ var ( "type": "Opaque" }` - emptySecretJson = `{ + helmSecretJson = `{ + "apiVersion": "v1", + "data": { + "release": "blob" + }, + "kind": "Secret", + "metadata": { + "name": "sh.helm.release.v1.wordpress.v1", + "namespace": "default" + }, + "type": "helm.sh/release.v1" + }` + + invalidSecretJson = `{ "apiVersion": "v1", "data": {}, "kind": "Secret", @@ -45,17 +58,18 @@ func TestSerialize(t *testing.T) { want Secret wantErr error }{ - "empty secret": { - input: emptySecretJson, + "empty opqague secret": { + input: invalidSecretJson, want: Secret{ Metadata: Metadata{ Name: "test", Namespace: "default", }, + Type: Opaque, }, wantErr: errors.New("invalid character '}' looking for beginning of object key string"), }, - "valid secret": { + "valid opague secret": { input: validSecretJson, want: Secret{ Data: SecretData{ @@ -66,6 +80,20 @@ func TestSerialize(t *testing.T) { Name: "test", Namespace: "default", }, + Type: Opaque, + }, + }, + "valid helm secret": { + input: helmSecretJson, + want: Secret{ + Data: SecretData{ + "release": "blob", + }, + Metadata: Metadata{ + Name: "sh.helm.release.v1.wordpress.v1", + Namespace: "default", + }, + Type: Helm, }, }, } diff --git a/pkg/cmd/view-secret.go b/pkg/cmd/view-secret.go index 8cbd9b5..2d57375 100644 --- a/pkg/cmd/view-secret.go +++ b/pkg/cmd/view-secret.go @@ -2,7 +2,6 @@ package cmd import ( "bytes" - "encoding/base64" "errors" "fmt" "io" @@ -221,19 +220,19 @@ func ProcessSecret(outWriter, errWriter io.Writer, inputReader io.Reader, secret if decodeAll { for _, k := range keys { - b64d, _ := base64.StdEncoding.DecodeString(data[k]) - _, _ = fmt.Fprintf(outWriter, "%s='%s'\n", k, string(b64d)) + s, _ := secret.Decode(data[k]) + _, _ = fmt.Fprintf(outWriter, "%s='%s'\n", k, s) } } else if len(data) == 1 { for k, v := range data { _, _ = fmt.Fprintf(errWriter, singleKeyDescription+"\n", k) - b64d, _ := base64.StdEncoding.DecodeString(v) - _, _ = fmt.Fprintf(outWriter, "%s\n", string(b64d)) + s, _ := secret.Decode(v) + _, _ = fmt.Fprintf(outWriter, "%s\n", s) } } else if secretKey != "" { if v, ok := data[secretKey]; ok { - b64d, _ := base64.StdEncoding.DecodeString(v) - _, _ = fmt.Fprintf(outWriter, "%s\n", string(b64d)) + s, _ := secret.Decode(v) + _, _ = fmt.Fprintf(outWriter, "%s\n", s) } else { return ErrSecretKeyNotFound } diff --git a/pkg/cmd/view-secret_test.go b/pkg/cmd/view-secret_test.go index 28403cf..669fdab 100644 --- a/pkg/cmd/view-secret_test.go +++ b/pkg/cmd/view-secret_test.go @@ -170,7 +170,7 @@ func TestProcessSecret(t *testing.T) { readBuf = *strings.NewReader(test.feedkeys) } - err := ProcessSecret(&stdOutBuf, &stdErrBuf, &readBuf, Secret{Data: test.secretData}, test.secretKey, test.decodeAll) + err := ProcessSecret(&stdOutBuf, &stdErrBuf, &readBuf, Secret{Data: test.secretData, Type: Opaque}, test.secretKey, test.decodeAll) if test.err != nil { assert.Equal(t, err, test.err)