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/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 e01bf759..7a06e988 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -730,5 +730,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/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 717a72c0..aff7085c 100644 --- a/pkg/component/types.go +++ b/pkg/component/types.go @@ -101,6 +101,17 @@ 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, 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 +} + // +kubebuilder:object:generate=true // Legacy placement spec. Components may include this into their spec. @@ -201,6 +212,16 @@ 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 { + AdditionalManagedTypes []reconciler.TypeInfo `json:"additionalManagedTypes,omitempty"` +} + +var _ TypeConfiguration = &TypeSpec{} + +// +kubebuilder:object:generate=true + // Component Status. Components must include this into their status. type Status struct { ObservedGeneration int64 `json:"observedGeneration"` 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 +} 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 c55b8e2d..7ca6a165 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 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 // 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 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) } @@ -614,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 @@ -625,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 && !isInstanceOfManagedType(*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++ + } } } } @@ -645,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 || !isInstanceOfManagedType(*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 { @@ -752,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) && isInstanceOfManagedType(*inventory, _item) { + if (_item.Phase == PhaseScheduledForDeletion || _item.Phase == PhaseDeleting) && isManaged(_item) { numManagedToBeDeleted++ } } @@ -778,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 || isInstanceOfManagedType(*inventory, item)) { + if !isUsedNamespace(item) && (numManagedToBeDeleted == 0 || isManaged(item)) { if orphan { item.Phase = "" } else { @@ -867,7 +898,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 isManagedInstance(r.additionalManagedTypes, *inventory, _item) { numManagedToBeDeleted++ } } @@ -902,7 +933,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 || isManagedInstance(r.additionalManagedTypes, *inventory, item)) { if orphan { item.Phase = "" } else { @@ -939,12 +970,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): @@ -956,7 +1001,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()) } @@ -972,7 +1017,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()) } @@ -983,6 +1028,7 @@ func (r *Reconciler) IsDeletionAllowed(ctx context.Context, inventory *[]*Invent } } } + return true, "", nil } @@ -1145,7 +1191,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() } @@ -1165,7 +1211,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)) } @@ -1192,7 +1238,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 } @@ -1217,7 +1263,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 } @@ -1333,19 +1379,59 @@ 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) { + resLists, err := r.client.DiscoveryClient().ServerPreferredResources() + if err != nil { + return false, err + } + 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, + }) + } + } + } + } + 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 false, 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 @@ -1353,7 +1439,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 { @@ -1368,7 +1454,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{ @@ -1376,7 +1462,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 72e204b1..db1c9fdc 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,22 +354,47 @@ func isNamespaceUsed(inventory []*InventoryItem, namespace string) bool { return false } -func isInstanceOfManagedType(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 isManagedByTypes(types, key) { + return true + } for _, item := range inventory { - if isManaged := isManagedBy(item, key); isManaged { + if isManagedByTypeVersions(item.ManagedTypes, key) { return true } } return false } -func isManagedBy(item *InventoryItem, key types.TypeKey) bool { +func isManagedByTypeVersions(types []TypeVersionInfo, key types.TypeKey) bool { gvk := key.GetObjectKind().GroupVersionKind() - for _, t := range item.ManagedTypes { - if (t.Group == "*" || t.Group == gvk.Group) && (t.Version == "*" || t.Version == gvk.Version) && (t.Kind == "*" || t.Kind == gvk.Kind) { + for _, t := range types { + if matches(gvk.Group, t.Group) && (t.Version == "*" || t.Version == gvk.Version) && (t.Kind == "*" || t.Kind == gvk.Kind) { return true } } return false } + +func isManagedByTypes(types []TypeInfo, key types.TypeKey) bool { + gvk := key.GetObjectKind().GroupVersionKind() + for _, t := range types { + 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 + } +} 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 { 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