From 074cb04ef333b0a31c49e2352aba89cc1046b8a8 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Wed, 12 Feb 2025 10:16:00 +0100 Subject: [PATCH 1/8] add option to specify additional managed types (1) --- pkg/reconciler/reconciler.go | 21 ++++++++++++++------- pkg/reconciler/util.go | 11 +++++++---- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 27e2ea44..76b0a727 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -111,6 +111,11 @@ type ReconcilerOptions struct { // Whether namespaces are auto-created if missing. // If unspecified, MissingNamespacesPolicyCreate is assumed. MissingNamespacesPolicy *MissingNamespacesPolicy + // Additional managed types. Instances of this type are handled differently during + // apply and delete; foreign instances of this type will block deletion of the component; + // a typical example of such additional managed types are CRDs which are implicitly created + // by the workloads of the component, but not part of the manifests. + AdditionalManagedTypes []TypeInfo // How to analyze the state of the dependent objects. // If unspecified, an optimized kstatus based implementation is used. StatusAnalyzer status.StatusAnalyzer @@ -139,6 +144,7 @@ type Reconciler struct { updatePolicy UpdatePolicy deletePolicy DeletePolicy missingNamespacesPolicy MissingNamespacesPolicy + additionalManagedTypes []TypeInfo labelKeyOwnerId string annotationKeyOwnerId string annotationKeyDigest string @@ -189,6 +195,7 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions) updatePolicy: *options.UpdatePolicy, deletePolicy: *options.DeletePolicy, missingNamespacesPolicy: *options.MissingNamespacesPolicy, + additionalManagedTypes: options.AdditionalManagedTypes, labelKeyOwnerId: name + "/" + types.LabelKeySuffixOwnerId, annotationKeyOwnerId: name + "/" + types.AnnotationKeySuffixOwnerId, annotationKeyDigest: name + "/" + types.AnnotationKeySuffixDigest, @@ -492,7 +499,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj for _, item := range newInventory { if isCrd(item) || isApiService(item) { for _, _item := range newInventory { - if isManagedBy(item, _item) { + if isManagedBy(item.ManagedTypes, _item) { if _item.ApplyOrder < item.ApplyOrder { return false, fmt.Errorf("error valdidating object set (%s): managed instance must not have an apply order lesser than the one of its type", _item) } @@ -629,7 +636,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj for j := k; j < len(objects) && getApplyOrder(objects[j]) == applyOrder; j++ { _object := objects[j] _item := mustGetItem(*inventory, _object) - if _item.Phase != PhaseReady && _item.Phase != PhaseCompleted && !isInstanceOfManagedType(*inventory, _object) { + if _item.Phase != PhaseReady && _item.Phase != PhaseCompleted && !isInstanceOfManagedType(r.additionalManagedTypes, *inventory, _object) { // that means: _item.Phase is one of PhaseScheduledForApplication, PhaseCreating, PhaseUpdating numNotManagedToBeApplied++ } @@ -641,7 +648,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // reconcile all instances of managed types after remaining objects // this ensures that everything is running what is needed for the reconciliation of the managed instances, // such as webhook servers, api servers, ... - if numNotManagedToBeApplied == 0 || !isInstanceOfManagedType(*inventory, object) { + if numNotManagedToBeApplied == 0 || !isInstanceOfManagedType(r.additionalManagedTypes, *inventory, object) { // fetch object (if existing) existingObject, err := r.readObject(ctx, item) if err != nil { @@ -748,7 +755,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj numManagedToBeDeleted = 0 for j := k; j < len(*inventory) && (*inventory)[j].DeleteOrder == item.DeleteOrder; j++ { _item := (*inventory)[j] - if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isInstanceOfManagedType(*inventory, _item) { + if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isInstanceOfManagedType(r.additionalManagedTypes, *inventory, _item) { numManagedToBeDeleted++ } } @@ -774,7 +781,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // delete namespaces after all contained inventory items // delete all instances of managed types before remaining objects; this ensures that no objects are prematurely // deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ... - if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(*inventory, item)) { + if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(r.additionalManagedTypes, *inventory, item)) { if orphan { item.Phase = "" } else { @@ -854,7 +861,7 @@ func (r *Reconciler) Delete(ctx context.Context, inventory *[]*InventoryItem, ow numManagedToBeDeleted = 0 for j := k; j < len(*inventory) && (*inventory)[j].DeleteOrder == item.DeleteOrder; j++ { _item := (*inventory)[j] - if isInstanceOfManagedType(*inventory, _item) { + if isInstanceOfManagedType(r.additionalManagedTypes, *inventory, _item) { numManagedToBeDeleted++ } } @@ -880,7 +887,7 @@ func (r *Reconciler) Delete(ctx context.Context, inventory *[]*InventoryItem, ow // delete namespaces after all contained inventory items // delete all instances of managed types before remaining objects; this ensures that no objects are prematurely // deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ... - if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(*inventory, item)) { + if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(r.additionalManagedTypes, *inventory, item)) { if orphan { item.Phase = "" } else { diff --git a/pkg/reconciler/util.go b/pkg/reconciler/util.go index 72e204b1..b428b548 100644 --- a/pkg/reconciler/util.go +++ b/pkg/reconciler/util.go @@ -354,19 +354,22 @@ func isNamespaceUsed(inventory []*InventoryItem, namespace string) bool { return false } -func isInstanceOfManagedType(inventory []*InventoryItem, key types.TypeKey) bool { +func isInstanceOfManagedType(types []TypeInfo, inventory []*InventoryItem, key types.TypeKey) bool { // TODO: do not consider inventory items with certain Phases (e.g. Completed)? + if isManagedBy(types, key) { + return true + } for _, item := range inventory { - if isManaged := isManagedBy(item, key); isManaged { + if isManagedBy(item.ManagedTypes, key) { return true } } return false } -func isManagedBy(item *InventoryItem, key types.TypeKey) bool { +func isManagedBy(types []TypeInfo, key types.TypeKey) bool { gvk := key.GetObjectKind().GroupVersionKind() - for _, t := range item.ManagedTypes { + for _, t := range types { if (t.Group == "*" || t.Group == gvk.Group) && (t.Version == "*" || t.Version == gvk.Version) && (t.Kind == "*" || t.Kind == gvk.Kind) { return true } From e0dce38bdb21b7e9858c01b7877d3920f5cad1a1 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Wed, 12 Feb 2025 11:36:31 +0100 Subject: [PATCH 2/8] add option to specify additional managed types (2) --- pkg/component/component.go | 16 ++++++++++++++++ pkg/component/reconciler.go | 3 +++ pkg/component/types.go | 18 ++++++++++++++++++ pkg/reconciler/reconciler.go | 4 ++-- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/pkg/component/component.go b/pkg/component/component.go index a1d70f64..4875794d 100644 --- a/pkg/component/component.go +++ b/pkg/component/component.go @@ -109,6 +109,17 @@ func assertPolicyConfiguration[T Component](component T) (PolicyConfiguration, b return nil, false } +// Check if given component or its spec implements TypeConfiguration (and return it). +func assertTypeConfiguration[T Component](component T) (TypeConfiguration, bool) { + if typeConfiguration, ok := Component(component).(TypeConfiguration); ok { + return typeConfiguration, true + } + if typeConfiguration, ok := getSpec(component).(TypeConfiguration); ok { + return typeConfiguration, true + } + return nil, false +} + // Calculate digest of given component, honoring annotations, spec, and references. func calculateComponentDigest[T Component](component T) string { digestData := make(map[string]any) @@ -218,6 +229,11 @@ func (s *PolicySpec) GetMissingNamespacesPolicy() reconciler.MissingNamespacesPo return s.MissingNamespacesPolicy } +// Implement the TypeConfiguration interface. +func (s *TypeSpec) GetAdditionalManagedTypes() []reconciler.TypeInfo { + return s.AdditionalManagedTypes +} + // Check if state is Ready. func (s *Status) IsReady() bool { // caveat: this operates only on the status, so it does not check that observedGeneration == generation diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 97654517..b8d91333 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -729,5 +729,8 @@ func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.Reconcile options.MissingNamespacesPolicy = &missingNamespacesPolicy } } + if typeConfiguration, ok := assertTypeConfiguration(component); ok { + options.AdditionalManagedTypes = typeConfiguration.GetAdditionalManagedTypes() + } return options } diff --git a/pkg/component/types.go b/pkg/component/types.go index 717a72c0..67cc4fd3 100644 --- a/pkg/component/types.go +++ b/pkg/component/types.go @@ -101,6 +101,16 @@ type PolicyConfiguration interface { GetMissingNamespacesPolicy() reconciler.MissingNamespacesPolicy } +// The TypeConfiguration interface is meant to be implemented by compoments (or their spec) which allow +// to specify additional managed types. +type TypeConfiguration interface { + // Get additional managed types; instances of these types are handled differently during + // apply and delete; foreign instances of these types will block deletion of the component. + // The fields of the returned TypeInfo structs can be concrete api groups, versions, kinds, + // or wildcards ("*"); patterns are not possible. + GetAdditionalManagedTypes() []reconciler.TypeInfo +} + // +kubebuilder:object:generate=true // Legacy placement spec. Components may include this into their spec. @@ -199,6 +209,14 @@ type PolicySpec struct { var _ PolicyConfiguration = &PolicySpec{} +// TypeSpec allows to specify additional managed types, which are not explicitly part of the component's manifests. +// Components providing TypeConfiguration may include this into their spec. +type TypeSpec struct { + AdditionalManagedTypes []reconciler.TypeInfo `json:"additionalManagedTypes,omitempty"` +} + +var _ TypeConfiguration = &TypeSpec{} + // +kubebuilder:object:generate=true // Component Status. Components must include this into their status. diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 76b0a727..0d948aa7 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -111,8 +111,8 @@ type ReconcilerOptions struct { // Whether namespaces are auto-created if missing. // If unspecified, MissingNamespacesPolicyCreate is assumed. MissingNamespacesPolicy *MissingNamespacesPolicy - // Additional managed types. Instances of this type are handled differently during - // apply and delete; foreign instances of this type will block deletion of the component; + // Additional managed types. Instances of these types are handled differently during + // apply and delete; foreign instances of these types will block deletion of the component; // a typical example of such additional managed types are CRDs which are implicitly created // by the workloads of the component, but not part of the manifests. AdditionalManagedTypes []TypeInfo From c3002bd3538aea80e1e8277311a071e78388c71b Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Thu, 13 Feb 2025 13:47:40 +0100 Subject: [PATCH 3/8] add generate markers, generate --- pkg/component/types.go | 2 ++ pkg/component/zz_generated.deepcopy.go | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pkg/component/types.go b/pkg/component/types.go index 67cc4fd3..92bfadbf 100644 --- a/pkg/component/types.go +++ b/pkg/component/types.go @@ -209,6 +209,8 @@ type PolicySpec struct { var _ PolicyConfiguration = &PolicySpec{} +// +kubebuilder:object:generate=true + // TypeSpec allows to specify additional managed types, which are not explicitly part of the component's manifests. // Components providing TypeConfiguration may include this into their spec. type TypeSpec struct { diff --git a/pkg/component/zz_generated.deepcopy.go b/pkg/component/zz_generated.deepcopy.go index 307654a0..dce5f81d 100644 --- a/pkg/component/zz_generated.deepcopy.go +++ b/pkg/component/zz_generated.deepcopy.go @@ -507,3 +507,23 @@ func (in *TimeoutSpec) DeepCopy() *TimeoutSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TypeSpec) DeepCopyInto(out *TypeSpec) { + *out = *in + if in.AdditionalManagedTypes != nil { + in, out := &in.AdditionalManagedTypes, &out.AdditionalManagedTypes + *out = make([]reconciler.TypeInfo, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TypeSpec. +func (in *TypeSpec) DeepCopy() *TypeSpec { + if in == nil { + return nil + } + out := new(TypeSpec) + in.DeepCopyInto(out) + return out +} From d3a6fe64d55ccc6fca0617b3357ee354048c9f5c Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Sat, 15 Feb 2025 15:59:05 +0100 Subject: [PATCH 4/8] add option to specify additional managed types (3) --- clm/cmd/delete.go | 2 +- clm/cmd/util.go | 2 +- pkg/component/target.go | 3 +- pkg/component/types.go | 2 +- pkg/reconciler/inventory.go | 4 +- pkg/reconciler/reconciler.go | 71 ++++++++++++++++++------- pkg/reconciler/types.go | 16 ++++-- pkg/reconciler/util.go | 24 ++++++--- pkg/reconciler/zz_generated.deepcopy.go | 4 +- 9 files changed, 89 insertions(+), 39 deletions(-) diff --git a/clm/cmd/delete.go b/clm/cmd/delete.go index 653987a1..9c4dd5fd 100644 --- a/clm/cmd/delete.go +++ b/clm/cmd/delete.go @@ -61,7 +61,7 @@ func newDeleteCmd() *cobra.Command { return err } - if ok, msg, err := reconciler.IsDeletionAllowed(context.TODO(), &release.Inventory); err != nil { + if ok, msg, err := reconciler.IsDeletionAllowed(context.TODO(), &release.Inventory, ownerId); err != nil { return err } else if !ok { return fmt.Errorf(msg) diff --git a/clm/cmd/util.go b/clm/cmd/util.go index 2370e634..635865aa 100644 --- a/clm/cmd/util.go +++ b/clm/cmd/util.go @@ -67,7 +67,7 @@ func isEphmeralError(err error) bool { } func formatTimestamp(t time.Time) string { - d := time.Now().Sub(t) + d := time.Since(t) if d > 24*time.Hour { return fmt.Sprintf("%dd", d/24*time.Hour) } else if d > time.Hour { diff --git a/pkg/component/target.go b/pkg/component/target.go index 001b0d7b..306d6778 100644 --- a/pkg/component/target.go +++ b/pkg/component/target.go @@ -82,7 +82,8 @@ func (t *reconcileTarget[T]) Delete(ctx context.Context, component T) (bool, err func (t *reconcileTarget[T]) IsDeletionAllowed(ctx context.Context, component T) (bool, string, error) { // log := log.FromContext(ctx) + ownerId := t.reconcilerId + "/" + component.GetNamespace() + "/" + component.GetName() status := component.GetStatus() - return t.reconciler.IsDeletionAllowed(ctx, &status.Inventory) + return t.reconciler.IsDeletionAllowed(ctx, &status.Inventory, ownerId) } diff --git a/pkg/component/types.go b/pkg/component/types.go index 92bfadbf..4088da39 100644 --- a/pkg/component/types.go +++ b/pkg/component/types.go @@ -106,7 +106,7 @@ type PolicyConfiguration interface { type TypeConfiguration interface { // Get additional managed types; instances of these types are handled differently during // apply and delete; foreign instances of these types will block deletion of the component. - // The fields of the returned TypeInfo structs can be concrete api groups, versions, kinds, + // The fields of the returned TypeInfo structs can be concrete api groups, kinds, // or wildcards ("*"); patterns are not possible. GetAdditionalManagedTypes() []reconciler.TypeInfo } diff --git a/pkg/reconciler/inventory.go b/pkg/reconciler/inventory.go index 1ec6a1ca..e9ad3726 100644 --- a/pkg/reconciler/inventory.go +++ b/pkg/reconciler/inventory.go @@ -18,12 +18,12 @@ func (i *InventoryItem) GetObjectKind() schema.ObjectKind { // Get inventory item's GroupVersionKind. func (i InventoryItem) GroupVersionKind() schema.GroupVersionKind { - return schema.GroupVersionKind(i.TypeInfo) + return schema.GroupVersionKind(i.TypeVersionInfo) } // Set inventory item's GroupVersionKind. func (i *InventoryItem) SetGroupVersionKind(gvk schema.GroupVersionKind) { - i.TypeInfo = TypeInfo(gvk) + i.TypeVersionInfo = TypeVersionInfo(gvk) } // Get inventory item's namespace. diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 0d948aa7..f2652ae7 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -499,7 +499,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj for _, item := range newInventory { if isCrd(item) || isApiService(item) { for _, _item := range newInventory { - if isManagedBy(item.ManagedTypes, _item) { + if isManagedByTypeVersions(item.ManagedTypes, _item) { if _item.ApplyOrder < item.ApplyOrder { return false, fmt.Errorf("error valdidating object set (%s): managed instance must not have an apply order lesser than the one of its type", _item) } @@ -636,7 +636,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj for j := k; j < len(objects) && getApplyOrder(objects[j]) == applyOrder; j++ { _object := objects[j] _item := mustGetItem(*inventory, _object) - if _item.Phase != PhaseReady && _item.Phase != PhaseCompleted && !isInstanceOfManagedType(r.additionalManagedTypes, *inventory, _object) { + if _item.Phase != PhaseReady && _item.Phase != PhaseCompleted && !isManagedInstance(r.additionalManagedTypes, *inventory, _object) { // that means: _item.Phase is one of PhaseScheduledForApplication, PhaseCreating, PhaseUpdating numNotManagedToBeApplied++ } @@ -648,7 +648,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // reconcile all instances of managed types after remaining objects // this ensures that everything is running what is needed for the reconciliation of the managed instances, // such as webhook servers, api servers, ... - if numNotManagedToBeApplied == 0 || !isInstanceOfManagedType(r.additionalManagedTypes, *inventory, object) { + if numNotManagedToBeApplied == 0 || !isManagedInstance(r.additionalManagedTypes, *inventory, object) { // fetch object (if existing) existingObject, err := r.readObject(ctx, item) if err != nil { @@ -755,7 +755,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj numManagedToBeDeleted = 0 for j := k; j < len(*inventory) && (*inventory)[j].DeleteOrder == item.DeleteOrder; j++ { _item := (*inventory)[j] - if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isInstanceOfManagedType(r.additionalManagedTypes, *inventory, _item) { + if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isManagedInstance(r.additionalManagedTypes, *inventory, _item) { numManagedToBeDeleted++ } } @@ -781,7 +781,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // delete namespaces after all contained inventory items // delete all instances of managed types before remaining objects; this ensures that no objects are prematurely // deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ... - if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(r.additionalManagedTypes, *inventory, item)) { + if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isManagedInstance(r.additionalManagedTypes, *inventory, item)) { if orphan { item.Phase = "" } else { @@ -861,7 +861,7 @@ func (r *Reconciler) Delete(ctx context.Context, inventory *[]*InventoryItem, ow numManagedToBeDeleted = 0 for j := k; j < len(*inventory) && (*inventory)[j].DeleteOrder == item.DeleteOrder; j++ { _item := (*inventory)[j] - if isInstanceOfManagedType(r.additionalManagedTypes, *inventory, _item) { + if isManagedInstance(r.additionalManagedTypes, *inventory, _item) { numManagedToBeDeleted++ } } @@ -887,7 +887,7 @@ func (r *Reconciler) Delete(ctx context.Context, inventory *[]*InventoryItem, ow // delete namespaces after all contained inventory items // delete all instances of managed types before remaining objects; this ensures that no objects are prematurely // deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ... - if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isInstanceOfManagedType(r.additionalManagedTypes, *inventory, item)) { + if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isManagedInstance(r.additionalManagedTypes, *inventory, item)) { if orphan { item.Phase = "" } else { @@ -924,12 +924,26 @@ func (r *Reconciler) Delete(ctx context.Context, inventory *[]*InventoryItem, ow // types (as custom resource definition or from an api service), while there exist instances of these types in the cluster, // which are not contained in the inventory. There is one exception of this rule: if all objects in the inventory have their // deletion policy set to Orphan or OrphanOnDelete, then the deletion of the component is immediately allowed. -func (r *Reconciler) IsDeletionAllowed(ctx context.Context, inventory *[]*InventoryItem) (bool, string, error) { +func (r *Reconciler) IsDeletionAllowed(ctx context.Context, inventory *[]*InventoryItem, ownerId string) (bool, string, error) { + hashedOwnerId := sha256base32([]byte(ownerId)) + + for _, t := range r.additionalManagedTypes { + gk := schema.GroupKind(t) + used, err := r.isTypeUsed(ctx, gk, hashedOwnerId, true) + if err != nil { + return false, "", errors.Wrapf(err, "error checking usage of type %s", gk) + } + if used { + return false, fmt.Sprintf("type %s is still in use (instances exist)", gk), nil + } + } + if slices.All(*inventory, func(item *InventoryItem) bool { return item.DeletePolicy == DeletePolicyOrphan || item.DeletePolicy == DeletePolicyOrphanOnDelete }) { return true, "", nil } + for _, item := range *inventory { switch { case isCrd(item): @@ -941,7 +955,7 @@ func (r *Reconciler) IsDeletionAllowed(ctx context.Context, inventory *[]*Invent return false, "", errors.Wrapf(err, "error retrieving crd %s", item.GetName()) } } - used, err := r.isCrdUsed(ctx, crd, true) + used, err := r.isCrdUsed(ctx, crd, hashedOwnerId, true) if err != nil { return false, "", errors.Wrapf(err, "error checking usage of crd %s", item.GetName()) } @@ -957,7 +971,7 @@ func (r *Reconciler) IsDeletionAllowed(ctx context.Context, inventory *[]*Invent return false, "", errors.Wrapf(err, "error retrieving api service %s", item.GetName()) } } - used, err := r.isApiServiceUsed(ctx, apiService, true) + used, err := r.isApiServiceUsed(ctx, apiService, hashedOwnerId, true) if err != nil { return false, "", errors.Wrapf(err, "error checking usage of api service %s", item.GetName()) } @@ -968,6 +982,7 @@ func (r *Reconciler) IsDeletionAllowed(ctx context.Context, inventory *[]*Invent } } } + return true, "", nil } @@ -1130,7 +1145,7 @@ func (r *Reconciler) updateObject(ctx context.Context, object client.Object, exi // then an error will be returned after issuing the delete call; otherwise, if the crd or api service is not in use, then our // finalizer (i.e. the finalizer equal to the reconciler name) will be cleared, such that the object can be physically // removed (unless other finalizers prevent this) -func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, existingObject *unstructured.Unstructured, ownerId string) (err error) { +func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, existingObject *unstructured.Unstructured, hashedOwnerId string) (err error) { if counter := r.metrics.DeleteCounter; counter != nil { counter.Inc() } @@ -1150,7 +1165,7 @@ func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, exis // TODO: validate (by panic) that existingObject (if present) fits to key - if existingObject != nil && existingObject.GetLabels()[r.labelKeyOwnerId] != ownerId { + if existingObject != nil && existingObject.GetLabels()[r.labelKeyOwnerId] != hashedOwnerId { return fmt.Errorf("owner conflict; object %s has no or different owner", types.ObjectKeyToString(key)) } @@ -1177,7 +1192,7 @@ func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, exis if err := r.client.Get(ctx, apitypes.NamespacedName{Name: key.GetName()}, crd); err != nil { return client.IgnoreNotFound(err) } - used, err := r.isCrdUsed(ctx, crd, false) + used, err := r.isCrdUsed(ctx, crd, hashedOwnerId, false) if err != nil { return err } @@ -1202,7 +1217,7 @@ func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, exis if err := r.client.Get(ctx, apitypes.NamespacedName{Name: key.GetName()}, apiService); err != nil { return client.IgnoreNotFound(err) } - used, err := r.isApiServiceUsed(ctx, apiService, false) + used, err := r.isApiServiceUsed(ctx, apiService, hashedOwnerId, false) if err != nil { return err } @@ -1318,19 +1333,36 @@ func (r *Reconciler) getDeleteOrder(object client.Object) (int, error) { return deleteOrder, nil } -func (r *Reconciler) isCrdUsed(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, onlyForeign bool) (bool, error) { +func (r *Reconciler) isTypeUsed(ctx context.Context, gk schema.GroupKind, hashedOwnerId string, onlyForeign bool) (bool, error) { + restMapping, err := r.client.RESTMapper().RESTMapping(gk) + if err != nil { + return false, err + } + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(restMapping.GroupVersionKind) + labelSelector := labels.Everything() + if onlyForeign { + // note: this must() is ok because the label selector string is static, and correct + labelSelector = must(labels.Parse(r.labelKeyOwnerId + "!=" + hashedOwnerId)) + } + if err := r.client.List(ctx, list, &client.ListOptions{LabelSelector: labelSelector, Limit: 1}); err != nil { + return false, err + } + return len(list.Items) > 0, nil +} + +func (r *Reconciler) isCrdUsed(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, hashedOwnerId string, onlyForeign bool) (bool, error) { gvk := schema.GroupVersionKind{ Group: crd.Spec.Group, Version: crd.Spec.Versions[0].Name, Kind: crd.Spec.Names.Kind, } - // TODO: better use metav1.PartialObjectMetadataList? list := &unstructured.UnstructuredList{} list.SetGroupVersionKind(gvk) labelSelector := labels.Everything() if onlyForeign { // note: this must() is ok because the label selector string is static, and correct - labelSelector = must(labels.Parse(r.labelKeyOwnerId + "!=" + crd.Labels[r.labelKeyOwnerId])) + labelSelector = must(labels.Parse(r.labelKeyOwnerId + "!=" + hashedOwnerId)) } if err := r.client.List(ctx, list, &client.ListOptions{LabelSelector: labelSelector, Limit: 1}); err != nil { return false, err @@ -1338,7 +1370,7 @@ func (r *Reconciler) isCrdUsed(ctx context.Context, crd *apiextensionsv1.CustomR return len(list.Items) > 0, nil } -func (r *Reconciler) isApiServiceUsed(ctx context.Context, apiService *apiregistrationv1.APIService, onlyForeign bool) (bool, error) { +func (r *Reconciler) isApiServiceUsed(ctx context.Context, apiService *apiregistrationv1.APIService, hashedOwnerId string, onlyForeign bool) (bool, error) { gv := schema.GroupVersion{Group: apiService.Spec.Group, Version: apiService.Spec.Version} resList, err := r.client.DiscoveryClient().ServerResourcesForGroupVersion(gv.String()) if err != nil { @@ -1353,7 +1385,7 @@ func (r *Reconciler) isApiServiceUsed(ctx context.Context, apiService *apiregist labelSelector := labels.Everything() if onlyForeign { // note: this must() is ok because the label selector string is static, and correct - labelSelector = must(labels.Parse(r.labelKeyOwnerId + "!=" + apiService.Labels[r.labelKeyOwnerId])) + labelSelector = must(labels.Parse(r.labelKeyOwnerId + "!=" + hashedOwnerId)) } for _, kind := range kinds { gvk := schema.GroupVersionKind{ @@ -1361,7 +1393,6 @@ func (r *Reconciler) isApiServiceUsed(ctx context.Context, apiService *apiregist Version: apiService.Spec.Version, Kind: kind, } - // TODO: better use metav1.PartialObjectMetadataList? list := &unstructured.UnstructuredList{} list.SetGroupVersionKind(gvk) if err := r.client.List(ctx, list, &client.ListOptions{LabelSelector: labelSelector, Limit: 1}); err != nil { diff --git a/pkg/reconciler/types.go b/pkg/reconciler/types.go index 5b02cbf1..d22f47d0 100644 --- a/pkg/reconciler/types.go +++ b/pkg/reconciler/types.go @@ -11,8 +11,8 @@ import ( "github.com/sap/component-operator-runtime/pkg/status" ) -// TypeInfo represents a Kubernetes type. -type TypeInfo struct { +// TypeVersionInfo represents a Kubernetes type version. +type TypeVersionInfo struct { // API group. Group string `json:"group"` // API group version. @@ -21,6 +21,14 @@ type TypeInfo struct { Kind string `json:"kind"` } +// TypeInfo represents a Kubernetes type. +type TypeInfo struct { + // API group. + Group string `json:"group"` + // API kind. + Kind string `json:"kind"` +} + // NameInfo represents an object's namespace and name. type NameInfo struct { // Namespace of the referenced object; empty for non-namespaced objects @@ -99,7 +107,7 @@ const ( // InventoryItem represents a dependent object managed by this operator. type InventoryItem struct { // Type of the dependent object. - TypeInfo `json:",inline"` + TypeVersionInfo `json:",inline"` // Namespace and name of the dependent object. NameInfo `json:",inline"` // Adoption policy. @@ -115,7 +123,7 @@ type InventoryItem struct { // Delete order. DeleteOrder int `json:"deleteOrder"` // Managed types. - ManagedTypes []TypeInfo `json:"managedTypes,omitempty"` + ManagedTypes []TypeVersionInfo `json:"managedTypes,omitempty"` // Digest of the descriptor of the dependent object. Digest string `json:"digest"` // Phase of the dependent object. diff --git a/pkg/reconciler/util.go b/pkg/reconciler/util.go index b428b548..256eb3ad 100644 --- a/pkg/reconciler/util.go +++ b/pkg/reconciler/util.go @@ -159,12 +159,12 @@ func getApiServices(objects []client.Object) []*apiregistrationv1.APIService { return apiServices } -func getManagedTypes(object client.Object) []TypeInfo { +func getManagedTypes(object client.Object) []TypeVersionInfo { switch { case isCrd(object): switch crd := object.(type) { case *apiextensionsv1.CustomResourceDefinition: - return []TypeInfo{{Group: crd.Spec.Group, Version: "*", Kind: crd.Spec.Names.Kind}} + return []TypeVersionInfo{{Group: crd.Spec.Group, Version: "*", Kind: crd.Spec.Names.Kind}} default: // note: this panic relies on v1 being the only version in which apiextensions.k8s.io/CustomResourceDefinition is available in the cluster panic("this cannot happen") @@ -172,7 +172,7 @@ func getManagedTypes(object client.Object) []TypeInfo { case isApiService(object): switch apiService := object.(type) { case *apiregistrationv1.APIService: - return []TypeInfo{{Group: apiService.Spec.Group, Version: apiService.Spec.Version, Kind: "*"}} + return []TypeVersionInfo{{Group: apiService.Spec.Group, Version: apiService.Spec.Version, Kind: "*"}} default: // note: this panic relies on v1 being the only version in which apiregistration.k8s.io/APIService is available in the cluster panic("this cannot happen") @@ -354,20 +354,20 @@ func isNamespaceUsed(inventory []*InventoryItem, namespace string) bool { return false } -func isInstanceOfManagedType(types []TypeInfo, inventory []*InventoryItem, key types.TypeKey) bool { +func isManagedInstance(types []TypeInfo, inventory []*InventoryItem, key types.TypeKey) bool { // TODO: do not consider inventory items with certain Phases (e.g. Completed)? - if isManagedBy(types, key) { + if isManagedByTypes(types, key) { return true } for _, item := range inventory { - if isManagedBy(item.ManagedTypes, key) { + if isManagedByTypeVersions(item.ManagedTypes, key) { return true } } return false } -func isManagedBy(types []TypeInfo, key types.TypeKey) bool { +func isManagedByTypeVersions(types []TypeVersionInfo, key types.TypeKey) bool { gvk := key.GetObjectKind().GroupVersionKind() for _, t := range types { if (t.Group == "*" || t.Group == gvk.Group) && (t.Version == "*" || t.Version == gvk.Version) && (t.Kind == "*" || t.Kind == gvk.Kind) { @@ -376,3 +376,13 @@ func isManagedBy(types []TypeInfo, key types.TypeKey) bool { } return false } + +func isManagedByTypes(types []TypeInfo, key types.TypeKey) bool { + gvk := key.GetObjectKind().GroupVersionKind() + for _, t := range types { + if (t.Group == "*" || t.Group == gvk.Group) && (t.Kind == "*" || t.Kind == gvk.Kind) { + return true + } + } + return false +} diff --git a/pkg/reconciler/zz_generated.deepcopy.go b/pkg/reconciler/zz_generated.deepcopy.go index 6b3517e0..d17b2d89 100644 --- a/pkg/reconciler/zz_generated.deepcopy.go +++ b/pkg/reconciler/zz_generated.deepcopy.go @@ -14,11 +14,11 @@ import () // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InventoryItem) DeepCopyInto(out *InventoryItem) { *out = *in - out.TypeInfo = in.TypeInfo + out.TypeVersionInfo = in.TypeVersionInfo out.NameInfo = in.NameInfo if in.ManagedTypes != nil { in, out := &in.ManagedTypes, &out.ManagedTypes - *out = make([]TypeInfo, len(*in)) + *out = make([]TypeVersionInfo, len(*in)) copy(*out, *in) } if in.LastAppliedAt != nil { From dff35cba5bbdd34c016f57f89ec9da9c9a716ba6 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Sat, 15 Feb 2025 19:34:14 +0100 Subject: [PATCH 5/8] add option to specify additional managed types (4) --- pkg/reconciler/reconciler.go | 43 +++++++++++++++++++++++++++--------- pkg/reconciler/util.go | 16 ++++++++++++-- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index f2652ae7..4777fde8 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -1334,21 +1334,44 @@ func (r *Reconciler) getDeleteOrder(object client.Object) (int, error) { } func (r *Reconciler) isTypeUsed(ctx context.Context, gk schema.GroupKind, hashedOwnerId string, onlyForeign bool) (bool, error) { - restMapping, err := r.client.RESTMapper().RESTMapping(gk) + resLists, err := r.client.DiscoveryClient().ServerPreferredResources() if err != nil { return false, err } - list := &unstructured.UnstructuredList{} - list.SetGroupVersionKind(restMapping.GroupVersionKind) - labelSelector := labels.Everything() - if onlyForeign { - // note: this must() is ok because the label selector string is static, and correct - labelSelector = must(labels.Parse(r.labelKeyOwnerId + "!=" + hashedOwnerId)) + var gvks []schema.GroupVersionKind + for _, resList := range resLists { + gv, err := schema.ParseGroupVersion(resList.GroupVersion) + if err != nil { + return false, err + } + if matches(gv.Group, gk.Group) { + for _, res := range resList.APIResources { + if gk.Kind == "*" || gk.Kind == res.Kind { + gvks = append(gvks, schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: res.Kind, + }) + } + } + } } - if err := r.client.List(ctx, list, &client.ListOptions{LabelSelector: labelSelector, Limit: 1}); err != nil { - return false, err + for _, gvk := range gvks { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + labelSelector := labels.Everything() + if onlyForeign { + // note: this must() is ok because the label selector string is static, and correct + labelSelector = must(labels.Parse(r.labelKeyOwnerId + "!=" + hashedOwnerId)) + } + if err := r.client.List(ctx, list, &client.ListOptions{LabelSelector: labelSelector, Limit: 1}); err != nil { + return false, err + } + if len(list.Items) > 0 { + return true, nil + } } - return len(list.Items) > 0, nil + return false, nil } func (r *Reconciler) isCrdUsed(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, hashedOwnerId string, onlyForeign bool) (bool, error) { diff --git a/pkg/reconciler/util.go b/pkg/reconciler/util.go index 256eb3ad..db1c9fdc 100644 --- a/pkg/reconciler/util.go +++ b/pkg/reconciler/util.go @@ -370,7 +370,7 @@ func isManagedInstance(types []TypeInfo, inventory []*InventoryItem, key types.T func isManagedByTypeVersions(types []TypeVersionInfo, key types.TypeKey) bool { gvk := key.GetObjectKind().GroupVersionKind() for _, t := range types { - if (t.Group == "*" || t.Group == gvk.Group) && (t.Version == "*" || t.Version == gvk.Version) && (t.Kind == "*" || t.Kind == gvk.Kind) { + if matches(gvk.Group, t.Group) && (t.Version == "*" || t.Version == gvk.Version) && (t.Kind == "*" || t.Kind == gvk.Kind) { return true } } @@ -380,9 +380,21 @@ func isManagedByTypeVersions(types []TypeVersionInfo, key types.TypeKey) bool { func isManagedByTypes(types []TypeInfo, key types.TypeKey) bool { gvk := key.GetObjectKind().GroupVersionKind() for _, t := range types { - if (t.Group == "*" || t.Group == gvk.Group) && (t.Kind == "*" || t.Kind == gvk.Kind) { + if matches(gvk.Group, t.Group) && (t.Kind == "*" || t.Kind == gvk.Kind) { return true } } return false } + +func matches(s string, pattern string) bool { + if pattern == "*" { + return true + } else if strings.HasPrefix(pattern, "*.") { + return strings.HasSuffix(s, pattern[1:]) + } else if strings.ContainsRune(pattern, '*') { + return false + } else { + return s == pattern + } +} From 4977a062f3b4047d6f356297a7e1a51162ebe736 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Sat, 15 Feb 2025 23:34:34 +0100 Subject: [PATCH 6/8] add option to specify additional managed types (5) --- pkg/component/types.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/component/types.go b/pkg/component/types.go index 4088da39..aff7085c 100644 --- a/pkg/component/types.go +++ b/pkg/component/types.go @@ -107,7 +107,8 @@ type TypeConfiguration interface { // Get additional managed types; instances of these types are handled differently during // apply and delete; foreign instances of these types will block deletion of the component. // The fields of the returned TypeInfo structs can be concrete api groups, kinds, - // or wildcards ("*"); patterns are not possible. + // or wildcards ("*"); in addition, groups can be specified as a pattern of the form "*."", + // where the wildcard matches one or multiple dns labels. GetAdditionalManagedTypes() []reconciler.TypeInfo } From eee4fe8ab71474b5a1754b0c14416f0900da7637 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Sun, 16 Feb 2025 15:55:03 +0100 Subject: [PATCH 7/8] update docs --- .../content/en/docs/concepts/reconciler.md | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/website/content/en/docs/concepts/reconciler.md b/website/content/en/docs/concepts/reconciler.md index ad667de1..c52871bc 100644 --- a/website/content/en/docs/concepts/reconciler.md +++ b/website/content/en/docs/concepts/reconciler.md @@ -229,4 +229,42 @@ type PolicyConfiguration interface { } ``` -interface. Note that most of the above policies can be overridden on a per-object level by setting certain annotations, as described [here](../dependents). \ No newline at end of file +interface. Note that most of the above policies can be overridden on a per-object level by setting certain annotations, as described [here](../dependents). + +## Declaring additional (implicit) managed types + +One of component-operator-runtime's core features is the special handling of instances of managed types. +Managed types are API extension types (such as custom resource definitions or types added by federation of an aggregated API server). Instances of these types which are part of the component (so-called managed instances) are treated differently. For example, the framework tries to process these instances as late as possible when applying the component, and as early as possible when the component is deleted. Other instances of these types which are not part of the component (so-called foreign instances) block the deletion of the whole component. + +Sometimes, components are implicitly adding extension types. That means that the type definition is not part of the component manifest, but the types are just created at runtime by controllers or operators contained in the component. A typical example are crossplane providers. These types are of course not recognized by the framework as managed types. However it is probably desired that (both managed and foreign) instances of these types experience the same special handling like instances of real managed types. + +To make this possible, components can implement the + +```go +// The TypeConfiguration interface is meant to be implemented by compoments (or their spec) which allow +// to specify additional managed types. +type TypeConfiguration interface { + // Get additional managed types; instances of these types are handled differently during + // apply and delete; foreign instances of these types will block deletion of the component. + // The fields of the returned TypeInfo structs can be concrete api groups, kinds, + // or wildcards ("*"); in addition, groups can be specified as a pattern of the form "*."", + // where the wildcard matches one or multiple dns labels. + GetAdditionalManagedTypes() []reconciler.TypeInfo +} +``` + +interface. The types returned by `GetAdditionalManagedTypes()` contain a group and a kind, such as + +```go +// TypeInfo represents a Kubernetes type. +type TypeInfo struct { + // API group. + Group string `json:"group"` + // API kind. + Kind string `json:"kind"` +} +``` + +To match multiple types, the following pattern syntax is supported: +- the `Group` can be just `*` or have the form `*.domain.suffix`; note that the second case, the asterisk matches one or multiple DNS labels +- the `Kind` can be just `*`, which matches any kind. \ No newline at end of file From e57bd2e836e9a38e959b83d06c944f40250b62d4 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Wed, 19 Feb 2025 10:32:06 +0100 Subject: [PATCH 8/8] improve apply handing for APIService objects --- pkg/reconciler/reconciler.go | 42 ++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 508a53b5..7ca6a165 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -621,9 +621,20 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // that means, only if all objects of a wave are ready or completed, the next wave // will be procesed; within each wave, objects which are instances of managed types // will be applied after all other objects - // TODO: it would be good to have a special handling of APIService objects; probably, APIService objects - // should be applied after all regular (aka unmanaged until now) and before all managed objects - numNotManagedToBeApplied := 0 + isManaged := func(key types.ObjectKey) bool { + return isManagedInstance(r.additionalManagedTypes, *inventory, key) + } + isLate := func(key types.ObjectKey) bool { + return isApiService(key) + } + isRegular := func(key types.ObjectKey) bool { + return !isLate(key) && !isManaged(key) + } + isUsedNamespace := func(key types.ObjectKey) bool { + return isNamespace(key) && isNamespaceUsed(*inventory, key.GetName()) + } + numRegularToBeApplied := 0 + numLateToBeApplied := 0 numUnready := 0 for k, object := range objects { // retrieve inventory item corresponding to this object @@ -632,17 +643,29 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // retrieve object order applyOrder := getApplyOrder(object) + // within each apply order, objects are deployed to readiness in three sub stages + // - regular objects (all 'normal' objects) + // - late objects (currently, this is only APIService objects) + // - instances of managed types (that is instances of types which are added in this component as CRD or through an APIService) + // within each of these sub groups, the static ordering defined in sortObjectsForApply() is effective + // if this is the first object of an order, then // count instances of managed types in this order which are about to be applied if k == 0 || getApplyOrder(objects[k-1]) < applyOrder { log.V(2).Info("begin of apply wave", "order", applyOrder) - numNotManagedToBeApplied = 0 + numRegularToBeApplied = 0 + numLateToBeApplied = 0 for j := k; j < len(objects) && getApplyOrder(objects[j]) == applyOrder; j++ { _object := objects[j] _item := mustGetItem(*inventory, _object) - if _item.Phase != PhaseReady && _item.Phase != PhaseCompleted && !isManagedInstance(r.additionalManagedTypes, *inventory, _object) { + if _item.Phase != PhaseReady && _item.Phase != PhaseCompleted { // that means: _item.Phase is one of PhaseScheduledForApplication, PhaseCreating, PhaseUpdating - numNotManagedToBeApplied++ + if isRegular(_object) { + numRegularToBeApplied++ + } // (same as) else (because isRegular() and isLate() are mutually exclusive) + if isLate(_object) { + numLateToBeApplied++ + } } } } @@ -652,7 +675,8 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // reconcile all instances of managed types after remaining objects // this ensures that everything is running what is needed for the reconciliation of the managed instances, // such as webhook servers, api servers, ... - if numNotManagedToBeApplied == 0 || !isManagedInstance(r.additionalManagedTypes, *inventory, object) { + // note: here, phase is one of PhaseScheduledForApplication, PhaseCreating, PhaseUpdating, PhaseReady + if isRegular(object) || isLate(object) && numRegularToBeApplied == 0 || isManaged(object) && numRegularToBeApplied == 0 && numLateToBeApplied == 0 { // fetch object (if existing) existingObject, err := r.readObject(ctx, item) if err != nil { @@ -759,7 +783,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj numManagedToBeDeleted = 0 for j := k; j < len(*inventory) && (*inventory)[j].DeleteOrder == item.DeleteOrder; j++ { _item := (*inventory)[j] - if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isManagedInstance(r.additionalManagedTypes, *inventory, _item) { + if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isManaged(_item) { numManagedToBeDeleted++ } } @@ -785,7 +809,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // delete namespaces after all contained inventory items // delete all instances of managed types before remaining objects; this ensures that no objects are prematurely // deleted which are needed for the deletion of the managed instances, such as webhook servers, api servers, ... - if (!isNamespace(item) || !isNamespaceUsed(*inventory, item.Name)) && (numManagedToBeDeleted == 0 || isManagedInstance(r.additionalManagedTypes, *inventory, item)) { + if !isUsedNamespace(item) && (numManagedToBeDeleted == 0 || isManaged(item)) { if orphan { item.Phase = "" } else {