Skip to content

Commit

Permalink
Add support for helm secrets (#54)
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 authored Jan 3, 2025
1 parent 0dd244a commit 25aacc9
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 39 deletions.
22 changes: 22 additions & 0 deletions hack/kind-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,25 @@ kind: Namespace
metadata:
name: empty
EOF

## 'helm' namespace
kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: helm
EOF

## helm secret 'test3' in namespace 'helm' (single key)
sec=$(echo "helm-test" | gzip -c | base64 | base64)
kubectl apply -f - <<EOF
apiVersion: v1
data:
release: $sec
kind: Secret
metadata:
name: test3
namespace: helm
type: helm.sh/release.v1
EOF

51 changes: 51 additions & 0 deletions pkg/cmd/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"strings"
)

// SecretDecoder is an interface for decoding various kubernetes secret resources
type SecretDecoder interface {
Decode(input string) (string, error)
}

// Decode decodes a secret based on its type, currently supporting only Opaque and Helm secrets
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 "", err
}
return string(b64d), nil
case Helm:
b64dk8s, err := base64.StdEncoding.DecodeString(input)
if err != nil {
return "", err
}
b64dhelm, err := base64.StdEncoding.DecodeString(string(b64dk8s))
if err != nil {
return "", err
}

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)
}
105 changes: 105 additions & 0 deletions pkg/cmd/decode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package cmd

import (
"bytes"
"compress/gzip"
"encoding/base64"
"errors"
"testing"

"github.com/stretchr/testify/assert"
)

func TestDecode(t *testing.T) {
tests := map[string]struct {
data func() Secret
want string
wantErr error
}{
// base64 encoded
"opaque": {
func() Secret {
return Secret{
Data: map[string]string{
"key": "dGVzdAo=",
},
Type: Opaque,
}
},
"test\n",
nil,
},
// base64 encoded
"opaque invalid": {
func() Secret {
return Secret{
Data: map[string]string{
"key": "dGVzdAo}}}=",
},
Type: Opaque,
}
},
"",
base64.CorruptInputError(7),
},
// 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",
nil,
},
"unknown secret": {
func() Secret {
return Secret{
Type: "invalid",
}
},
"",
errors.New("couldn't decode unknown secret type \"invalid\""),
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

data := tt.data()

got, err := data.Decode(data.Data["key"])
if err != nil {
if tt.wantErr == nil {
assert.Fail(t, "unexpected error", err)
} else if err.Error() != tt.wantErr.Error() {
assert.Equal(t, tt.wantErr, err)
}
return
} else if tt.wantErr != nil {
assert.Fail(t, "expected error, got nil", tt.wantErr)
return
}

assert.Equal(t, tt.want, got)
})
}
}
34 changes: 34 additions & 0 deletions pkg/cmd/types.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,51 @@
package cmd

// SecretList represents a list of secrets
type SecretList struct {
Items []Secret `json:"items"`
}

// Secret represents a kubernetes secret
type Secret struct {
Data SecretData `json:"data"`
Metadata Metadata `json:"metadata"`
Type SecretType `json:"type"`
}

// SecretData represents the data of a secret
type SecretData map[string]string

// Metadata represents the metadata of a secret
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"
)
87 changes: 59 additions & 28 deletions pkg/cmd/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,48 @@ import (

var (
validSecretJson = `{
"apiVersion": "v1",
"data": {
"key1": "dmFsdWUxCg==",
"key2": "dmFsdWUyCg=="
},
"kind": "Secret",
"metadata": {
"creationTimestamp": "2024-08-02T21:25:40Z",
"name": "test",
"namespace": "default",
"resourceVersion": "715",
"uid": "0027fdc9-5371-4715-a0a8-61f3f78fdd36"
},
"type": "Opaque"
}`
"apiVersion": "v1",
"data": {
"key1": "dmFsdWUxCg==",
"key2": "dmFsdWUyCg=="
},
"kind": "Secret",
"metadata": {
"creationTimestamp": "2024-08-02T21:25:40Z",
"name": "test",
"namespace": "default",
"resourceVersion": "715",
"uid": "0027fdc9-5371-4715-a0a8-61f3f78fdd36"
},
"type": "Opaque"
}
`

helmSecretJson = `{
"apiVersion": "v1",
"data": {
"release": "blob"
},
"kind": "Secret",
"metadata": {
"name": "sh.helm.release.v1.wordpress.v1",
"namespace": "default"
},
"type": "helm.sh/release.v1"
}
`

emptySecretJson = `{
"apiVersion": "v1",
"data": {},
"kind": "Secret",
"metadata": {
"name": "test-empty",
"namespace": "default",
},
"type": "Opaque"
}`
invalidSecretJson = `{
"apiVersion": "v1",
"data": {},
"kind": "Secret",
"metadata": {
"name": "test-empty",
"namespace": "default",
},
"type": "Opaque"
}
`
)

func TestSerialize(t *testing.T) {
Expand All @@ -45,17 +61,18 @@ func TestSerialize(t *testing.T) {
want Secret
wantErr error
}{
"empty secret": {
input: emptySecretJson,
"empty opaque 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 opaque secret": {
input: validSecretJson,
want: Secret{
Data: SecretData{
Expand All @@ -66,6 +83,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
Loading

0 comments on commit 25aacc9

Please sign in to comment.