diff --git a/fixtures/input/nonempty/secret_dockerconfigjson.yaml b/fixtures/input/nonempty/secret_dockerconfigjson.yaml new file mode 100644 index 00000000..3c5a0e68 --- /dev/null +++ b/fixtures/input/nonempty/secret_dockerconfigjson.yaml @@ -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: -dockerconfigjson + namespace: +data: + .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL215LXNlcnZlci5sb2NhbCI6eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6Ilx1MDAzY3BhdGg6c2VjcmV0L3Rlc3Rpbmcjc2VjcmV0LXZhci12YWx1ZVx1MDAzZSIsImF1dGgiOiJkWE5sY2pvOGNHRjBhRHB6WldOeVpYUXZkR1Z6ZEdsdVp5TnpaV055WlhRdGRtRnlMWFpoYkhWbFBnPT0ifX19 diff --git a/fixtures/output/all.yaml b/fixtures/output/all.yaml index eea02833..1976bf73 100644 --- a/fixtures/output/all.yaml +++ b/fixtures/output/all.yaml @@ -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 diff --git a/pkg/kube/template.go b/pkg/kube/template.go index 4e4adb8a..99ef97a1 100644 --- a/pkg/kube/template.go +++ b/pkg/kube/template.go @@ -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 @@ -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, @@ -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 } diff --git a/pkg/kube/util.go b/pkg/kube/util.go index aeaccc20..84a362a5 100644 --- a/pkg/kube/util.go +++ b/pkg/kube/util.go @@ -12,6 +12,7 @@ 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" ) @@ -19,6 +20,10 @@ type missingKeyError struct { s string } +type dockerConfig struct { + Auths map[string](map[string]string) `json:"auths"` +} + func (e *missingKeyError) Error() string { return e.s } @@ -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: @@ -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 +} diff --git a/pkg/kube/util_test.go b/pkg/kube/util_test.go index b09fef90..dafe4b94 100644 --- a/pkg/kube/util_test.go +++ b/pkg/kube/util_test.go @@ -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{}