Skip to content

Commit 88c5286

Browse files
authored
revisit deletion handling, update website (#180)
1 parent 9a01b9e commit 88c5286

File tree

11 files changed

+168
-53
lines changed

11 files changed

+168
-53
lines changed

pkg/component/component.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/sap/component-operator-runtime/internal/walk"
15+
"github.com/sap/component-operator-runtime/pkg/reconciler"
1516
)
1617

1718
// Instantiate given Component type T; panics unless T is a pointer type.
@@ -97,6 +98,17 @@ func assertTimeoutConfiguration[T Component](component T) (TimeoutConfiguration,
9798
return nil, false
9899
}
99100

101+
// Check if given component or its spec implements PolicyConfiguration (and return it).
102+
func assertPolicyConfiguration[T Component](component T) (PolicyConfiguration, bool) {
103+
if policyConfiguration, ok := Component(component).(PolicyConfiguration); ok {
104+
return policyConfiguration, true
105+
}
106+
if policyConfiguration, ok := getSpec(component).(PolicyConfiguration); ok {
107+
return policyConfiguration, true
108+
}
109+
return nil, false
110+
}
111+
100112
// Calculate digest of given component, honoring annotations, spec, and references.
101113
func calculateComponentDigest[T Component](component T) string {
102114
digestData := make(map[string]any)
@@ -181,12 +193,6 @@ func (s *RetrySpec) GetRetryInterval() time.Duration {
181193
return time.Duration(0)
182194
}
183195

184-
// Check if state is Ready.
185-
func (s *Status) IsReady() bool {
186-
// caveat: this operates only on the status, so it does not check that observedGeneration == generation
187-
return s.State == StateReady
188-
}
189-
190196
// Implement the TimeoutConfiguration interface.
191197
func (s *TimeoutSpec) GetTimeout() time.Duration {
192198
if s.Timeout != nil {
@@ -195,6 +201,25 @@ func (s *TimeoutSpec) GetTimeout() time.Duration {
195201
return time.Duration(0)
196202
}
197203

204+
// Implement the PolicyConfiguration interface.
205+
func (s *PolicySpec) GetAdoptionPolicy() reconciler.AdoptionPolicy {
206+
return s.AdoptionPolicy
207+
}
208+
209+
func (s *PolicySpec) GetUpdatePolicy() reconciler.UpdatePolicy {
210+
return s.UpdatePolicy
211+
}
212+
213+
func (s *PolicySpec) GetDeletePolicy() reconciler.DeletePolicy {
214+
return s.DeletePolicy
215+
}
216+
217+
// Check if state is Ready.
218+
func (s *Status) IsReady() bool {
219+
// caveat: this operates only on the status, so it does not check that observedGeneration == generation
220+
return s.State == StateReady
221+
}
222+
198223
// Get condition (and return nil if not existing).
199224
// Caveat: the returned pointer might become invalid if further appends happen to the Conditions slice in the status object.
200225
func (s *Status) getCondition(condType ConditionType) *Condition {

pkg/component/reconciler.go

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ import (
5252
// TODO: emitting events to deployment target may fail if corresponding rbac privileges are missing; either this should be pre-discovered or we
5353
// should stop emitting events to remote targets at all; howerver pre-discovering is difficult (may vary from object to object); one option could
5454
// be to send events only if we are cluster-admin
55-
// TODO: allow to override namespace auto-creation and policies on a per-component level
56-
// (e.g. through annotations or another interface that components could optionally implement)
55+
// TODO: allow to override namespace auto-creation and reconcile policy on a per-component level
56+
// that is: consider adding them to the PolicyConfiguration interface?
5757
// TODO: allow to override namespace auto-creation on a per-object level
5858
// TODO: allow some timeout feature, such that component will go into error state if not ready within the given timeout
5959
// (e.g. through a TimeoutConfiguration interface that components could optionally implement)
@@ -101,6 +101,10 @@ type ReconcilerOptions struct {
101101
// If unspecified, UpdatePolicyReplace is assumed.
102102
// Can be overridden by annotation on object level.
103103
UpdatePolicy *reconciler.UpdatePolicy
104+
// How to perform deletion of dependent objects.
105+
// If unspecified, DeletePolicyDelete is assumed.
106+
// Can be overridden by annotation on object level.
107+
DeletePolicy *reconciler.DeletePolicy
104108
// SchemeBuilder allows to define additional schemes to be made available in the
105109
// target client.
106110
SchemeBuilder types.SchemeBuilder
@@ -133,6 +137,8 @@ type Reconciler[T Component] struct {
133137
// resourceGenerator must be an implementation of the manifests.Generator interface.
134138
func NewReconciler[T Component](name string, resourceGenerator manifests.Generator, options ReconcilerOptions) *Reconciler[T] {
135139
// TOOD: validate options
140+
// TODO: currently, the defaulting of CreateMissingNamespaces and *Policy here is identical to the defaulting in the underlying reconciler.Reconciler;
141+
// under the assumption that these attributes are not used here, we could skip the defaulting here, and let it happen in the underlying implementation only
136142
if options.CreateMissingNamespaces == nil {
137143
options.CreateMissingNamespaces = ref(true)
138144
}
@@ -142,6 +148,9 @@ func NewReconciler[T Component](name string, resourceGenerator manifests.Generat
142148
if options.UpdatePolicy == nil {
143149
options.UpdatePolicy = ref(reconciler.UpdatePolicyReplace)
144150
}
151+
if options.DeletePolicy == nil {
152+
options.DeletePolicy = ref(reconciler.DeletePolicyDelete)
153+
}
145154

146155
return &Reconciler[T]{
147156
name: name,
@@ -330,18 +339,8 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
330339
if err != nil {
331340
return ctrl.Result{}, errors.Wrap(err, "error getting client for component")
332341
}
333-
target := newReconcileTarget[T](r.name, r.id, targetClient, r.resourceGenerator, reconciler.ReconcilerOptions{
334-
CreateMissingNamespaces: r.options.CreateMissingNamespaces,
335-
AdoptionPolicy: r.options.AdoptionPolicy,
336-
UpdatePolicy: r.options.UpdatePolicy,
337-
StatusAnalyzer: r.statusAnalyzer,
338-
Metrics: reconciler.ReconcilerMetrics{
339-
ReadCounter: metrics.Operations.WithLabelValues(r.controllerName, "read"),
340-
CreateCounter: metrics.Operations.WithLabelValues(r.controllerName, "create"),
341-
UpdateCounter: metrics.Operations.WithLabelValues(r.controllerName, "update"),
342-
DeleteCounter: metrics.Operations.WithLabelValues(r.controllerName, "delete"),
343-
},
344-
})
342+
targetOptions := r.getOptionsForComponent(component)
343+
target := newReconcileTarget[T](r.name, r.id, targetClient, r.resourceGenerator, targetOptions)
345344
// TODO: enhance ctx with tailored logger and event recorder
346345
// TODO: enhance ctx with the local client
347346
hookCtx = NewContext(ctx).WithReconcilerName(r.name).WithClient(targetClient)
@@ -636,3 +635,32 @@ func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, erro
636635
}
637636
return clnt, nil
638637
}
638+
639+
func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.ReconcilerOptions {
640+
options := reconciler.ReconcilerOptions{
641+
CreateMissingNamespaces: r.options.CreateMissingNamespaces,
642+
AdoptionPolicy: r.options.AdoptionPolicy,
643+
UpdatePolicy: r.options.UpdatePolicy,
644+
DeletePolicy: r.options.DeletePolicy,
645+
StatusAnalyzer: r.statusAnalyzer,
646+
Metrics: reconciler.ReconcilerMetrics{
647+
ReadCounter: metrics.Operations.WithLabelValues(r.controllerName, "read"),
648+
CreateCounter: metrics.Operations.WithLabelValues(r.controllerName, "create"),
649+
UpdateCounter: metrics.Operations.WithLabelValues(r.controllerName, "update"),
650+
DeleteCounter: metrics.Operations.WithLabelValues(r.controllerName, "delete"),
651+
},
652+
}
653+
if policyConfiguration, ok := assertPolicyConfiguration(component); ok {
654+
// TODO: check the values returned by the PolicyConfiguration
655+
if adoptionPolicy := policyConfiguration.GetAdoptionPolicy(); adoptionPolicy != "" {
656+
options.AdoptionPolicy = &adoptionPolicy
657+
}
658+
if updatePolicy := policyConfiguration.GetUpdatePolicy(); updatePolicy != "" {
659+
options.UpdatePolicy = &updatePolicy
660+
}
661+
if deletePolicy := policyConfiguration.GetDeletePolicy(); deletePolicy != "" {
662+
options.DeletePolicy = &deletePolicy
663+
}
664+
}
665+
return options
666+
}

pkg/component/types.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ type TimeoutConfiguration interface {
8484
GetTimeout() time.Duration
8585
}
8686

87+
// The PolicyConfiguration interface is meant to be implemented by compoments (or their spec) which offer
88+
// tweaking policies affecting the dependents handling.
89+
type PolicyConfiguration interface {
90+
// Get adoption policy.
91+
// Must return a valid AdoptionPolicy, or the empty string (then the reconciler/framework default applies).
92+
GetAdoptionPolicy() reconciler.AdoptionPolicy
93+
// Get update policy.
94+
// Must return a valid UpdatePolicy, or the empty string (then the reconciler/framework default applies).
95+
GetUpdatePolicy() reconciler.UpdatePolicy
96+
// Get delete policy.
97+
// Must return a valid DeletePolicy, or the empty string (then the reconciler/framework default applies).
98+
GetDeletePolicy() reconciler.DeletePolicy
99+
}
100+
87101
// +kubebuilder:object:generate=true
88102

89103
// Legacy placement spec. Components may include this into their spec.
@@ -167,6 +181,21 @@ var _ TimeoutConfiguration = &TimeoutSpec{}
167181

168182
// +kubebuilder:object:generate=true
169183

184+
// PolicySpec defines some of the policies tuning the reconciliation of the compooment's dependent objects.
185+
// Components providing PolicyConfiguration may include this into their spec.
186+
type PolicySpec struct {
187+
// +kubebuilder:validation:Enum=Never;IfUnowned;Always
188+
AdoptionPolicy reconciler.AdoptionPolicy `json:"adoptionPolicy,omitempty"`
189+
// +kubebuilder:validation:Enum=Recreate;Replace;SsaMerge;SsaOverride
190+
UpdatePolicy reconciler.UpdatePolicy `json:"updatePolicy,omitempty"`
191+
// +kubebuilder:validation:Enum=Delete;Orphan
192+
DeletePolicy reconciler.DeletePolicy `json:"deletePolicy,omitempty"`
193+
}
194+
195+
var _ PolicyConfiguration = &PolicySpec{}
196+
197+
// +kubebuilder:object:generate=true
198+
170199
// Component Status. Components must include this into their status.
171200
type Status struct {
172201
ObservedGeneration int64 `json:"observedGeneration"`

pkg/component/zz_generated.deepcopy.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/reconciler/reconciler.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ type ReconcilerOptions struct {
9494
// If unspecified, UpdatePolicyReplace is assumed.
9595
// Can be overridden by annotation on object level.
9696
UpdatePolicy *UpdatePolicy
97+
// How to perform deletion of dependent objects.
98+
// If unspecified, DeletePolicyDelete is assumed.
99+
// Can be overridden by annotation on object level.
100+
DeletePolicy *DeletePolicy
97101
// How to analyze the state of the dependent objects.
98102
// If unspecified, an optimized kstatus based implementation is used.
99103
StatusAnalyzer status.StatusAnalyzer
@@ -147,6 +151,9 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions)
147151
if options.UpdatePolicy == nil {
148152
options.UpdatePolicy = ref(UpdatePolicyReplace)
149153
}
154+
if options.DeletePolicy == nil {
155+
options.DeletePolicy = ref(DeletePolicyDelete)
156+
}
150157
if options.StatusAnalyzer == nil {
151158
options.StatusAnalyzer = status.NewStatusAnalyzer(name)
152159
}
@@ -160,7 +167,7 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions)
160167
adoptionPolicy: *options.AdoptionPolicy,
161168
reconcilePolicy: ReconcilePolicyOnObjectChange,
162169
updatePolicy: *options.UpdatePolicy,
163-
deletePolicy: DeletePolicyDelete,
170+
deletePolicy: *options.DeletePolicy,
164171
labelKeyOwnerId: name + "/" + types.LabelKeySuffixOwnerId,
165172
annotationKeyOwnerId: name + "/" + types.AnnotationKeySuffixOwnerId,
166173
annotationKeyDigest: name + "/" + types.AnnotationKeySuffixDigest,
@@ -196,15 +203,14 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions)
196203
// will re-claim (and therefore potentially drop) fields owned by certain field managers, such as kubectl and helm
197204
// - if the effective update policy is UpdatePolicyRecreate, the object will be deleted and recreated.
198205
//
199-
// Redundant objects will be removed; that means, in the regular case, a http DELETE request will be sent to the Kubernetes API; if the object specifies
200-
// its delete policy as DeletePolicyOrphan, no physcial deletion will be performed, and the object will be left around in the cluster; however it will be no
201-
// longer be part of the inventory.
202-
//
203206
// Objects will be applied and deleted in waves, according to their apply/delete order. Objects which specify a purge order will be deleted from the cluster at the
204207
// end of the wave specified as purge order; other than redundant objects, a purged object will remain as Completed in the inventory;
205208
// and it might be re-applied/re-purged in case it runs out of sync. Within a wave, objects are processed following a certain internal order;
206209
// in particular, instances of types which are part of the wave are processed only if all other objects in that wave have a ready state.
207210
//
211+
// Redundant objects will be removed; that means, a http DELETE request will be sent to the Kubernetes API; note that an effective Orphan deletion
212+
// policy will not prevent deletion here; the deletion policy will only be honored when the component as whole gets deleted.
213+
//
208214
// This method will change the passed inventory (add or remove elements, change elements). If Apply() returns true, then all objects are successfully reconciled;
209215
// otherwise, if it returns false, the caller should recall it timely, until it returns true. In any case, the passed inventory should match the state of the
210216
// inventory after the previous invocation of Apply(); usually, the caller saves the inventory after calling Apply(), and loads it before calling Apply().
@@ -727,7 +733,12 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
727733
return false, errors.Wrapf(err, "error reading object %s", item)
728734
}
729735

730-
orphan := item.DeletePolicy == DeletePolicyOrphan
736+
// note: objects becoming obsolete during an apply are no longer honoring deletion policy (orphan)
737+
// TODO: not sure if there is a case where someone would like to orphan such resources while applying;
738+
// if so, then we probably should introduce a third deletion policy, OrphanApply or similar ...
739+
// in any case, the following code should be revisited; cleaned up or adjusted
740+
// orphan := item.DeletePolicy == DeletePolicyOrphan
741+
orphan := false
731742

732743
switch item.Phase {
733744
case PhaseScheduledForDeletion:
@@ -788,6 +799,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
788799
// objects having a certain delete order will only start if all objects with lower delete order are gone. Within a wave, objects are
789800
// deleted following a certain internal ordering; in particular, if there are instances of types which are part of the wave, then these
790801
// instances will be deleted first; only if all such instances are gone, the remaining objects of the wave will be deleted.
802+
// Objects which have an effective Orphan deletion policy will not be touched (remain in the cluster), but will no longer appear in the inventory.
791803
//
792804
// This method will change the passed inventory (remove elements, change elements). If Delete() returns true, then all objects are gone; otherwise,
793805
// if it returns false, the caller should recall it timely, until it returns true. In any case, the passed inventory should match the state of the
@@ -873,8 +885,12 @@ func (r *Reconciler) Delete(ctx context.Context, inventory *[]*InventoryItem) (b
873885

874886
// Check if the object set defined by inventory is ready for deletion; that means: check if the inventory contains
875887
// types (as custom resource definition or from an api service), while there exist instances of these types in the cluster,
876-
// which are not contained in the inventory.
888+
// which are not contained in the inventory. There is one exception of this rule: if all objects in the inventory have their
889+
// delete policy set to DeletePolicyOrphan, then the deletion of the component is immediately allowed.
877890
func (r *Reconciler) IsDeletionAllowed(ctx context.Context, inventory *[]*InventoryItem) (bool, string, error) {
891+
if slices.All(*inventory, func(item *InventoryItem) bool { return item.DeletePolicy == DeletePolicyOrphan }) {
892+
return true, "", nil
893+
}
878894
for _, item := range *inventory {
879895
switch {
880896
case isCrd(item):

website/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ enableMissingTranslationPlaceholders = true
1212
enableRobotsTXT = true
1313

1414
# Will give values to .Lastmod etc.
15-
enableGitInfo = true
15+
enableGitInfo = false
1616

1717
# Comment out to enable taxonomies in Docsy
1818
# disableKinds = ["taxonomy", "taxonomyTerm"]

website/content/en/docs/_index.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,13 @@ weight: 40
55
type: "docs"
66
---
77

8-
This repository provides a framework supporting the development of opinionated Kubernetes operators
8+
This repository provides a framework supporting the development of Kubernetes operators
99
managing the lifecycle of arbitrary deployment components of Kubernetes clusters, with a special focus
1010
on such components that are or contain Kubernetes operators themselves.
1111

1212
It can therefore serve as a starting point to develop [SAP Kyma module operators](https://github.com/kyma-project/template-operator),
13-
but can also be used independently of Kyma.
14-
15-
Regarding its mission statement, this project can be compared with the [Operator Lifecycle Manager (OLM)](https://olm.operatorframework.io/).
16-
However, other than OLM, which follows a generic modelling approach, component-operator-runtime encourages the development of opinionated,
17-
concretely modeled, component-specific operators. This makes the resulting logic much more explicit, and also allows to react better on
18-
specific lifecycle needs of the managed component.
19-
20-
Of course, components might equally be managed by using generic Kustomization or Helm chart deployers (such as provided by [ArgoCD](https://argoproj.github.io/) or [FluxCD](https://fluxcd.io/flux/)).
21-
However, these tools have certain weaknesses when it is about to deploy other operators, i.e. components which extend the Kubernetes API,
22-
e.g. by adding custom resource definitions, aggregated API servers, according controllers, or admission webhooks.
23-
For example these generic solutions tend to produce race conditions or dead locks upon first installation or deletion of the managed components.
24-
This is where component-operator-runtime tries to act in a smarter and more robust way.
13+
but can also be used independently of Kyma. While being perfectly suited to develop opiniated operators like Kyma module operators, it can be
14+
equally used to cover more generic use cases. A prominent example for such a generic operator is the [SAP component operator](https://github.com/sap/component-operator) which can be compared to flux's [kustomize controller](https://github.com/fluxcd/kustomize-controller) and [helm controller](https://github.com/fluxcd/helm-controller).
2515

2616
If you want to report bugs, or request new features or enhancements, please [open an issue](https://github.com/sap/component-operator-runtime/issues)
2717
or [raise a pull request](https://github.com/sap/component-operator-runtime/pulls).

0 commit comments

Comments
 (0)