Skip to content

Commit

Permalink
Add support for helm secrets
Browse files Browse the repository at this point in the history
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
  • Loading branch information
elsesiy committed Jan 2, 2025
1 parent 0dd244a commit f7fde38
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 12 deletions.
43 changes: 43 additions & 0 deletions pkg/cmd/decode.go
Original file line number Diff line number Diff line change
@@ -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)
}
72 changes: 72 additions & 0 deletions pkg/cmd/decode_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
30 changes: 30 additions & 0 deletions pkg/cmd/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
)
36 changes: 32 additions & 4 deletions pkg/cmd/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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{
Expand All @@ -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,
},
},
}
Expand Down
13 changes: 6 additions & 7 deletions pkg/cmd/view-secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/view-secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit f7fde38

Please sign in to comment.