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 3707de9
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
}

Check warning on line 22 in pkg/cmd/decode.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L21-L22

Added lines #L21 - L22 were not covered by tests
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
}

Check warning on line 31 in pkg/cmd/decode.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L30-L31

Added lines #L30 - L31 were not covered by tests
defer gz.Close()

s, err := io.ReadAll(gz)
if err != nil {
return "", err
}

Check warning on line 37 in pkg/cmd/decode.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L36-L37

Added lines #L36 - L37 were not covered by tests

return string(s), nil
}

return "", fmt.Errorf("couldn't decode unknown secret type %q", s.Type)

Check warning on line 42 in pkg/cmd/decode.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L42

Added line #L42 was not covered by tests
}
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 3707de9

Please sign in to comment.