diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 9df833a43..81a05240c 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -484,7 +484,15 @@ func rdelete(c *Client, resources ResourceList, propagation metav1.DeletionPropa mtx := sync.Mutex{} err := perform(resources, func(info *resource.Info) error { c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind) - err := deleteResource(info, propagation) + propagationPolicy := propagation + if annotations, err := metadataAccessor.Annotations(info.Object); err != nil { + c.Log("Unable to get annotations on %q, err: %s", info.Name, err) + errs = append(errs, err) + } else if annotations != nil && annotations[ResourceDeletionPolicyAnno] != "" { + propagationPolicy = selectDeletionPolicy(annotations[ResourceDeletionPolicyAnno], propagation) + } + + err := deleteResource(info, propagationPolicy) if err == nil || apierrors.IsNotFound(err) { if err != nil { c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err) diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 55aa5d8ed..969419c7b 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -213,6 +213,93 @@ func TestUpdate(t *testing.T) { } } +func TestDelete(t *testing.T) { + listA := newPodList("starfish", "otter", "squid", "jellyfish") + listA.Items[0].Annotations = map[string]string{ + ResourceDeletionPolicyAnno: "foreground", + } + listA.Items[1].Annotations = map[string]string{ + ResourceDeletionPolicyAnno: "orphan", + } + listA.Items[2].Annotations = map[string]string{ + ResourceDeletionPolicyAnno: "background", + } + listA.Items[3].Annotations = map[string]string{ + ResourceDeletionPolicyAnno: "oops", + } + + var actions []string + + c := newTestClient(t) + c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + p, m := req.URL.Path, req.Method + b, err := io.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + req.Body.Close() + actions = append(actions, p+":"+m) + t.Logf("got request %s %s", p, m) + switch { + case p == "/namespaces/default/pods/jellyfish" && m == "DELETE" && string(b) == "{\"propagationPolicy\":\"Background\"}\n": + return newResponse(200, &listA.Items[3]) + case p == "/namespaces/default/pods/squid" && m == "DELETE" && string(b) == "{\"propagationPolicy\":\"Background\"}\n": + return newResponse(200, &listA.Items[2]) + case p == "/namespaces/default/pods/otter" && m == "DELETE" && string(b) == "{\"propagationPolicy\":\"Orphan\"}\n": + return newResponse(200, &listA.Items[1]) + case p == "/namespaces/default/pods/starfish" && m == "DELETE" && string(b) == "{\"propagationPolicy\":\"Foreground\"}\n": + return newResponse(200, &listA.Items[0]) + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) + return nil, nil + } + }), + } + first, err := c.Build(objBody(&listA), false) + if err != nil { + t.Fatal(err) + } + + result, errs := c.Delete(first) + if len(errs) > 0 { + t.Fatal(errs) + } + + if len(result.Created) != 0 { + t.Errorf("expected 0 resource created, got %d", len(result.Created)) + } + if len(result.Updated) != 0 { + t.Errorf("expected 0 resource updated, got %d", len(result.Updated)) + } + if len(result.Deleted) != 4 { + t.Errorf("expected 4 resource deleted, got %d", len(result.Deleted)) + } + + expectedActions := []string{ + "/namespaces/default/pods/squid:DELETE", + "/namespaces/default/pods/starfish:DELETE", + "/namespaces/default/pods/otter:DELETE", + "/namespaces/default/pods/jellyfish:DELETE", + } + if len(expectedActions) != len(actions) { + t.Fatalf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) + } + for _, v := range expectedActions { + found := false + for _, action := range actions { + if action == v { + found = true + } + } + + if !found { + t.Errorf("expected %s request got %#v", v, actions) + } + } +} + func TestBuild(t *testing.T) { tests := []struct { name string diff --git a/pkg/kube/resource_delete_policy.go b/pkg/kube/resource_delete_policy.go new file mode 100644 index 000000000..4bca9ee9b --- /dev/null +++ b/pkg/kube/resource_delete_policy.go @@ -0,0 +1,40 @@ +/* +Copyright The Helm Authors. + +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. +*/ + +package kube // import "helm.sh/helm/v3/pkg/kube" + +import ( + "strings" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ResourceDeletionPolicyAnno = "helm.sh/resource-deletion-policy" + +// selectDeletionPolicy allows to select an override deleteion policy per resource, +// based on ResourceDeletionPolicyAnno. +func selectDeletionPolicy(policyAnnotation string, defualt v1.DeletionPropagation) v1.DeletionPropagation { + switch policyAnnotation { + case strings.ToLower(string(v1.DeletePropagationBackground)): + return v1.DeletePropagationBackground + case strings.ToLower(string(v1.DeletePropagationForeground)): + return v1.DeletePropagationForeground + case strings.ToLower(string(v1.DeletePropagationOrphan)): + return v1.DeletePropagationOrphan + default: + return defualt + } +}