Skip to content

Commit

Permalink
fix: ensure data is not dropped when normalizing Secrets (#2514)
Browse files Browse the repository at this point in the history
Co-authored-by: Levi Blackstone <[email protected]>
  • Loading branch information
rquitales and lblackstone authored Jul 21, 2023
1 parent 26ddf79 commit 3dc1233
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
31 changes: 15 additions & 16 deletions provider/pkg/clients/unstructured.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package clients

import (
"encoding/base64"
"fmt"

"github.com/pulumi/pulumi-kubernetes/provider/v4/pkg/kinds"
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 37 additions & 0 deletions tests/sdk/nodejs/nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions tests/sdk/nodejs/secrets/step1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
69 changes: 69 additions & 0 deletions tests/sdk/nodejs/secrets/step2/index.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 3dc1233

Please sign in to comment.