diff --git a/api/internal/generators/secret.go b/api/internal/generators/secret.go index 9afaff156a..3f02136799 100644 --- a/api/internal/generators/secret.go +++ b/api/internal/generators/secret.go @@ -4,6 +4,8 @@ package generators import ( + "fmt" + "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/yaml" @@ -11,14 +13,14 @@ import ( // MakeSecret makes a kubernetes Secret. // -// Secret: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#secret-v1-core +// Secret: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/secret-v1/ // // ConfigMaps and Secrets are similar. // // Like a ConfigMap, a Secret has a `data` field, but unlike a ConfigMap it has -// no `binaryData` field. +// no `binaryData` field. Secret also provides a `stringData` field. // -// All of a Secret's data is assumed to be opaque in nature, and assumed to be +// A Secret's `data` is assumed to be opaque in nature, and assumed to be // base64 encoded from its original representation, regardless of whether the // original data was UTF-8 text or binary. // @@ -26,6 +28,12 @@ import ( // represent opaque text and binary data. Beneath the base64 encoding // is presumably further encoding under control of the Secret's consumer. // +// A Secret's `stringData` field is similar to ConfigMap's `data` field. +// `stringData` allows specifying non-binary, UTF-8 secret data in string form. +// It is provided as a write-only input field for convenience. +// All keys and values are merged into the data field on write, overwriting any +// existing values. The stringData field is never output when reading from the API. +// // A Secret has string field `type` which holds an identifier, used by the // client, to choose the algorithm to interpret the `data` field. Kubernetes // cannot make use of this data; it's up to a controller or some pod's service @@ -50,8 +58,14 @@ func MakeSecret( if err != nil { return nil, err } - if err = rn.LoadMapIntoSecretData(m); err != nil { - return nil, err + if args.StringData { + if err = rn.LoadMapIntoSecretStringData(m); err != nil { + return nil, fmt.Errorf("Failed to load map into Secret stringData: %w", err) + } + } else { + if err = rn.LoadMapIntoSecretData(m); err != nil { + return nil, fmt.Errorf("Failed to load map into Secret data: %w", err) + } } copyLabelsAndAnnotations(rn, args.Options) setImmutable(rn, args.Options) diff --git a/api/internal/generators/secret_test.go b/api/internal/generators/secret_test.go index dfc13fc9ad..9ee05e4d05 100644 --- a/api/internal/generators/secret_test.go +++ b/api/internal/generators/secret_test.go @@ -193,6 +193,82 @@ data: c: SGVsbG8gV29ybGQ= d: dHJ1ZQ== immutable: true +`, + }, + }, + "construct secret from text file as stringData": { + args: types.SecretArgs{ + StringData: true, + GeneratorArgs: types.GeneratorArgs{ + Name: "fileSecret1", + KvPairSources: types.KvPairSources{ + FileSources: []string{ + filepath.Join("secret", "app-init.ini"), + }, + }, + }, + }, + exp: expected{ + out: `apiVersion: v1 +kind: Secret +metadata: + name: fileSecret1 +type: Opaque +stringData: + app-init.ini: | + FOO=bar + BAR=baz +`, + }, + }, + "construct secret from text and binary file with stringData and data": { + args: types.SecretArgs{ + StringData: true, + GeneratorArgs: types.GeneratorArgs{ + Name: "fileSecret2", + KvPairSources: types.KvPairSources{ + FileSources: []string{ + filepath.Join("secret", "app-init.ini"), + filepath.Join("secret", "app.bin"), + }, + }, + }, + }, + exp: expected{ + out: `apiVersion: v1 +kind: Secret +metadata: + name: fileSecret2 +type: Opaque +stringData: + app-init.ini: | + FOO=bar + BAR=baz +data: + app.bin: //0= +`, + }, + }, + "construct secret from a binary file and fallback to data from stringData": { + args: types.SecretArgs{ + StringData: true, + GeneratorArgs: types.GeneratorArgs{ + Name: "fileSecret2", + KvPairSources: types.KvPairSources{ + FileSources: []string{ + filepath.Join("secret", "app.bin"), + }, + }, + }, + }, + exp: expected{ + out: `apiVersion: v1 +kind: Secret +metadata: + name: fileSecret2 +type: Opaque +data: + app.bin: //0= `, }, }, diff --git a/api/krusty/secrets_test.go b/api/krusty/secrets_test.go new file mode 100644 index 0000000000..f4cf7f050d --- /dev/null +++ b/api/krusty/secrets_test.go @@ -0,0 +1,850 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package krusty_test + +import ( + "testing" + + kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" +) + +// Numbers and booleans are quoted +func TestSecretGeneratorIntVsStringNoMerge(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK(".", ` +resources: +- service.yaml +secretGenerator: +- name: bob + literals: + - fruit=Mango + - year=2025 + - crisis=true +`) + th.WriteF("service.yaml", ` +apiVersion: v1 +kind: Service +metadata: + name: demo +spec: + clusterIP: None +`) + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected( + m, ` +apiVersion: v1 +kind: Service +metadata: + name: demo +spec: + clusterIP: None +--- +apiVersion: v1 +data: + crisis: dHJ1ZQ== + fruit: TWFuZ28= + year: MjAyNQ== +kind: Secret +metadata: + name: bob-9kb2bk8b2g +type: Opaque +`) +} + +func TestSecretGeneratorIntVsStringWithMerge(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base", ` +secretGenerator: +- name: bob + literals: + - fruit=Mango + - year=2025 + - crisis=true +`) + th.WriteK("overlay", ` +resources: +- ../base +secretGenerator: +- name: bob + behavior: merge + literals: + - month=12 +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, `apiVersion: v1 +data: + crisis: dHJ1ZQ== + fruit: TWFuZ28= + month: MTI= + year: MjAyNQ== +kind: Secret +metadata: + name: bob-mf6f2t4b62 +type: Opaque +`) +} + +func TestSecretGeneratorFromProperties(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base", ` +secretGenerator: + - name: test-secret + behavior: create + envs: + - properties +`) + th.WriteF("base/properties", ` +VAR1=100 +`) + th.WriteK("overlay", ` +resources: +- ../base +secretGenerator: +- name: test-secret + behavior: "merge" + envs: + - properties +`) + th.WriteF("overlay/properties", ` +VAR2=200 +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + VAR1: MTAw + VAR2: MjAw +kind: Secret +metadata: + name: test-secret-c8c6d984gb +type: Opaque +`) +} + +func TestSecretGeneratorStringDataFromProperties(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base", ` +secretGenerator: + - name: test-secret + behavior: create + envs: + - properties +`) + th.WriteF("base/properties", ` +VAR1=100 +`) + th.WriteK("overlay", ` +resources: +- ../base +secretGenerator: +- name: test-secret + stringData: true + behavior: "merge" + envs: + - properties +`) + th.WriteF("overlay/properties", ` +VAR2=200 +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + VAR1: MTAw +kind: Secret +metadata: + name: test-secret-cbkdg78c5m +stringData: + VAR2: "200" +type: Opaque +`) +} + +// Generate Secrets similar to TestGeneratorBasics with stringData enabled and +// disabled. +func TestSecretGeneratorStringData(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK(".", ` +namePrefix: blah- +secretGenerator: +- name: bob + literals: + - fruit=apple + - vegetable=broccoli + envs: + - foo.env + env: bar.env + files: + - passphrase=phrase.dat + - forces.txt +- name: json + literals: + - 'v2=[{"path": "var/druid/segment-cache"}]' + - >- + druid_segmentCache_locations=[{"path": + "var/druid/segment-cache", + "maxSize": 32000000000, + "freeSpacePercent": 1.0}] +- name: bob-string-data + stringData: true + literals: + - fruit=apple + - vegetable=broccoli + envs: + - foo.env + env: bar.env + files: + - passphrase=phrase.dat + - forces.txt +- name: json-string-data + stringData: true + literals: + - 'v2=[{"path": "var/druid/segment-cache"}]' + - >- + druid_segmentCache_locations=[{"path": + "var/druid/segment-cache", + "maxSize": 32000000000, + "freeSpacePercent": 1.0}] +`) + th.WriteF("foo.env", ` +MOUNTAIN=everest +OCEAN=pacific +`) + th.WriteF("bar.env", ` +BIRD=falcon +`) + th.WriteF("phrase.dat", ` +Life is short. +But the years are long. +Not while the evil days come not. +`) + th.WriteF("forces.txt", ` +gravitational +electromagnetic +strong nuclear +weak nuclear +`) + opts := th.MakeDefaultOptions() + m := th.Run(".", opts) + th.AssertActualEqualsExpected( + m, ` +apiVersion: v1 +data: + BIRD: ZmFsY29u + MOUNTAIN: ZXZlcmVzdA== + OCEAN: cGFjaWZpYw== + forces.txt: | + CmdyYXZpdGF0aW9uYWwKZWxlY3Ryb21hZ25ldGljCnN0cm9uZyBudWNsZWFyCndlYWsgbn + VjbGVhcgo= + fruit: YXBwbGU= + passphrase: | + CkxpZmUgaXMgc2hvcnQuCkJ1dCB0aGUgeWVhcnMgYXJlIGxvbmcuCk5vdCB3aGlsZSB0aG + UgZXZpbCBkYXlzIGNvbWUgbm90Lgo= + vegetable: YnJvY2NvbGk= +kind: Secret +metadata: + name: blah-bob-58g62h555c +type: Opaque +--- +apiVersion: v1 +data: + druid_segmentCache_locations: | + W3sicGF0aCI6ICJ2YXIvZHJ1aWQvc2VnbWVudC1jYWNoZSIsICJtYXhTaXplIjogMzIwMD + AwMDAwMDAsICJmcmVlU3BhY2VQZXJjZW50IjogMS4wfV0= + v2: W3sicGF0aCI6ICJ2YXIvZHJ1aWQvc2VnbWVudC1jYWNoZSJ9XQ== +kind: Secret +metadata: + name: blah-json-5cdg9f2644 +type: Opaque +--- +apiVersion: v1 +kind: Secret +metadata: + name: blah-bob-string-data-5g7kgmf529 +stringData: + BIRD: falcon + MOUNTAIN: everest + OCEAN: pacific + forces.txt: |2 + + gravitational + electromagnetic + strong nuclear + weak nuclear + fruit: apple + passphrase: |2 + + Life is short. + But the years are long. + Not while the evil days come not. + vegetable: broccoli +type: Opaque +--- +apiVersion: v1 +kind: Secret +metadata: + name: blah-json-string-data-g7ktb2m7bm +stringData: + druid_segmentCache_locations: '[{"path": "var/druid/segment-cache", "maxSize": 32000000000, + "freeSpacePercent": 1.0}]' + v2: '[{"path": "var/druid/segment-cache"}]' +type: Opaque +`) +} + +// TODO: This should be an error instead. However, we can't strict unmarshal until we have a yaml +// lib that support case-insensitive keys and anchors. +// See https://github.com/kubernetes-sigs/kustomize/issues/5061 +func TestSecretGeneratorRepeatsInKustomization(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK(".", ` +namePrefix: blah- +secretGenerator: +- name: bob + behavior: create + literals: + - bean=pinto + - star=wolf-rayet + literals: + - fruit=apple + - vegetable=broccoli + files: + - forces.txt + files: + - nobles=nobility.txt +`) + th.WriteF("forces.txt", ` +gravitational +electromagnetic +strong nuclear +weak nuclear +`) + th.WriteF("nobility.txt", ` +helium +neon +argon +krypton +xenon +radon +`) + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + fruit: YXBwbGU= + nobles: CmhlbGl1bQpuZW9uCmFyZ29uCmtyeXB0b24KeGVub24KcmFkb24K + vegetable: YnJvY2NvbGk= +kind: Secret +metadata: + name: blah-bob-7dffd4g7cf +type: Opaque +`) +} + +func TestSecretIssue3393(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK(".", ` +resources: +- cm.yaml +secretGenerator: + - name: project + behavior: merge + literals: + - ANOTHER_ENV_VARIABLE="bar" +`) + th.WriteF("cm.yaml", ` +apiVersion: v1 +kind: Secret +metadata: + name: project +type: Opaque +data: + A_FIRST_ENV_VARIABLE: "foo" +`) + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + A_FIRST_ENV_VARIABLE: foo + ANOTHER_ENV_VARIABLE: YmFy +kind: Secret +metadata: + name: project +type: Opaque +`) +} + +func TestSecretGeneratorSimpleOverlay(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base", ` +namePrefix: p- +secretGenerator: +- name: cm + behavior: create + literals: + - fruit=apple +`) + th.WriteK("overlay", ` +resources: +- ../base +secretGenerator: +- name: cm + behavior: merge + literals: + - veggie=broccoli +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + fruit: YXBwbGU= + veggie: YnJvY2NvbGk= +kind: Secret +metadata: + name: p-cm-5fg9k7g895 +type: Opaque +`) +} + +func manyHunter2s(count int) (result []byte) { + binaryHunter2 := []byte{ + 0xff, // non-utf8 + 0x68, // h + 0x75, // u + 0x6e, // n + 0x74, // t + 0x65, // e + 0x72, // r + 0x32, // 2 + } + + for i := 0; i < count; i++ { + result = append(result, binaryHunter2...) + } + return +} + +func TestSecretGeneratorOverlaysBinaryData(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteF("base/data.bin", string(manyHunter2s(30))) + th.WriteK("base", ` +namePrefix: p1- +secretGenerator: +- name: com1 + behavior: create + files: + - data.bin +`) + th.WriteK("overlay", ` +resources: +- ../base +secretGenerator: +- name: com1 + behavior: merge +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + data.bin: | + /2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bn + RlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/ + aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudG + VyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9o + dW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy +kind: Secret +metadata: + name: p1-com1-4k9fgt2gct +type: Opaque +`) +} + +func TestSecretGeneratorOverlaysBinaryDataStringDataFallback(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteF("base/data.bin", string(manyHunter2s(30))) + th.WriteK("base", ` +namePrefix: p1- +secretGenerator: +- name: com1 + stringData: true + behavior: create + files: + - data.bin +`) + th.WriteK("overlay", ` +resources: +- ../base +secretGenerator: +- name: com1 + behavior: merge +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + data.bin: | + /2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bn + RlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/ + aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudG + VyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9o + dW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy +kind: Secret +metadata: + name: p1-com1-4k9fgt2gct +type: Opaque +`) +} + +func TestSecretGeneratorMixedStringDataFallback(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteF("base/data.bin", string(manyHunter2s(30))) + th.WriteF("base/forces.txt", ` +gravitational +electromagnetic +strong nuclear +weak nuclear +`) + th.WriteK("base", ` +namePrefix: p1- +secretGenerator: +- name: com1 + stringData: true + behavior: create + files: + - data.bin + - forces.txt + literals: + - fruit=Mango + - year=2025 + - crisis=true +`) + m := th.Run("base", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + data.bin: | + /2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bn + RlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/ + aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudG + VyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy/2h1bnRlcjL/aHVudGVyMv9o + dW50ZXIy/2h1bnRlcjL/aHVudGVyMv9odW50ZXIy +kind: Secret +metadata: + name: p1-com1-8k8d8b8g5b +stringData: + crisis: "true" + forces.txt: |2 + + gravitational + electromagnetic + strong nuclear + weak nuclear + fruit: Mango + year: "2025" +type: Opaque +`) +} + +func TestSecretGeneratorOverlays(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base1", ` +namePrefix: p1- +secretGenerator: +- name: com1 + behavior: create + literals: + - from=base +`) + th.WriteK("base2", ` +namePrefix: p2- +secretGenerator: +- name: com2 + behavior: create + literals: + - from=base +`) + th.WriteK("overlay/o1", ` +resources: +- ../../base1 +secretGenerator: +- name: com1 + behavior: merge + literals: + - from=overlay +`) + th.WriteK("overlay/o2", ` +resources: +- ../../base2 +secretGenerator: +- name: com2 + behavior: merge + literals: + - from=overlay +`) + th.WriteK("overlay", ` +resources: +- o1 +- o2 +secretGenerator: +- name: com1 + behavior: merge + literals: + - foo=bar + - baz=qux +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + baz: cXV4 + foo: YmFy + from: b3ZlcmxheQ== +kind: Secret +metadata: + name: p1-com1-9tg2879fh2 +type: Opaque +--- +apiVersion: v1 +data: + from: b3ZlcmxheQ== +kind: Secret +metadata: + name: p2-com2-b4g8g6529g +type: Opaque +`) +} + +// regression test for https://github.com/kubernetes-sigs/kustomize/issues/4233 +func TestSecretDataEndsWithQuotes(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK(".", ` +secretGenerator: + - name: test + literals: + - TEST=this is a 'test' +`) + + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected( + m, ` +apiVersion: v1 +data: + TEST: dGhpcyBpcyBhICd0ZXN0Jw== +kind: Secret +metadata: + name: test-tg88t27545 +type: Opaque +`) +} + +func TestSecretDataIsSingleQuote(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK(".", ` +secretGenerator: + - name: test + literals: + - TEST=' +`) + + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected( + m, ` +apiVersion: v1 +data: + TEST: Jw== +kind: Secret +metadata: + name: test-7dthck49k5 +type: Opaque +`) +} + +// Regression test for https://github.com/kubernetes-sigs/kustomize/issues/5047 +func TestSecretPrefixSuffix(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteF("kustomization.yaml", ` +resources: +- a +- b +`) + + th.WriteF("a/kustomization.yaml", ` +resources: +- ../common + +namePrefix: a +`) + + th.WriteF("b/kustomization.yaml", ` +resources: +- ../common + +namePrefix: b +`) + + th.WriteF("common/kustomization.yaml", ` +resources: +- service + +secretGenerator: +- name: "-example-secret" +`) + + th.WriteF("common/service/deployment.yaml", ` +kind: Deployment +apiVersion: apps/v1 + +metadata: + name: "-" + +spec: + template: + spec: + containers: + - name: app + envFrom: + - secretRef: + name: "-example-secret" +`) + + th.WriteF("common/service/kustomization.yaml", ` +resources: +- deployment.yaml + +nameSuffix: api +`) + + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: a-api +spec: + template: + spec: + containers: + - envFrom: + - secretRef: + name: a-example-secret-46f8b28mk5 + name: app +--- +apiVersion: v1 +data: {} +kind: Secret +metadata: + name: a-example-secret-46f8b28mk5 +type: Opaque +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: b-api +spec: + template: + spec: + containers: + - envFrom: + - secretRef: + name: b-example-secret-46f8b28mk5 + name: app +--- +apiVersion: v1 +data: {} +kind: Secret +metadata: + name: b-example-secret-46f8b28mk5 +type: Opaque +`) +} + +// Regression test for https://github.com/kubernetes-sigs/kustomize/issues/5047 +func TestSecretPrefixSuffix2(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteF("kustomization.yaml", ` +resources: +- a +- b +`) + + th.WriteF("a/kustomization.yaml", ` +resources: +- ../common + +namePrefix: a +`) + + th.WriteF("b/kustomization.yaml", ` +resources: +- ../common + +namePrefix: b +`) + + th.WriteF("common/deployment.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "-example" +spec: + template: + spec: + containers: + - name: app + envFrom: + - secretRef: + name: "-example-secret" +`) + + th.WriteF("common/kustomization.yaml", ` +resources: +- deployment.yaml + +secretGenerator: +- name: "-example-secret" +`) + + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: a-example +spec: + template: + spec: + containers: + - envFrom: + - secretRef: + name: a-example-secret-46f8b28mk5 + name: app +--- +apiVersion: v1 +data: {} +kind: Secret +metadata: + name: a-example-secret-46f8b28mk5 +type: Opaque +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: b-example +spec: + template: + spec: + containers: + - envFrom: + - secretRef: + name: b-example-secret-46f8b28mk5 + name: app +--- +apiVersion: v1 +data: {} +kind: Secret +metadata: + name: b-example-secret-46f8b28mk5 +type: Opaque +`) +} diff --git a/api/resmap/reswrangler.go b/api/resmap/reswrangler.go index f6443539f1..158ad4de95 100644 --- a/api/resmap/reswrangler.go +++ b/api/resmap/reswrangler.go @@ -591,6 +591,7 @@ func (m *resWrangler) appendReplaceOrMerge(res *resource.Resource) error { res.CopyMergeMetaDataFieldsFrom(old) res.MergeDataMapFrom(old) res.MergeBinaryDataMapFrom(old) + res.MergeStringDataMapFrom(old) if orig != nil { res.SetOrigin(orig) } diff --git a/api/resource/resource.go b/api/resource/resource.go index 9884a672c5..8ef01e50b6 100644 --- a/api/resource/resource.go +++ b/api/resource/resource.go @@ -157,7 +157,7 @@ func (r *Resource) DeepCopy() *Resource { // the resource. // TODO: move to RNode, use GetMeta to improve performance. // TODO: make a version of mergeStringMaps that is build-annotation aware -// to avoid repeatedly setting refby and genargs annotations +// to avoid repeatedly setting refby and genargs annotations // Must remove the kustomize bit at the end. func (r *Resource) CopyMergeMetaDataFieldsFrom(other *Resource) error { if err := r.SetLabels( @@ -197,6 +197,10 @@ func (r *Resource) MergeBinaryDataMapFrom(o *Resource) { r.SetBinaryDataMap(mergeStringMaps(o.GetBinaryDataMap(), r.GetBinaryDataMap())) } +func (r *Resource) MergeStringDataMapFrom(o *Resource) { + r.SetStringDataMap(mergeStringMaps(o.GetStringDataMap(), r.GetStringDataMap())) +} + func (r *Resource) ErrIfNotEquals(o *Resource) error { meYaml, err := r.AsYAML() if err != nil { diff --git a/api/types/secretargs.go b/api/types/secretargs.go index 62dbe26a73..94542134d5 100644 --- a/api/types/secretargs.go +++ b/api/types/secretargs.go @@ -16,4 +16,10 @@ type SecretArgs struct { // If type is "kubernetes.io/tls", then "literals" or "files" must have exactly two // keys: "tls.key" and "tls.crt" Type string `json:"type,omitempty" yaml:"type,omitempty"` + + // StringData if true generates a v1/Secret with plain-text stringData fields + // instead of base64-encoded data fields. If any fields are not UTF-8, they + // are still base64-encoded and stored as data as a fallback behavior. This + // is similar to the default behavior of a ConfigMap. + StringData bool `json:"stringData,omitempty" yaml:"stringData,omitempty"` } diff --git a/kyaml/yaml/const.go b/kyaml/yaml/const.go index 6a2cc45166..da43ccf0e4 100644 --- a/kyaml/yaml/const.go +++ b/kyaml/yaml/const.go @@ -24,6 +24,7 @@ const ( MetadataField = "metadata" DataField = "data" BinaryDataField = "binaryData" + StringDataField = "stringData" NameField = "name" NamespaceField = "namespace" LabelsField = "labels" diff --git a/kyaml/yaml/datamap.go b/kyaml/yaml/datamap.go index f4b7e6664e..1a22ed54d8 100644 --- a/kyaml/yaml/datamap.go +++ b/kyaml/yaml/datamap.go @@ -95,6 +95,35 @@ func makeSecretValueRNode(s string) *RNode { return NewRNode(yN) } +func (rn *RNode) LoadMapIntoSecretStringData(m map[string]string) error { + for _, k := range SortedMapKeys(m) { + fldName, vrN := makeSecretStringValueRNode(m[k]) + if _, err := rn.Pipe( + LookupCreate(MappingNode, fldName), + SetField(k, vrN)); err != nil { + return err + } + } + return nil +} + +// valid UTF-8 strings can be stringData, but fallback to Base64 for non UTF-8 +func makeSecretStringValueRNode(s string) (field string, rN *RNode) { + yN := &Node{Kind: ScalarNode} + yN.Tag = NodeTagString + if utf8.ValidString(s) { + field = StringDataField + yN.Value = s + } else { + field = DataField + yN.Value = encodeBase64(s) + } + if strings.Contains(yN.Value, "\n") { + yN.Style = LiteralStyle + } + return field, NewRNode(yN) +} + // encodeBase64 encodes s as base64 that is broken up into multiple lines // as appropriate for the resulting length. func encodeBase64(s string) string { diff --git a/kyaml/yaml/rnode.go b/kyaml/yaml/rnode.go index 07c782d730..1086017a87 100644 --- a/kyaml/yaml/rnode.go +++ b/kyaml/yaml/rnode.go @@ -632,6 +632,19 @@ func (rn *RNode) GetBinaryDataMap() map[string]string { return result } +func (rn *RNode) GetStringDataMap() map[string]string { + n, err := rn.Pipe(Lookup(StringDataField)) + if err != nil { + return nil + } + result := map[string]string{} + _ = n.VisitFields(func(node *MapNode) error { + result[GetValue(node.Key)] = GetValue(node.Value) + return nil + }) + return result +} + // GetValidatedDataMap retrieves the data map and returns an error if the data // map contains entries which are not included in the expectedKeys set. func (rn *RNode) GetValidatedDataMap(expectedKeys []string) (map[string]string, error) { @@ -688,6 +701,21 @@ func (rn *RNode) SetBinaryDataMap(m map[string]string) { } } +func (rn *RNode) SetStringDataMap(m map[string]string) { + if rn == nil { + log.Fatal("cannot set stringData map on nil Rnode") + } + if err := rn.PipeE(Clear(StringDataField)); err != nil { + log.Fatal(err) + } + if len(m) == 0 { + return + } + if err := rn.LoadMapIntoSecretStringData(m); err != nil { + log.Fatal(err) + } +} + // AppendToFieldPath appends a field name to the FieldPath. func (rn *RNode) AppendToFieldPath(parts ...string) { rn.fieldPath = append(rn.fieldPath, parts...)