Skip to content

Commit

Permalink
Merge pull request #16 from vshn/add/namespace_deletion_override
Browse files Browse the repository at this point in the history
Override deletion protection on instance namespace deletion
  • Loading branch information
Kidswiss authored May 16, 2023
2 parents 54a9ae8 + 2be1f07 commit 198b44d
Show file tree
Hide file tree
Showing 5 changed files with 427 additions and 93 deletions.
11 changes: 10 additions & 1 deletion config/controller/cluster-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,13 @@ rules:
resources:
- objects
verbs:
- delete
- delete
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- update
- list
- watch
76 changes: 71 additions & 5 deletions controller/postgres/deletion_protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package postgres
import (
"context"
"encoding/json"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
logging "sigs.k8s.io/controller-runtime/pkg/log"
"fmt"
"strconv"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
logging "sigs.k8s.io/controller-runtime/pkg/log"

"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
Expand Down Expand Up @@ -39,10 +43,10 @@ func handle(ctx context.Context, inst client.Object, enabled bool, retention int
op := opNone

if !enabled {
log.Info("DeletionProtection is not enabled, ensuring no finalizer set", "objectName", inst.GetName())
removed := controllerutil.RemoveFinalizer(inst, finalizerName)

if removed {
log.Info("DeletionProtection is not enabled, ensuring no finalizer set", "objectName", inst.GetName())
op = opRemove
}

Expand Down Expand Up @@ -81,7 +85,7 @@ func checkRetention(ctx context.Context, inst client.Object, retention int) json
return op
}

func getRequeueTime(ctx context.Context, inst client.Object, deletionTime *v1.Time, retention int) time.Duration {
func getRequeueTime(ctx context.Context, inst client.Object, deletionTime *metav1.Time, retention int) time.Duration {
log := logging.FromContext(ctx, "namespace", inst.GetNamespace(), "instance", inst.GetName())
now := getCurrentTime()
if deletionTime != nil {
Expand All @@ -108,10 +112,15 @@ func getPatchObjectFinalizer(log logr.Logger, inst client.Object, op jsonOp) (cl

log.V(1).Info("Index size", "size", index, "found finalizers", inst.GetFinalizers())

strIndex := strconv.Itoa(index)
if op == opAdd {
strIndex = "-"
}

patchOps := []jsonpatch{
{
Op: op,
Path: "/metadata/finalizers/" + strconv.Itoa(index),
Path: "/metadata/finalizers/" + strIndex,
Value: finalizerName,
},
}
Expand All @@ -130,3 +139,60 @@ func getCurrentTime() time.Time {
t := time.Now()
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
}

func getPostgreSQLNamespace(inst client.Object) string {
return fmt.Sprintf("vshn-postgresql-%s", inst.GetName())
}

// instanceNamespaceDeleted handles the case, if the instance namespace gets deleted.
// The customer can't delete the namespace by themselves, so this is usally when the customer as a whole gets deleted.
// Or some other administrative action.
// In those cases we should disable the deletionprotection.
// If the namespace is deleted or not found it will return a patch to remove the finalizer.
func instanceNamespaceDeleted(ctx context.Context, log logr.Logger, inst client.Object, enabled bool, c client.Client) (jsonOp, error) {
ns := &corev1.Namespace{}
err := c.Get(ctx, client.ObjectKey{Name: getPostgreSQLNamespace(inst)}, ns)
if err != nil {
if apierrors.IsNotFound(err) {
log.V(1).Info("Instance namespace was not found, ignoring")
return opNone, nil
}
return opNone, err
}

if ns.DeletionTimestamp != nil && controllerutil.RemoveFinalizer(ns, finalizerName) {
log.Info("Instance namespace was deleted, overriding deletionprotection")
return opRemove, c.Update(ctx, ns)
}

if enabled && controllerutil.AddFinalizer(ns, finalizerName) {
log.Info("Deletion protection enabled, protecting instance namespace")
err := controllerutil.SetControllerReference(inst, ns, c.Scheme())
if err != nil {
return opNone, err
}
return opNone, c.Update(ctx, ns)
}

if !enabled && controllerutil.RemoveFinalizer(ns, finalizerName) {
log.Info("Deletion protection disabled, removing protection from instance namespace")
return opRemove, c.Update(ctx, ns)
}

return opNone, nil
}

func getInstanceNamespaceOverride(ctx context.Context, inst client.Object, enabled bool, c client.Client) (client.Patch, error) {
log := logging.FromContext(ctx, "namespace", inst.GetNamespace(), "instance", inst.GetName())

overrideOp, err := instanceNamespaceDeleted(ctx, log, inst, enabled, c)
if err != nil {
return nil, errors.Wrap(err, "could not determine instance namespace status")
}

patch, err := getPatchObjectFinalizer(log, inst, overrideOp)
if err != nil {
return nil, errors.Wrap(err, "can't create namespace override patch")
}
return patch, nil
}
180 changes: 164 additions & 16 deletions controller/postgres/deletion_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,36 @@ package postgres
import (
"context"
"encoding/json"
"strconv"
"testing"
"time"

xkube "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
v1 "github.com/vshn/component-appcat/apis/vshn/v1"
apis "k8s.io/apimachinery/pkg/apis/meta/v1"
vshnv1 "github.com/vshn/component-appcat/apis/vshn/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"strconv"
"testing"
"time"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

var currentTimeKey = "now"
var (
currentTimeKey = "now"
)

func init() {
_ = vshnv1.AddToScheme(s)
_ = corev1.AddToScheme(s)
_ = xkube.SchemeBuilder.AddToScheme(s)
}

func Test_Handle(t *testing.T) {
previousDay := getCurrentTime().AddDate(0, 0, -1)
tests := map[string]struct {
ctx context.Context
obj v1.XVSHNPostgreSQL
obj vshnv1.XVSHNPostgreSQL
enabled bool
retention int
expectedPatch client.Patch
Expand Down Expand Up @@ -87,7 +99,7 @@ func Test_CheckRetention(t *testing.T) {
nextDay := getCurrentTime().AddDate(0, 0, 1)
tests := map[string]struct {
ctx context.Context
obj v1.XVSHNPostgreSQL
obj vshnv1.XVSHNPostgreSQL
retention int
expectedOp jsonOp
}{
Expand Down Expand Up @@ -138,7 +150,7 @@ func Test_GetRequeueTime(t *testing.T) {
previousDay := getCurrentTime().AddDate(0, 0, -1)
tests := map[string]struct {
ctx context.Context
obj v1.XVSHNPostgreSQL
obj vshnv1.XVSHNPostgreSQL
deletionTime *time.Time
retention int
expectedDuration time.Duration
Expand Down Expand Up @@ -175,7 +187,7 @@ func Test_GetRequeueTime(t *testing.T) {

func Test_GetPatchObjectFinalizer(t *testing.T) {
tests := map[string]struct {
obj v1.XVSHNPostgreSQL
obj vshnv1.XVSHNPostgreSQL
op jsonOp
expectedPatch client.Patch
}{
Expand Down Expand Up @@ -217,33 +229,169 @@ func Test_GetPatchObjectFinalizer(t *testing.T) {
}
}

func transformToK8sTime(t *time.Time) *apis.Time {
func transformToK8sTime(t *time.Time) *metav1.Time {
if t != nil {
temp := apis.NewTime(*t)
temp := metav1.NewTime(*t)
return &temp
}
return nil
}

func getXVSHNPostgreSQL(addFinalizer bool, deletedTime *time.Time) v1.XVSHNPostgreSQL {
obj := v1.XVSHNPostgreSQL{}
func getXVSHNPostgreSQL(addFinalizer bool, deletedTime *time.Time) vshnv1.XVSHNPostgreSQL {
obj := vshnv1.XVSHNPostgreSQL{}
if addFinalizer {
obj.Finalizers = []string{finalizerName}
}
if deletedTime != nil {
obj.SetDeletionTimestamp(&apis.Time{Time: *deletedTime})
obj.SetDeletionTimestamp(&metav1.Time{Time: *deletedTime})
}
return obj
}

func getPatch(op jsonOp) client.Patch {
strIndex := strconv.Itoa(0)
if op == opAdd {
strIndex = "-"
}
patchOps := []jsonpatch{
{
Op: op,
Path: "/metadata/finalizers/" + strconv.Itoa(0),
Path: "/metadata/finalizers/" + strIndex,
Value: finalizerName,
},
}
patch, _ := json.Marshal(patchOps)
return client.RawPatch(types.JSONPatchType, patch)
}

func Test_instanceNamespaceDeleted(t *testing.T) {
tests := []struct {
name string
want jsonOp
wantDeleted bool
wantFinalizer bool
wantErr bool
instance vshnv1.XVSHNPostgreSQL
namespace corev1.Namespace
enabled bool
}{
{
name: "GivenEnabledAndNotDeleted_ThenExpectNoOpAndFinalizer",
want: opNone,
wantFinalizer: true,
enabled: true,
instance: vshnv1.XVSHNPostgreSQL{
ObjectMeta: metav1.ObjectMeta{
Name: "instance-1",
},
},
namespace: corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vshn-postgresql-instance-1",
},
},
},
{
name: "GivenEnabledAndDeleted_ThenExpectRemoveOpAndNoObject",
want: opRemove,
wantFinalizer: false,
wantDeleted: true,
enabled: true,
instance: vshnv1.XVSHNPostgreSQL{
ObjectMeta: metav1.ObjectMeta{
Name: "instance-1",
},
},
namespace: corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vshn-postgresql-instance-1",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{finalizerName},
},
},
},
{
name: "GivenNotEnabledAndNotDeleted_ThenExpectNoOpAndNoFinalizer",
want: opNone,
wantFinalizer: false,
enabled: false,
instance: vshnv1.XVSHNPostgreSQL{
ObjectMeta: metav1.ObjectMeta{
Name: "instance-1",
},
},
namespace: corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vshn-postgresql-instance-1",
},
},
},
{
name: "GivenNotEnabledAndNotFound_ThenExpectNoOpAndNoObject",
want: opNone,
wantFinalizer: false,
wantDeleted: true,
enabled: false,
instance: vshnv1.XVSHNPostgreSQL{
ObjectMeta: metav1.ObjectMeta{
Name: "instance-1",
},
},
namespace: corev1.Namespace{},
},
{
name: "GivenNotEnabledAndFinalizer_ThenExpectRemoveOpAndNoFinalizer",
want: opRemove,
wantFinalizer: false,
enabled: false,
instance: vshnv1.XVSHNPostgreSQL{
ObjectMeta: metav1.ObjectMeta{
Name: "instance-1",
},
},
namespace: corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vshn-postgresql-instance-1",
Finalizers: []string{finalizerName},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

// Given
fclient := fake.NewClientBuilder().
WithScheme(s).
WithRuntimeObjects(&tt.instance, &tt.namespace).
Build()
logger := logr.Discard()

// When
got, err := instanceNamespaceDeleted(context.TODO(), logger, &tt.instance, tt.enabled, fclient)
if (err != nil) != tt.wantErr {
t.Errorf("instanceNamespaceDeleted() error = %v, wantErr %v", err, tt.wantErr)
}

// Then
if got != tt.want {
t.Errorf("instanceNamespaceDeleted() = %v, want %v", got, tt.want)
}

resultNs := &corev1.Namespace{}
err = fclient.Get(context.TODO(), client.ObjectKey{Name: "vshn-postgresql-" + tt.instance.Name}, resultNs)

if tt.wantDeleted {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}

if tt.wantFinalizer {
assert.Contains(t, resultNs.GetFinalizers(), finalizerName)
} else {
assert.NotContains(t, resultNs.GetFinalizers(), finalizerName)
}
})
}
}
Loading

0 comments on commit 198b44d

Please sign in to comment.