Skip to content

Commit

Permalink
test: Add integration test for SSA updating empty labels/annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
rquitales committed Mar 16, 2023
1 parent 78c61ae commit dc76730
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 0 deletions.
68 changes: 68 additions & 0 deletions tests/sdk/nodejs/nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -1092,6 +1093,73 @@ func TestServerSideApply(t *testing.T) {
integration.ProgramTest(t, &test)
}

// TestServerSideApplyEmptyMaps tests that we correctly handle merging structs containing empty maps when diffing live and wanted
// states. This is a regression test for issue #2332 to ensure Pulumi can handle updating a resource which has a
// map field that is empty in the live state but non-empty in the wanted state.
func TestServerSideApplyEmptyMaps(t *testing.T) {
var ns, cmName string

applyStep := baseOptions.With(integration.ProgramTestOptions{
Dir: filepath.Join("server-side-apply-empty-maps", "configmap"),
ExpectRefreshChanges: true,
// Enable destroy-on-cleanup so we can shell out to kubectl to make external changes to the resource and reuse the same stack.
DestroyOnCleanup: true,
OrderedConfig: []integration.ConfigValue{
{
Key: "pulumi:disable-default-providers[0]",
Value: "kubernetes",
Path: true,
},
},
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
cm := stackInfo.Outputs["cm"].(map[string]interface{})
// Save the name and namespace for later use with kubectl. We check that the vars are empty,
// in case pulumi up creates a new ConfigMap/Namespace instead of updating the existing one on
// subsequent runs.
if ns != "" && cmName != "" {
ns = cm["metadata"].(map[string]interface{})["namespace"].(string)
cmName = cm["metadata"].(map[string]interface{})["name"].(string)
}

// Validate we applied ConfigMap with wanted labels.
fooV, ok, err := unstructured.NestedString(cm, "metadata", "labels", "foo")
assert.True(t, ok)
assert.NoError(t, err)
assert.Equal(t, "bar", fooV)
},
})

// Use manual lifecycle management since we need to run external commands in between pulumi up steps, while referencing
// the same stack.
pt := integration.ProgramTestManualLifeCycle(t, &applyStep)
err := pt.TestLifeCycleInitAndDestroy()
assert.NoError(t, err)

// Sanity check with kubectl to verify that the ConfigMap was created with the wanted label.
out, err := exec.Command("kubectl", "get", "configmap", "-o", "yaml", "-n", ns, cmName).CombinedOutput()
assert.NoError(t, err)
assert.Contains(t, string(out), "bar") // ConfigMap should have been created with label foo=bar.

// Update the ConfigMap and remove label using kubectl.
out, err = exec.Command("kubectl", "label", "configmap", "-n", ns, cmName, "foo-").CombinedOutput()
assert.NoError(t, err)
assert.Contains(t, string(out), "configmap/"+cmName+" unlabeled") // Ensure CM was unlabeled.

// Use kubectl to verify that the ConfigMap was updated and no longer has the label.
out, err = exec.Command("kubectl", "get", "configmap", "-o", "yaml", "-n", ns, cmName).CombinedOutput()
assert.NoError(t, err)
assert.NotContains(t, string(out), "bar") // ConfigMap should no longer have label foo=bar.

// Re-run `pulumi up --refresh` to update the ConfigMap and re-add the label.
err = pt.TestPreviewUpdateAndEdits()
assert.NoError(t, err)

// Use kubectl to verify that the ConfigMap was updated and has the label again.
out, err = exec.Command("kubectl", "get", "configmap", "-o", "yaml", "-n", ns, cmName).CombinedOutput()
assert.NoError(t, err)
assert.Contains(t, string(out), "bar") // ConfigMap should have been updated with label foo=bar.
}

func TestYAMLURL(t *testing.T) {
test := baseOptions.With(integration.ProgramTestOptions{
Dir: filepath.Join("yaml-url", "step1"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: server-side-apply-empty-maps-tests
description: Tests Server-side Apply support with proper handling of empty map fields
runtime: nodejs
35 changes: 35 additions & 0 deletions tests/sdk/nodejs/server-side-apply-empty-maps/configmap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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 k8s from "@pulumi/kubernetes";

// This test creates a Provider with `enableServerSideApply` enabled. The following scenarios are tested:
// 1. Create a namespace and ConfigMap with pulumi.
// 2. Externally delete labels in the ConfigMap using kubectl.
// 3. Rerun the pulumi program and verify that the labels are restored.

// Create provider with SSA enabled.
const provider = new k8s.Provider("k8s", {enableServerSideApply: true});

// Create a randomly-named Namespace.
const ns = new k8s.core.v1.Namespace("test", undefined, {provider});

export const cm = new k8s.core.v1.ConfigMap("test", {
metadata: {
name: "foo",
namespace: ns.metadata.name,
labels: {foo: "bar"}, // This is the field of interest.
},
data: {dataKey: "fake data"},
}, {provider});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "server-side-apply-empty-maps",
"version": "0.1.0",
"dependencies": {
"@pulumi/pulumi": "latest",
"@pulumi/random": "latest"
},
"peerDependencies": {
"@pulumi/kubernetes": "latest"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}

0 comments on commit dc76730

Please sign in to comment.