Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c0839d3

Browse files
committedJan 3, 2025·
Don't delete resources unless field managers match
1 parent b0ab343 commit c0839d3

File tree

7 files changed

+253
-0
lines changed

7 files changed

+253
-0
lines changed
 

‎provider/pkg/await/await.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
4949
"k8s.io/apimachinery/pkg/runtime/schema"
5050
"k8s.io/apimachinery/pkg/types"
51+
"k8s.io/apimachinery/pkg/util/sets"
5152
"k8s.io/apimachinery/pkg/watch"
5253
"k8s.io/apiserver/pkg/storage/names"
5354
"k8s.io/client-go/dynamic"
@@ -834,6 +835,27 @@ func Deletion(c DeleteConfig) error {
834835
PropagationPolicy: &deletePolicy,
835836
}
836837

838+
live, err := client.Get(c.Context, c.Name, metav1.GetOptions{})
839+
if err != nil {
840+
return nilIfGVKDeleted(err)
841+
}
842+
843+
actualSSAManagers := sets.Set[string]{}
844+
for _, f := range live.GetManagedFields() {
845+
// Ignore fields not managed by pulumi SSA.
846+
if !strings.HasPrefix(f.Manager, "pulumi-kubernetes-") {
847+
continue
848+
}
849+
actualSSAManagers = actualSSAManagers.Insert(f.Manager)
850+
}
851+
if c.ServerSideApply && !actualSSAManagers.Has(c.FieldManager) {
852+
// Didn't find our expected manager on the object. Assume it was
853+
// upserted by another manager and refuse to delete it. For the sake of
854+
// the program's state, report that it has been deleted since we are no
855+
// longer managing it.
856+
return nil
857+
}
858+
837859
err = client.Delete(c.Context, c.Name, deleteOpts)
838860
if err != nil {
839861
return nilIfGVKDeleted(err)

‎provider/pkg/await/await_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,34 @@ func TestDeletion(t *testing.T) {
787787
}
788788
}
789789

790+
field := []byte(`{
791+
"f:metadata": {
792+
"f:generateName": {},
793+
"f:labels": {},
794+
"f:ownerReferences": {}
795+
}
796+
}`)
797+
798+
original := validPodUnstructured.DeepCopy()
799+
original.SetManagedFields([]metav1.ManagedFieldsEntry{{
800+
Manager: "pulumi-kubernetes-123",
801+
Operation: "Update",
802+
FieldsType: "FieldsV1",
803+
FieldsV1: &metav1.FieldsV1{
804+
Raw: field,
805+
},
806+
}})
807+
808+
upserted := validPodUnstructured.DeepCopy()
809+
upserted.SetManagedFields([]metav1.ManagedFieldsEntry{{
810+
Manager: "pulumi-kubernetes-XYZ",
811+
Operation: "Update",
812+
FieldsType: "FieldsV1",
813+
FieldsV1: &metav1.FieldsV1{
814+
Raw: field,
815+
},
816+
}})
817+
790818
tests := []struct {
791819
name string
792820
client client
@@ -909,6 +937,19 @@ func TestDeletion(t *testing.T) {
909937
condition: awaitNoop,
910938
expect: []expectF{succeeded()},
911939
},
940+
{
941+
name: "Field manager mismatch",
942+
args: args{
943+
resType: tokens.Type("kubernetes:core/v1:Pod"),
944+
name: "foo",
945+
objects: []runtime.Object{original},
946+
inputs: original,
947+
outputs: original,
948+
serverSideApply: true,
949+
},
950+
condition: awaitNoop,
951+
expect: []expectF{succeeded()},
952+
},
912953
}
913954

