Skip to content

Commit

Permalink
feat: support placeholders in dockerconfigjson auth field
Browse files Browse the repository at this point in the history
  • Loading branch information
teejaded committed Dec 1, 2023
1 parent b2d7f10 commit cdc3110
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 1 deletion.
11 changes: 11 additions & 0 deletions fixtures/input/nonempty/secret_dockerconfigjson.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
annotations:
avp.kubernetes.io/path: secret/testing
avp.kubernetes.io/kv-version: "1"
name: <name>-dockerconfigjson
namespace: <namespace>
data:
.dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL215LXNlcnZlci5sb2NhbCI6eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6Ilx1MDAzY3BhdGg6c2VjcmV0L3Rlc3Rpbmcjc2VjcmV0LXZhci12YWx1ZVx1MDAzZSIsImF1dGgiOiJkWE5sY2pvOGNHRjBhRHB6WldOeVpYUXZkR1Z6ZEdsdVp5TnpaV055WlhRdGRtRnlMWFpoYkhWbFBnPT0ifX19
12 changes: 12 additions & 0 deletions fixtures/output/all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,18 @@ metadata:
type: Opaque
---
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL215LXNlcnZlci5sb2NhbCI6eyJhdXRoIjoiZFhObGNqcGtSMVo2WkVNeGQxbFlUbnBrTWpsNVdrRTlQUT09IiwicGFzc3dvcmQiOiJkR1Z6ZEMxd1lYTnpkMjl5WkE9PSIsInVzZXJuYW1lIjoidXNlciJ9fX0=
kind: Secret
metadata:
annotations:
avp.kubernetes.io/kv-version: "1"
avp.kubernetes.io/path: secret/testing
name: test-name-dockerconfigjson
namespace: test-namespace
type: kubernetes.io/dockerconfigjson
---
apiVersion: v1
data:
secret.yaml: c29tZQ==dGVzdC1wYXNzd29yZA==dmFsdWU=
kind: Secret
Expand Down
8 changes: 7 additions & 1 deletion pkg/kube/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
// A Resource is the basis for all Templates
type Resource struct {
Kind string
Type string
TemplateData map[string]interface{} // The template as read from YAML
Backend types.Backend
replacementErrors []error // Any errors encountered in performing replacements
Expand Down Expand Up @@ -50,6 +51,7 @@ func NewTemplate(template unstructured.Unstructured, backend types.Backend, path
return &Template{
Resource{
Kind: template.GetKind(),
Type: getType(template),
TemplateData: template.Object,
Backend: backend,
Data: data,
Expand All @@ -71,7 +73,11 @@ func (t *Template) Replace() error {
case "ConfigMap":
replacerFunc = configReplacement
case "Secret":
replacerFunc = secretReplacement
if t.Type == "kubernetes.io/dockerconfigjson" {
replacerFunc = dockerSecretReplacement
} else {
replacerFunc = secretReplacement
}
default:
replacerFunc = genericReplacement
}
Expand Down
62 changes: 62 additions & 0 deletions pkg/kube/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ import (

"github.com/argoproj-labs/argocd-vault-plugin/pkg/types"
"github.com/argoproj-labs/argocd-vault-plugin/pkg/utils"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8yaml "k8s.io/apimachinery/pkg/util/yaml"
)

type missingKeyError struct {
s string
}

type dockerConfig struct {
Auths map[string](map[string]string) `json:"auths"`
}

func (e *missingKeyError) Error() string {
return e.s
}
Expand Down Expand Up @@ -226,6 +231,54 @@ func secretReplacement(key, value string, resource Resource) (interface{}, []err
return genericReplacement(key, value, resource)
}

func dockerSecretReplacement(key, value string, resource Resource) (interface{}, []error) {
reencode := true
bytes, err := base64.StdEncoding.DecodeString(value)
if err != nil {
reencode = false
bytes = []byte(value)
}

dc := dockerConfig{}
err = json.Unmarshal(bytes, &dc)
if err != nil {
return secretReplacement(key, value, resource)
}

errs := []error{}

// iterate through the auths map and run a secretReplacement
// on each value so we can replace secrets that have
// been base64 encoded twice or are HTML escaped
for repo, auth := range dc.Auths {
for k, v := range auth {
res, err := secretReplacement(key, v, resource)
if err != nil {
errs = append(errs, err...)
} else {
auth[k], _ = res.(string)
}
}
dc.Auths[repo] = auth
}

bytes, err = json.Marshal(dc)
if err != nil {
return nil, append(errs, err)
}

value = string(bytes)
if reencode {
// only base64 encode if the original value was encoded
value = base64.StdEncoding.EncodeToString(bytes)
}

// run a fallback genericReplacement to catch any placeholders
// that are not in .dockerconfigjson
res, fallbackErr := genericReplacement(key, value, resource)
return res, append(errs, fallbackErr...)
}

func stringify(input interface{}) string {
switch input.(type) {
case int:
Expand Down Expand Up @@ -270,3 +323,12 @@ func secretNamespaceName(input string) (string, string) {

return secretNamespace, secretName
}

func getType(template unstructured.Unstructured) string {
val := template.UnstructuredContent()["type"]
s, ok := val.(string)
if !ok {
return ""
}
return s
}
68 changes: 68 additions & 0 deletions pkg/kube/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,74 @@ func TestSecretReplacement_Base64Substrings(t *testing.T) {
assertSuccessfulReplacement(&dummyResource, &expected, t)
}

func TestDockerReplacement_Base64(t *testing.T) {
dummyResource := Resource{
TemplateData: map[string]interface{}{
"data": map[string]interface{}{
".dockerconfigjson": `eyJhdXRocyI6eyJodHRwczovL215LXNlcnZlci5sb2NhbCI6eyJ1c2VybmFtZSI6Ilx1MDAzY3VzZXJcdTAwM2UiLCJwYXNzd29yZCI6Ilx1MDAzY3Bhc3NcdTAwM2UiLCJhdXRoIjoiUEhWelpYSStPanh3WVhOelBnPT0ifX19`,
},
},
Data: map[string]interface{}{
"user": "testuser",
"pass": "testpass",
},
Annotations: map[string]string{
(types.AVPPathAnnotation): "",
},
}

replaceInner(&dummyResource, &dummyResource.TemplateData, dockerSecretReplacement)

expected := Resource{
TemplateData: map[string]interface{}{
"data": map[string]interface{}{
".dockerconfigjson": `eyJhdXRocyI6eyJodHRwczovL215LXNlcnZlci5sb2NhbCI6eyJhdXRoIjoiZEdWemRIVnpaWEk2ZEdWemRIQmhjM009IiwicGFzc3dvcmQiOiJ0ZXN0cGFzcyIsInVzZXJuYW1lIjoidGVzdHVzZXIifX19`,
},
},
Data: map[string]interface{}{
"user": "testuser",
"pass": "testpass",
},
replacementErrors: []error{},
}

assertSuccessfulReplacement(&dummyResource, &expected, t)
}

func TestDockerReplacement_Plain(t *testing.T) {
dummyResource := Resource{
TemplateData: map[string]interface{}{
"stringData": map[string]interface{}{
".dockerconfigjson": `{"auths":{"https://my-server.local":{"username":"\u003cuser\u003e","password":"\u003cpass\u003e","auth":"PHVzZXI+OjxwYXNzPg=="}}}`,
},
},
Data: map[string]interface{}{
"user": "testuser",
"pass": "testpass",
},
Annotations: map[string]string{
(types.AVPPathAnnotation): "",
},
}

replaceInner(&dummyResource, &dummyResource.TemplateData, dockerSecretReplacement)

expected := Resource{
TemplateData: map[string]interface{}{
"stringData": map[string]interface{}{
".dockerconfigjson": `{"auths":{"https://my-server.local":{"auth":"dGVzdHVzZXI6dGVzdHBhc3M=","password":"testpass","username":"testuser"}}}`,
},
},
Data: map[string]interface{}{
"user": "testuser",
"pass": "testpass",
},
replacementErrors: []error{},
}

assertSuccessfulReplacement(&dummyResource, &expected, t)
}

func TestStringify(t *testing.T) {
testCases := []struct {
input interface{}
Expand Down

0 comments on commit cdc3110

Please sign in to comment.