diff --git a/integrations/operator/controllers/resources/base_reconciler.go b/integrations/operator/controllers/resources/base_reconciler.go index d0515141f5002..bbf199841d9c2 100644 --- a/integrations/operator/controllers/resources/base_reconciler.go +++ b/integrations/operator/controllers/resources/base_reconciler.go @@ -20,6 +20,7 @@ package resources import ( "context" + "fmt" "github.com/gravitational/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -29,9 +30,18 @@ import ( ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) -// DeletionFinalizer is a name of finalizer added to resource's 'finalizers' field -// for tracking deletion events. -const DeletionFinalizer = "resources.teleport.dev/deletion" +const ( + // DeletionFinalizer is a name of finalizer added to resource's 'finalizers' field + // for tracking deletion events. + DeletionFinalizer = "resources.teleport.dev/deletion" + // AnnotationFlagIgnore is the Kubernetes annotation containing the "ignore" flag. + // When set to true, the operator will not reconcile the CR. + AnnotationFlagIgnore = "teleport.dev/ignore" + // AnnotationFlagKeep is the Kubernetes annotation containing the "keep" flag. + // When set to true, the operator will not delete the Teleport resource if the + // CR is deleted. + AnnotationFlagKeep = "teleport.dev/keep" +) type DeleteExternal func(context.Context, kclient.Object) error type UpsertExternal func(context.Context, kclient.Object) error @@ -79,15 +89,24 @@ func (r ResourceBaseReconciler) Do(ctx context.Context, req ctrl.Request, obj kc return ctrl.Result{}, trace.Wrap(err) } + if isIgnored(obj) { + log.Info(fmt.Sprintf("Resource is flagged with annotation %q, it will not be reconciled.", AnnotationFlagIgnore)) + return ctrl.Result{}, nil + } + hasDeletionFinalizer := controllerutil.ContainsFinalizer(obj, DeletionFinalizer) isMarkedToBeDeleted := !obj.GetDeletionTimestamp().IsZero() // Delete if isMarkedToBeDeleted { if hasDeletionFinalizer { - log.Info("deleting object in Teleport") - if err := r.DeleteExternal(ctx, obj); err != nil && !trace.IsNotFound(err) { - return ctrl.Result{}, trace.Wrap(err) + if isKept(obj) { + log.Info(fmt.Sprintf("Resource is flagged with annotation %q, it will not be deleted in Teleport.", AnnotationFlagKeep)) + } else { + log.Info("deleting object in Teleport") + if err := r.DeleteExternal(ctx, obj); err != nil && !trace.IsNotFound(err) { + return ctrl.Result{}, trace.Wrap(err) + } } log.Info("removing finalizer") @@ -115,3 +134,13 @@ func (r ResourceBaseReconciler) Do(ctx context.Context, req ctrl.Request, obj kc err := r.UpsertExternal(ctx, obj) return ctrl.Result{}, trace.Wrap(err) } + +// isIgnored checks if the CR should be ignored +func isIgnored(obj kclient.Object) bool { + return checkAnnotationFlag(obj, AnnotationFlagIgnore, false /* defaults to false */) +} + +// isKept checks if the Teleport resource should be kept if the CR is deleted +func isKept(obj kclient.Object) bool { + return checkAnnotationFlag(obj, AnnotationFlagKeep, false /* defaults to false */) +} diff --git a/integrations/operator/controllers/resources/utils.go b/integrations/operator/controllers/resources/utils.go index c8327b0c40a87..32d48ece073f9 100644 --- a/integrations/operator/controllers/resources/utils.go +++ b/integrations/operator/controllers/resources/utils.go @@ -21,6 +21,7 @@ package resources import ( "context" "fmt" + "strconv" "github.com/gravitational/trace" "k8s.io/apimachinery/pkg/api/meta" @@ -169,3 +170,18 @@ func GetUnstructuredObjectFromGVK(gvk schema.GroupVersionKind) (*unstructured.Un obj.SetGroupVersionKind(gvk) return &obj, nil } + +// checkAnnotationFlag checks is the Kubernetes resource is annotated with a +// flag and parses its value. Returns the default value if the flag is missing +// or the annotation value cannot be parsed. +func checkAnnotationFlag(object kclient.Object, flagName string, defaultValue bool) bool { + annotation, ok := object.GetAnnotations()[flagName] + if !ok { + return defaultValue + } + value, err := strconv.ParseBool(annotation) + if err != nil { + return defaultValue + } + return value +} diff --git a/integrations/operator/controllers/resources/utils_test.go b/integrations/operator/controllers/resources/utils_test.go index 0c7c1a0b07852..d22ed80697712 100644 --- a/integrations/operator/controllers/resources/utils_test.go +++ b/integrations/operator/controllers/resources/utils_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/gravitational/teleport/api/types" ) @@ -95,3 +96,71 @@ func TestCheckOwnership(t *testing.T) { }) } } + +func TestCheckAnnotationFlag(t *testing.T) { + testFlag := "foo" + tests := []struct { + name string + annotations map[string]string + defaultValue bool + expectedOutput bool + }{ + { + name: "flag set true, default true", + annotations: map[string]string{testFlag: "true"}, + defaultValue: true, + expectedOutput: true, + }, + { + name: "flag set false, default true", + annotations: map[string]string{testFlag: "false"}, + defaultValue: true, + expectedOutput: false, + }, + { + name: "flag set true, default false", + annotations: map[string]string{testFlag: "true"}, + defaultValue: false, + expectedOutput: true, + }, + { + name: "flag set false, default false", + annotations: map[string]string{testFlag: "false"}, + defaultValue: false, + expectedOutput: false, + }, + { + name: "flag missing, default true", + annotations: map[string]string{}, + defaultValue: true, + expectedOutput: true, + }, + { + name: "flag missing, default false", + annotations: map[string]string{}, + defaultValue: false, + expectedOutput: false, + }, + { + name: "flag malformed, default true", + annotations: map[string]string{testFlag: "malformed"}, + defaultValue: true, + expectedOutput: true, + }, + { + name: "flag malformed, default false", + annotations: map[string]string{testFlag: "malformed"}, + defaultValue: false, + expectedOutput: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + obj := &unstructured.Unstructured{} + obj.SetAnnotations(tt.annotations) + require.Equal(t, tt.expectedOutput, checkAnnotationFlag(obj, testFlag, tt.defaultValue)) + }) + } +}