914955
for _, tt := range tests {

‎tests/sdk/java/delete_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2025, Pulumi Corporation.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package test
16+
17+
import (
18+
"context"
19+
"testing"
20+
"time"
21+
22+
"github.com/pulumi/providertest/pulumitest"
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func TestDeleteDueToRename(t *testing.T) {
28+
t.Parallel()
29+
ctx := context.Background()
30+
test := pulumitest.NewPulumiTest(t, "testdata/delete/rename")
31+
t.Cleanup(func() {
32+
test.Destroy(t)
33+
})
34+
35+
test.Up(t)
36+
37+
// Change our Pod's resource name
38+
test.UpdateSource(t, "testdata/delete/rename/step2")
39+
test.Up(t)
40+
41+
// Renaming the namespace should not have deleted it. Perform a refresh and
42+
// make sure our pod is still running -- if it's not, Pulumi will have
43+
// deleted it from our state.
44+
refresh, err := test.CurrentStack().Refresh(ctx)
45+
assert.NoError(t, err)
46+
assert.NotContains(t, refresh.StdOut, "deleted", refresh.StdOut)
47+
}
48+
49+
func TestDeletePatchResource(t *testing.T) {
50+
t.Parallel()
51+
ctx := context.Background()
52+
test := pulumitest.NewPulumiTest(t, "testdata/delete/patch")
53+
t.Cleanup(func() {
54+
test.Destroy(t)
55+
})
56+
57+
test.Up(t)
58+
59+
time.Sleep(60 * time.Second)
60+
61+
outputs, err := test.CurrentStack().Outputs(ctx)
62+
require.NoError(t, err)
63+
64+
// The ConfigMap should have 2 managed fields.
65+
mf, ok := outputs["managedFields"]
66+
require.True(t, ok)
67+
assert.Len(t, mf.Value, 2)
68+
69+
// Delete a patch.
70+
test.UpdateSource(t, "testdata/delete/patch/step2")
71+
test.Up(t)
72+
73+
// One ConfigMapPatch should still be applied.
74+
outputs, err = test.CurrentStack().Outputs(ctx)
75+
require.NoError(t, err)
76+
mf, ok = outputs["managedFields"]
77+
require.True(t, ok)
78+
assert.Len(t, mf.Value, 1)
79+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: delete-patch-resource
2+
runtime: yaml
3+
description: |
4+
Deleting a logical patch resource should not delete the underlying physical
5+
resource.
6+
7+
outputs:
8+
managedFields: ${patch2.metadata.managedFields}
9+
10+
resources:
11+
configmap:
12+
type: kubernetes:core/v1:ConfigMap
13+
properties:
14+
metadata:
15+
name: patched-configmap
16+
17+
patch1:
18+
type: kubernetes:core/v1:ConfigMapPatch
19+
properties:
20+
metadata:
21+
name: patched-configmap
22+
data:
23+
foo: bar
24+
options:
25+
dependsOn:
26+
- ${configmap}
27+
28+
patch2:
29+
type: kubernetes:core/v1:ConfigMapPatch
30+
properties:
31+
metadata:
32+
name: patched-configmap
33+
data:
34+
boo: baz
35+
options:
36+
dependsOn:
37+
- ${patch1}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: delete-patch-resource
2+
runtime: yaml
3+
description: |
4+
Deleting a logical patch resource should not delete the underlying physical
5+
resource.
6+
7+
outputs:
8+
managedFields: ${patch1.metadata.managedFields}
9+
10+
resources:
11+
configmap:
12+
type: kubernetes:core/v1:ConfigMap
13+
properties:
14+
metadata:
15+
name: patched-configmap
16+
17+
patch1:
18+
type: kubernetes:core/v1:ConfigMapPatch
19+
properties:
20+
metadata:
21+
name: patched-configmap
22+
data:
23+
foo: bar
24+
options:
25+
dependsOn:
26+
- ${configmap}
27+
28+
# Delete patch2 - the underlying ConfigMap should not be deleted, and patch1
29+
# should still be applied.
30+
#
31+
# patch2:
32+
# type: kubernetes:core/v1:ConfigMapPatch
33+
# properties:
34+
# metadata:
35+
# name: patched-configmap
36+
# data:
37+
# boo: baz
38+
# options:
39+
# dependsOn:
40+
# - ${patch1}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: delete-with-rename
2+
runtime: yaml
3+
description: |
4+
Changing a resource's name, but leaving .metadata untouched, should not
5+
result in a deletion from the cluster.
6+
7+
resources:
8+
pod:
9+
type: kubernetes:core/v1:Pod
10+
properties:
11+
spec:
12+
containers:
13+
- image: nginx:1.14.2
14+
name: nginx
15+
ports:
16+
- containerPort: 80
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: delete-with-rename
2+
runtime: yaml
3+
description: |
4+
Changing a resource's name, but leaving .metadata untouched, should not
5+
result in a deletion from the cluster.
6+
7+
resources:
8+
# Change the resource's name from "pod" to "mypod" but leave everything
9+
# else the same.
10+
mypod:
11+
type: kubernetes:core/v1:Pod
12+
properties:
13+
spec:
14+
containers:
15+
- image: nginx:1.14.2
16+
name: nginx
17+
ports:
18+
- containerPort: 80

0 commit comments

Comments
 (0)
Please sign in to comment.