Skip to content

Commit

Permalink
annotate resources for deletion
Browse files Browse the repository at this point in the history
Signed-off-by: Mahmoud Gaballah <mahmoud.gaballah@zalando.de>
  • Loading branch information
myaser committed Jan 16, 2023
1 parent f0752b5 commit 7c6b0b9
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 34 deletions.
43 changes: 42 additions & 1 deletion provisioner/clusterpy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provisioner
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -36,6 +37,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/restmapper"
Expand Down Expand Up @@ -1113,8 +1115,11 @@ func performDeletion(ctx context.Context, logger *log.Entry, client dynamic.Inte
} else {
iface = client.Resource(gvr)
}

if deletion.Name != "" {
err := overrideDeletionProtection(ctx, deletion.Kind, deletion.Name, iface)
if err != nil {
return err
}
return deleteResource(ctx, iface, logger, deletion.Kind, deletion.Name, deletion.options())
} else if len(deletion.Labels) > 0 {
items, err := listResources(ctx, iface, deletion)
Expand All @@ -1127,6 +1132,10 @@ func performDeletion(ctx context.Context, logger *log.Entry, client dynamic.Inte
}

for _, item := range items {
err := overrideDeletionProtection(ctx, deletion.Kind, item.GetName(), iface)
if err != nil {
return err
}
err = deleteResource(ctx, iface, logger, deletion.Kind, item.GetName(), deletion.options())
if err != nil {
return err
Expand All @@ -1137,6 +1146,38 @@ func performDeletion(ctx context.Context, logger *log.Entry, client dynamic.Inte
return nil
}

func overrideDeletionProtection(ctx context.Context, kind, name string, iface dynamic.ResourceInterface) error {
annotation := ""
switch kind {
case "Namespace":
annotation = "zalando.org/delete-namespace"
case "Postgresql":
annotation = "zalando.org/delete-clustername"
default:
// no annotation needed
return nil
}

patch := map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"zalando.org/delete-date": time.Now().Format("2006-01-02"),
annotation: name,
},
},
}

payload, err := json.Marshal(patch)
if err != nil {
return err
}
_, err = iface.Patch(ctx, name, k8stypes.JSONPatchType, payload, metav1.PatchOptions{})
if err != nil {
return err
}
return nil
}

