From 3dc123346d3c30f513aae51fd726e9cc4cbfa6f3 Mon Sep 17 00:00:00 2001 From: Ramon Quitales Date: Sat, 22 Jul 2023 04:00:24 +1200 Subject: [PATCH] fix: ensure data is not dropped when normalizing Secrets (#2514) Co-authored-by: Levi Blackstone --- CHANGELOG.md | 4 ++ provider/pkg/clients/unstructured.go | 31 ++++++----- tests/sdk/nodejs/nodejs_test.go | 37 +++++++++++++ tests/sdk/nodejs/secrets/step1/index.ts | 1 + tests/sdk/nodejs/secrets/step2/index.ts | 69 +++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 tests/sdk/nodejs/secrets/step2/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fba9e664a7..2bfc920580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +## 4.0.3 (July 21, 2023) + +- fix: ensure data is not dropped when normalizing Secrets (https://github.com/pulumi/pulumi-kubernetes/pull/2514) + ## 4.0.2 (July 20, 2023) - [sdk/python] Drop unused pyyaml dependency (https://github.com/pulumi/pulumi-kubernetes/pull/2502) diff --git a/provider/pkg/clients/unstructured.go b/provider/pkg/clients/unstructured.go index 1c2019ca81..0eb85c958d 100644 --- a/provider/pkg/clients/unstructured.go +++ b/provider/pkg/clients/unstructured.go @@ -15,6 +15,7 @@ package clients import ( + "encoding/base64" "fmt" "github.com/pulumi/pulumi-kubernetes/provider/v4/pkg/kinds" @@ -115,30 +116,28 @@ func normalizeCRD(uns *unstructured.Unstructured) *unstructured.Unstructured { func normalizeSecret(uns *unstructured.Unstructured) *unstructured.Unstructured { contract.Assertf(IsSecret(uns), "normalizeSecret called on a non-Secret resource: %s:%s", uns.GetAPIVersion(), uns.GetKind()) - obj, err := FromUnstructured(uns) - if err != nil { - return uns // If the operation fails, just return the original object + stringData, found, err := unstructured.NestedStringMap(uns.Object, "stringData") + if err != nil || !found { + return uns + } + + data, found, err := unstructured.NestedMap(uns.Object, "data") + if err != nil || !found { + data = map[string]any{} } - secret := obj.(*corev1.Secret) // See https://github.com/kubernetes/kubernetes/blob/v1.27.4/pkg/apis/core/v1/conversion.go#L406-L414 // StringData overwrites Data - if len(secret.StringData) > 0 { - if secret.Data == nil { - secret.Data = map[string][]byte{} - } - for k, v := range secret.StringData { - secret.Data[k] = []byte(v) + if len(stringData) > 0 { + for k, v := range stringData { + data[k] = base64.StdEncoding.EncodeToString([]byte(v)) } - secret.StringData = nil + contract.IgnoreError(unstructured.SetNestedMap(uns.Object, data, "data")) + unstructured.RemoveNestedField(uns.Object, "stringData") } - updated, err := ToUnstructured(secret) - if err != nil { - return uns // If the operation fails, just return the original object - } - return updated + return uns } func PodFromUnstructured(uns *unstructured.Unstructured) (*corev1.Pod, error) { diff --git a/tests/sdk/nodejs/nodejs_test.go b/tests/sdk/nodejs/nodejs_test.go index f743642bfd..2170641aa3 100644 --- a/tests/sdk/nodejs/nodejs_test.go +++ b/tests/sdk/nodejs/nodejs_test.go @@ -1057,12 +1057,49 @@ func TestSecrets(t *testing.T) { state, err := json.Marshal(stackInfo.Deployment) assert.NoError(t, err) + ssStringDataData, ok := stackInfo.Outputs["ssStringDataData"] + assert.Truef(t, ok, "missing expected output \"ssStringDataData\"") + + ssStringDataStringData, ok := stackInfo.Outputs["ssStringDataStringData"] + assert.Truef(t, ok, "missing expected output \"ssStringDataStringData\"") + + assert.NotEmptyf(t, ssStringDataData, "data field is empty") + assert.NotEmptyf(t, ssStringDataStringData, "stringData field is empty") + assert.NotContains(t, string(state), secretMessage) // The program converts the secret message to base64, to make a ConfigMap from it, so the state // should also not contain the base64 encoding of secret message. assert.NotContains(t, string(state), b64.StdEncoding.EncodeToString([]byte(secretMessage))) }, + EditDirs: []integration.EditDir{ + { + Dir: filepath.Join("secrets", "step2"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + secretMessage += "updated" + + assert.NotNil(t, stackInfo.Deployment) + state, err := json.Marshal(stackInfo.Deployment) + assert.NoError(t, err) + + ssStringDataData, ok := stackInfo.Outputs["ssStringDataData"] + assert.Truef(t, ok, "missing expected output \"ssStringDataData\"") + + ssStringDataStringData, ok := stackInfo.Outputs["ssStringDataStringData"] + assert.Truef(t, ok, "missing expected output \"ssStringDataStringData\"") + + assert.NotEmptyf(t, ssStringDataData, "data field is empty") + assert.NotEmptyf(t, ssStringDataStringData, "stringData field is empty") + + assert.NotContains(t, string(state), secretMessage) + + // The program converts the secret message to base64, to make a ConfigMap from it, so the state + // should also not contain the base64 encoding of secret message. + assert.NotContains(t, string(state), b64.StdEncoding.EncodeToString([]byte(secretMessage))) + }, + }, + }, }) integration.ProgramTest(t, &test) } diff --git a/tests/sdk/nodejs/secrets/step1/index.ts b/tests/sdk/nodejs/secrets/step1/index.ts index b138115fc2..40b5a44361 100644 --- a/tests/sdk/nodejs/secrets/step1/index.ts +++ b/tests/sdk/nodejs/secrets/step1/index.ts @@ -63,6 +63,7 @@ const cg = new k8s.yaml.ConfigGroup("example", { export const cmDataData = cmData.data; export const cmBinaryDataData = cmBinaryData.binaryData; +export const ssStringDataStringData = ssStringData.stringData; export const ssStringDataData = ssStringData.data; export const ssDataData = ssData.data; export const cgSecret = cg.getResource("v1/Secret", name).stringData; diff --git a/tests/sdk/nodejs/secrets/step2/index.ts b/tests/sdk/nodejs/secrets/step2/index.ts new file mode 100644 index 0000000000..ea389a3787 --- /dev/null +++ b/tests/sdk/nodejs/secrets/step2/index.ts @@ -0,0 +1,69 @@ +// Copyright 2016-2022, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; + +const config = new pulumi.Config(); + +const pw = config.requireSecret("message"); +const rawPW = config.require("message")+"updated"; // Add suffix + +const provider = new k8s.Provider("k8s"); + +const cmData = new k8s.core.v1.ConfigMap("cmdata", { + data: { + password: pw, + } +}, {provider}); + +const cmBinaryData = new k8s.core.v1.ConfigMap("cmbinarydata", { + binaryData: { + password: pw.apply(d => Buffer.from(d).toString("base64")), + } +}, {provider}); + +const ssStringData = new k8s.core.v1.Secret("ssstringdata", { + stringData: { + password: rawPW, + } +}, {provider}); + +const ssData = new k8s.core.v1.Secret("ssdata", { + data: { + password: Buffer.from(rawPW).toString("base64"), + } +}, {provider}); + +const randSuffix = Math.random().toString(36).substring(7); +const name = `test-${randSuffix}`; + +const secretYaml = ` +apiVersion: v1 +kind: Secret +metadata: + name: ${name} +stringData: + password: ${rawPW} +` +const cg = new k8s.yaml.ConfigGroup("example", { + yaml: secretYaml, +}, {provider}); + +export const cmDataData = cmData.data; +export const cmBinaryDataData = cmBinaryData.binaryData; +export const ssStringDataData = ssStringData.data; +export const ssStringDataStringData = ssStringData.stringData; +export const ssDataData = ssData.data; +export const cgSecret = cg.getResource("v1/Secret", name).stringData;