Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for helm secrets #54

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}

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

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L29-L30

Added lines #L29 - L30 were not covered by tests
b64dhelm, err := base64.StdEncoding.DecodeString(string(b64dk8s))
if err != nil {
return "", err
}

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

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L33-L34

Added lines #L33 - L34 were not covered by tests

gz, err := gzip.NewReader(strings.NewReader(string(b64dhelm)))
if err != nil {
return "", err
}

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

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L38-L39

Added lines #L38 - L39 were not covered by tests
defer gz.Close()

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/decode.go#L44-L45

Added lines #L44 - L45 were not covered by tests

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