// parseDeletions reads and parses the deletions from the config.
func parseDeletions(config channel.Config, cluster *api.Cluster, values map[string]interface{}, adapter *awsAdapter) (*deletions, error) {
result := &deletions{}
Expand Down
119 changes: 86 additions & 33 deletions provisioner/clusterpy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provisioner

import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -486,12 +487,13 @@ func TestPerformDeletion(t *testing.T) {
objects.add("ReplicaSet", "ns-foo", "foo-app-com-env-0002", map[string]string{"app": "foo", "com": "foo", "env": "foo"}, fooAppComEnv)

yes, no := true, false
for _, tc := range []struct {
name string
deletion *resource
clientErrors []clientError
expectError string
expectDeleted []string
i := []struct {
name string
deletion *resource
clientErrors func(state map[string]interface{}) []clientError
clientReactors func(state map[string]interface{}) []k8stesting.SimpleReactor
expectError string
expectDeleted []string
}{
//
// TODO: update client-go version and add metav1.DeleteOptions tests
Expand All @@ -514,6 +516,40 @@ func TestPerformDeletion(t *testing.T) {
Name: "ns-foo",
Kind: "Namespace",
},
clientReactors: func(state map[string]interface{}) []k8stesting.SimpleReactor {
return []k8stesting.SimpleReactor{
{
Verb: "patch",
Resource: "namespaces",
Reaction: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
payload := map[string]interface{}{}
err = json.Unmarshal(action.(k8stesting.PatchAction).GetPatch(), &payload)
if err != nil {
return false, nil, err
}
for k, v := range payload {
state[k] = v
}
return true, nil, nil
},
},
}
},
clientErrors: func(state map[string]interface{}) []clientError {
if meta, ok := state["metadata"]; ok {
if annotations, ok := meta.(map[string]interface{})["annotations"]; ok {
if _, ok := annotations.(map[string]interface{})["zalando.org/delete-date"]; ok {
return nil
}
}
}
return []clientError{
{
matches: deleteAction("namespaces", "ns-foo"),
err: fmt.Errorf("admission webhook \"namespace-admitter.teapot.zalan.do\" denied the request: annotation zalando.org/delete-date not set in manifest to allow resource deletion. See https://cloud.docs.zalando.net/howtos/delete-protection/"),
},
}
},
expectDeleted: []string{
"/namespaces/ns-foo",
},
Expand Down Expand Up @@ -629,11 +665,13 @@ func TestPerformDeletion(t *testing.T) {
Kind: "Deployment",
Labels: map[string]string{"app": "foo"},
},
clientErrors: []clientError{
{
matches: listAction("deployments"),
err: fmt.Errorf("listing deployments failed"),
},
clientErrors: func(map[string]interface{}) []clientError {
return []clientError{
{
matches: listAction("deployments"),
err: fmt.Errorf("listing deployments failed"),
},
}
},
expectError: "listing deployments failed",
},
Expand All @@ -644,11 +682,13 @@ func TestPerformDeletion(t *testing.T) {
Kind: "Deployment",
Labels: map[string]string{"app": "foo"},
},
clientErrors: []clientError{
{
matches: deleteAction("deployments", "foo-app"),
err: fmt.Errorf("foo-app delete failed"),
},
clientErrors: func(map[string]interface{}) []clientError {
return []clientError{
{
matches: deleteAction("deployments", "foo-app"),
err: fmt.Errorf("foo-app delete failed"),
},
}
},
expectError: "unable to delete: foo-app delete failed",
},
Expand All @@ -659,31 +699,42 @@ func TestPerformDeletion(t *testing.T) {
Kind: "Deployment",
Labels: map[string]string{"app": "foo"},
},
clientErrors: []clientError{
{
matches: deleteAction("deployments", "foo-app"),
err: apierrors.NewNotFound(schema.GroupResource{}, "foo-app"),
},
clientErrors: func(map[string]interface{}) []clientError {
return []clientError{
{
matches: deleteAction("deployments", "foo-app"),
err: apierrors.NewNotFound(schema.GroupResource{}, "foo-app"),
},
}
},
expectDeleted: []string{
// skips "/namespaces/ns-foo/deployments/foo-app" due to not found error
"/namespaces/ns-foo/deployments/foo-app-com",
"/namespaces/ns-foo/deployments/foo-app-com-env",
},
},
} {
}
for _, tc := range i {
t.Run(tc.name, func(t *testing.T) {
logger := log.StandardLogger().WithFields(map[string]interface{}{"testcase": tc.name})

state := map[string]interface{}{}
client := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, objects...)
client.PrependReactor("*", "*", func(action k8stesting.Action) (bool, runtime.Object, error) {
for _, ce := range tc.clientErrors {
if ce.matches(action) {
return true, nil, ce.err
if tc.clientErrors != nil {
client.PrependReactor("*", "*", func(action k8stesting.Action) (bool, runtime.Object, error) {
for _, ce := range tc.clientErrors(state) {
if ce.matches(action) {
return true, nil, ce.err
}
}
return false, nil, nil
})
}
if tc.clientReactors != nil {
for _, cr := range tc.clientReactors(state) {
client.PrependReactor(cr.Verb, cr.Resource, cr.Reaction)
}
return false, nil, nil
})
}

gvr := schema.GroupVersionResource{Group: fakeAPIGroup, Version: fakeAPIVersion, Resource: kindToResource[tc.deletion.Kind]}

Expand All @@ -697,14 +748,16 @@ func TestPerformDeletion(t *testing.T) {
var deleted []string
for _, action := range client.Actions() {
ignore := false
for _, ce := range tc.clientErrors {
if ce.matches(action) {
ignore = true
break
if tc.clientErrors != nil {
for _, ce := range tc.clientErrors(state) {
if ce.matches(action) {
ignore = true
break
}
}
}

if action, ok := action.(k8stesting.DeleteAction); ok && !ignore {
if action, ok := action.(k8stesting.DeleteAction); ok && !ignore && action.GetVerb() == "delete" {
fqn := "/"
if action.GetNamespace() != "" {
fqn += "namespaces/" + action.GetNamespace() + "/"
Expand Down

0 comments on commit 7c6b0b9

Please sign in to comment.