From 9499d4a49c065af19758859b3f9ef47dd54d2f37 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Mon, 20 Jan 2025 20:41:49 +0100 Subject: [PATCH 01/13] refactoring --- pkg/component/reconciler.go | 28 +++++++++++++++++------ pkg/reconciler/reconciler.go | 44 ++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 4dd49cb..4b13c61 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -90,6 +90,12 @@ type HookFunc[T Component] func(ctx context.Context, clnt client.Client, compone // ReconcilerOptions are creation options for a Reconciler. type ReconcilerOptions struct { + // Which field manager to use in API calls. + // If unspecified, the reconciler name is used. + FieldOwner *string + // Which finalizer to use. + // If unspecified, the reconciler name is used. + Finalizer *string // Whether namespaces are auto-created if missing. // If unspecified, true is assumed. CreateMissingNamespaces *bool @@ -137,8 +143,14 @@ type Reconciler[T Component] struct { // resourceGenerator must be an implementation of the manifests.Generator interface. func NewReconciler[T Component](name string, resourceGenerator manifests.Generator, options ReconcilerOptions) *Reconciler[T] { // TOOD: validate options - // TODO: currently, the defaulting of CreateMissingNamespaces and *Policy here is identical to the defaulting in the underlying reconciler.Reconciler; + // TODO: currently, the defaulting here is identical to the defaulting in the underlying reconciler.Reconciler; // 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 + if options.FieldOwner == nil { + options.FieldOwner = &name + } + if options.Finalizer == nil { + options.Finalizer = &name + } if options.CreateMissingNamespaces == nil { options.CreateMissingNamespaces = ref(true) } @@ -311,7 +323,7 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result cond.LastTransitionTime = &now } } - if updateErr := r.client.Status().Update(ctx, component, client.FieldOwner(r.name)); updateErr != nil { + if updateErr := r.client.Status().Update(ctx, component, client.FieldOwner(*r.options.FieldOwner)); updateErr != nil { err = utilerrors.NewAggregate([]error{err, updateErr}) result = ctrl.Result{} } @@ -349,8 +361,8 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result if component.GetDeletionTimestamp().IsZero() { // create/update case // TODO: optionally (to be completely consistent) set finalizer through a mutating webhook - if added := controllerutil.AddFinalizer(component, r.name); added { - if err := r.client.Update(ctx, component, client.FieldOwner(r.name)); err != nil { + if added := controllerutil.AddFinalizer(component, *r.options.Finalizer); added { + if err := r.client.Update(ctx, component, client.FieldOwner(*r.options.FieldOwner)); err != nil { return ctrl.Result{}, errors.Wrap(err, "error adding finalizer") } // trigger another round trip @@ -415,7 +427,7 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result status.SetState(StateDeleting, readyConditionReasonDeletionBlocked, "Deletion blocked: "+msg) return ctrl.Result{RequeueAfter: 1*time.Second + r.backoff.Next(req, readyConditionReasonDeletionBlocked)}, nil } - if len(slices.Remove(component.GetFinalizers(), r.name)) > 0 { + if len(slices.Remove(component.GetFinalizers(), *r.options.Finalizer)) > 0 { // deletion is blocked because of foreign finalizers log.V(1).Info("deleted blocked due to existence of foreign finalizers") // TODO: have an additional StateDeletionBlocked? @@ -438,8 +450,8 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result } // all dependent resources are already gone, so that's it log.V(1).Info("all dependent resources are successfully deleted; removing finalizer") - if removed := controllerutil.RemoveFinalizer(component, r.name); removed { - if err := r.client.Update(ctx, component, client.FieldOwner(r.name)); err != nil { + if removed := controllerutil.RemoveFinalizer(component, *r.options.Finalizer); removed { + if err := r.client.Update(ctx, component, client.FieldOwner(*r.options.FieldOwner)); err != nil { return ctrl.Result{}, errors.Wrap(err, "error removing finalizer") } } @@ -638,6 +650,8 @@ func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, erro func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.ReconcilerOptions { options := reconciler.ReconcilerOptions{ + FieldOwner: r.options.FieldOwner, + Finalizer: r.options.Finalizer, CreateMissingNamespaces: r.options.CreateMissingNamespaces, AdoptionPolicy: r.options.AdoptionPolicy, UpdatePolicy: r.options.UpdatePolicy, diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index ac21871..da573b9 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -84,6 +84,12 @@ var deletePolicyByAnnotation = map[string]DeletePolicy{ // ReconcilerOptions are creation options for a Reconciler. type ReconcilerOptions struct { + // Which field manager to use in API calls. + // If unspecified, the reconciler name is used. + FieldOwner *string + // Which finalizer to use. + // If unspecified, the reconciler name is used. + Finalizer *string // Whether namespaces are auto-created if missing. // If unspecified, true is assumed. CreateMissingNamespaces *bool @@ -117,7 +123,8 @@ type ReconcilerMetrics struct { // Reconciler manages specified objects in the given target cluster. type Reconciler struct { - name string + fieldOwner string + finalizer string client cluster.Client statusAnalyzer status.StatusAnalyzer metrics ReconcilerMetrics @@ -143,6 +150,12 @@ type Reconciler struct { // The passed client's scheme must recognize at least the core group (v1) and apiextensions.k8s.io/v1 and apiregistration.k8s.io/v1. func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions) *Reconciler { // TOOD: validate options + if options.FieldOwner == nil { + options.FieldOwner = &name + } + if options.Finalizer == nil { + options.Finalizer = &name + } if options.CreateMissingNamespaces == nil { options.CreateMissingNamespaces = ref(true) } @@ -160,7 +173,8 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions) } return &Reconciler{ - name: name, + fieldOwner: *options.FieldOwner, + finalizer: *options.Finalizer, client: clnt, statusAnalyzer: options.StatusAnalyzer, metrics: options.Metrics, @@ -538,7 +552,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj if !apierrors.IsNotFound(err) { return false, errors.Wrapf(err, "error reading namespace %s", namespace) } - if err := r.client.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, client.FieldOwner(r.name)); err != nil { + if err := r.client.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, client.FieldOwner(r.fieldOwner)); err != nil { return false, errors.Wrapf(err, "error creating namespace %s", namespace) } } @@ -981,7 +995,7 @@ func (r *Reconciler) createObject(ctx context.Context, object client.Object, cre } object = &unstructured.Unstructured{Object: data} if isCrd(object) || isApiService(object) { - controllerutil.AddFinalizer(object, r.name) + controllerutil.AddFinalizer(object, r.finalizer) } // note: clearing managedFields is anyway required for ssa; but also in the create (post) case it does not harm object.SetManagedFields(nil) @@ -991,9 +1005,9 @@ func (r *Reconciler) createObject(ctx context.Context, object client.Object, cre case UpdatePolicySsaMerge, UpdatePolicySsaOverride: // set the target resource version to an impossible value; this will produce a 409 conflict in case the object already exists object.SetResourceVersion("1") - return r.client.Patch(ctx, object, client.Apply, client.FieldOwner(r.name)) + return r.client.Patch(ctx, object, client.Apply, client.FieldOwner(r.fieldOwner)) default: - return r.client.Create(ctx, object, client.FieldOwner(r.name)) + return r.client.Create(ctx, object, client.FieldOwner(r.fieldOwner)) } } @@ -1036,7 +1050,7 @@ func (r *Reconciler) updateObject(ctx context.Context, object client.Object, exi } object = &unstructured.Unstructured{Object: data} if isCrd(object) || isApiService(object) { - controllerutil.AddFinalizer(object, r.name) + controllerutil.AddFinalizer(object, r.finalizer) } // it is allowed that target object contains a resource version; otherwise, we set the resource version to the one of the existing object, // in order to ensure that we do not unintentionally overwrite a state different from the one we have read; @@ -1057,7 +1071,7 @@ func (r *Reconciler) updateObject(ctx context.Context, object client.Object, exi } // note: even if replacedFieldManagerPrefixes is empty, replaceFieldManager() will reclaim fields created by us through an Update operation, // that is through a create or update call; this may be necessary, if the update policy for the object changed (globally or per-object) - if managedFields, changed, err := replaceFieldManager(existingObject.GetManagedFields(), replacedFieldManagerPrefixes, r.name); err != nil { + if managedFields, changed, err := replaceFieldManager(existingObject.GetManagedFields(), replacedFieldManagerPrefixes, r.fieldOwner); err != nil { return err } else if changed { log.V(1).Info("adjusting field managers as preparation of ssa") @@ -1078,17 +1092,17 @@ func (r *Reconciler) updateObject(ctx context.Context, object client.Object, exi {"op": "replace", "path": "/metadata/resourceVersion", "value": object.GetResourceVersion()}, } // note: this must() is ok because marshalling the patch should always work - if err := r.client.Patch(ctx, obj, client.RawPatch(apitypes.JSONPatchType, must(json.Marshal(preparePatch))), client.FieldOwner(r.name)); err != nil { + if err := r.client.Patch(ctx, obj, client.RawPatch(apitypes.JSONPatchType, must(json.Marshal(preparePatch))), client.FieldOwner(r.fieldOwner)); err != nil { return err } object.SetResourceVersion(obj.GetResourceVersion()) } - return r.client.Patch(ctx, object, client.Apply, client.FieldOwner(r.name), client.ForceOwnership) + return r.client.Patch(ctx, object, client.Apply, client.FieldOwner(r.fieldOwner), client.ForceOwnership) default: for _, finalizer := range existingObject.GetFinalizers() { controllerutil.AddFinalizer(object, finalizer) } - return r.client.Update(ctx, object, client.FieldOwner(r.name)) + return r.client.Update(ctx, object, client.FieldOwner(r.fieldOwner)) } } @@ -1147,9 +1161,9 @@ func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, exis if used { return fmt.Errorf("error deleting custom resource definition %s, existing instances found", types.ObjectKeyToString(key)) } - if ok := controllerutil.RemoveFinalizer(crd, r.name); ok { + if ok := controllerutil.RemoveFinalizer(crd, r.finalizer); ok { // note: 409 error is very likely here (because of concurrent updates happening through the api server); this is why we retry once - if err := r.client.Update(ctx, crd, client.FieldOwner(r.name)); err != nil { + if err := r.client.Update(ctx, crd, client.FieldOwner(r.fieldOwner)); err != nil { if i == 1 && apierrors.IsConflict(err) { log.V(1).Info("error while updating CustomResourcedefinition (409 conflict); doing one retry", "error", err.Error()) continue @@ -1172,9 +1186,9 @@ func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, exis if used { return fmt.Errorf("error deleting api service %s, existing instances found", types.ObjectKeyToString(key)) } - if ok := controllerutil.RemoveFinalizer(apiService, r.name); ok { + if ok := controllerutil.RemoveFinalizer(apiService, r.finalizer); ok { // note: 409 error is very likely here (because of concurrent updates happening through the api server); this is why we retry once - if err := r.client.Update(ctx, apiService, client.FieldOwner(r.name)); err != nil { + if err := r.client.Update(ctx, apiService, client.FieldOwner(r.fieldOwner)); err != nil { if i == 1 && apierrors.IsConflict(err) { log.V(1).Info("error while updating APIService (409 conflict); doing one retry", "error", err.Error()) continue From 05430a2c75b9b6941b6b53ed9f38ecbd910177bf Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Mon, 20 Jan 2025 22:17:01 +0100 Subject: [PATCH 02/13] allow to specify default service account for target deployment --- pkg/component/reconciler.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 4b13c61..503cc53 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -96,6 +96,12 @@ type ReconcilerOptions struct { // Which finalizer to use. // If unspecified, the reconciler name is used. Finalizer *string + // Default service account used for impersonation of the target client. + // If set this service account (in the namespace of the reconciled component) will be used + // to default the impersonation of the target client (that is, the client used to manage dependents); + // otherwise no impersonation happens by default, and the controller's own service account is used. + // Of course, components can still customize impersonation by implementing the ImpersonationConfiguration interface. + DefaultServiceAccount *string // Whether namespaces are auto-created if missing. // If unspecified, true is assumed. CreateMissingNamespaces *bool @@ -641,6 +647,9 @@ func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, erro } } } + if !haveClientConfiguration && !haveImpersonationConfiguration && r.options.DefaultServiceAccount != nil { + impersonationUser = fmt.Sprintf("system:serviceaccount:%s:%s", component.GetNamespace(), *r.options.DefaultServiceAccount) + } clnt, err := r.clients.Get(kubeConfig, impersonationUser, impersonationGroups) if err != nil { return nil, errors.Wrap(err, "error getting remote or impersonated client") From ccfd3f808d87f4de171012d071794e36a19efa1e Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Thu, 23 Jan 2025 19:15:00 +0100 Subject: [PATCH 03/13] rework default service account logic --- pkg/component/context.go | 45 ++++++++++++++++++++++++++++++------- pkg/component/reconciler.go | 11 +++++---- pkg/component/target.go | 2 ++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/pkg/component/context.go b/pkg/component/context.go index b8f0237..3639d83 100644 --- a/pkg/component/context.go +++ b/pkg/component/context.go @@ -13,17 +13,21 @@ import ( ) type ( - reconcilerNameContextKeyType struct{} - clientContextKeyType struct{} - componentContextKeyType struct{} - componentDigestContextKeyType struct{} + reconcilerNameContextKeyType struct{} + clientContextKeyType struct{} + componentContextKeyType struct{} + componentNameContextKeyType struct{} + componentNamespaceContextKeyType struct{} + componentDigestContextKeyType struct{} ) var ( - reconcilerNameContextKey = reconcilerNameContextKeyType{} - clientContextKey = clientContextKeyType{} - componentContextKey = componentContextKeyType{} - componentDigestContextKey = componentDigestContextKeyType{} + reconcilerNameContextKey = reconcilerNameContextKeyType{} + clientContextKey = clientContextKeyType{} + componentContextKey = componentContextKeyType{} + componentNameContextKey = componentNameContextKeyType{} + componentNamespaceContextKey = componentNamespaceContextKeyType{} + componentDigestContextKey = componentDigestContextKeyType{} ) type Context interface { @@ -31,6 +35,8 @@ type Context interface { WithReconcilerName(reconcilerName string) Context WithClient(clnt cluster.Client) Context WithComponent(component Component) Context + WithComponentName(componentName string) Context + WithComponentNamespace(componentNamespace string) Context WithComponentDigest(componentDigest string) Context } @@ -54,6 +60,14 @@ func (c *contextImpl) WithComponent(component Component) Context { return &contextImpl{Context: context.WithValue(c, componentContextKey, component)} } +func (c *contextImpl) WithComponentName(componentName string) Context { + return &contextImpl{Context: context.WithValue(c, componentNameContextKey, componentName)} +} + +func (c *contextImpl) WithComponentNamespace(componentNamespace string) Context { + return &contextImpl{Context: context.WithValue(c, componentNamespaceContextKey, componentNamespace)} +} + func (c *contextImpl) WithComponentDigest(componentDigest string) Context { return &contextImpl{Context: context.WithValue(c, componentDigestContextKey, componentDigest)} } @@ -72,6 +86,7 @@ func ClientFromContext(ctx context.Context) (cluster.Client, error) { return nil, fmt.Errorf("client not found in context") } +// TODO: should this method be parameterized? func ComponentFromContext(ctx context.Context) (Component, error) { if component, ok := ctx.Value(componentContextKey).(Component); ok { return component, nil @@ -79,6 +94,20 @@ func ComponentFromContext(ctx context.Context) (Component, error) { return nil, fmt.Errorf("component not found in context") } +func ComponentNameFromContext(ctx context.Context) (string, error) { + if componentName, ok := ctx.Value(componentNameContextKey).(string); ok { + return componentName, nil + } + return "", fmt.Errorf("component name not found in context") +} + +func ComponentNamespaceFromContext(ctx context.Context) (string, error) { + if componentNamespace, ok := ctx.Value(componentNamespaceContextKey).(string); ok { + return componentNamespace, nil + } + return "", fmt.Errorf("component namespace not found in context") +} + func ComponentDigestFromContext(ctx context.Context) (string, error) { if componentDigest, ok := ctx.Value(componentDigestContextKey).(string); ok { return componentDigest, nil diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 503cc53..5c2e9a9 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -52,11 +52,9 @@ import ( // TODO: emitting events to deployment target may fail if corresponding rbac privileges are missing; either this should be pre-discovered or we // should stop emitting events to remote targets at all; howerver pre-discovering is difficult (may vary from object to object); one option could // be to send events only if we are cluster-admin -// TODO: allow to override namespace auto-creation and reconcile policy on a per-component level +// TODO: allow to override namespace auto-creation on a per-component level // that is: consider adding them to the PolicyConfiguration interface? // TODO: allow to override namespace auto-creation on a per-object level -// TODO: allow some timeout feature, such that component will go into error state if not ready within the given timeout -// (e.g. through a TimeoutConfiguration interface that components could optionally implement) // TODO: run admission webhooks (if present) in reconcile (e.g. as post-read hook) // TODO: improve overall log output // TODO: finalizer and fieldowner should be made more configurable (instead of just using the reconciler name) @@ -554,6 +552,11 @@ func (r *Reconciler[T]) WithPostDeleteHook(hook HookFunc[T]) *Reconciler[T] { // Register the reconciler with a given controller-runtime Manager and Builder. // This will call For() and Complete() on the provided builder. +// It populates the recnciler's client with an enhnanced client derived from mgr.GetClient() and mgr.GetConfig(). +// That client is used for three purposes: +// - reading/updating the reconciled component, sending events for this component +// - it is passed to hooks +// - it is passed to the factory for target clients as a default local client func (r *Reconciler[T]) SetupWithManagerAndBuilder(mgr ctrl.Manager, blder *ctrl.Builder) error { r.setupMutex.Lock() defer r.setupMutex.Unlock() @@ -647,7 +650,7 @@ func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, erro } } } - if !haveClientConfiguration && !haveImpersonationConfiguration && r.options.DefaultServiceAccount != nil { + if len(kubeConfig) == 0 && impersonationUser == "" && len(impersonationGroups) == 0 && r.options.DefaultServiceAccount != nil { impersonationUser = fmt.Sprintf("system:serviceaccount:%s:%s", component.GetNamespace(), *r.options.DefaultServiceAccount) } clnt, err := r.clients.Get(kubeConfig, impersonationUser, impersonationGroups) diff --git a/pkg/component/target.go b/pkg/component/target.go index d695dc3..33884b8 100644 --- a/pkg/component/target.go +++ b/pkg/component/target.go @@ -56,6 +56,8 @@ func (t *reconcileTarget[T]) Apply(ctx context.Context, component T) (bool, stri WithReconcilerName(t.reconcilerName). WithClient(t.client). WithComponent(component). + WithComponentName(component.GetName()). + WithComponentNamespace(component.GetNamespace()). WithComponentDigest(componentDigest) objects, err := t.resourceGenerator.Generate(generateCtx, namespace, name, component.GetSpec()) if err != nil { From c6ed07406d42d4a3879b506f516552d145212ce4 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Fri, 24 Jan 2025 13:27:18 +0100 Subject: [PATCH 04/13] allow to set namespace creation policy on component level --- pkg/component/component.go | 4 ++++ pkg/component/reconciler.go | 21 +++++++++---------- pkg/component/types.go | 5 +++++ pkg/reconciler/reconciler.go | 19 +++++++++-------- pkg/reconciler/types.go | 10 +++++++++ .../content/en/docs/concepts/reconciler.md | 18 +++++++++++++--- 6 files changed, 54 insertions(+), 23 deletions(-) diff --git a/pkg/component/component.go b/pkg/component/component.go index ff91640..a1d70f6 100644 --- a/pkg/component/component.go +++ b/pkg/component/component.go @@ -214,6 +214,10 @@ func (s *PolicySpec) GetDeletePolicy() reconciler.DeletePolicy { return s.DeletePolicy } +func (s *PolicySpec) GetMissingNamespacesPolicy() reconciler.MissingNamespacesPolicy { + return s.MissingNamespacesPolicy +} + // 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 5c2e9a9..5a773a0 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -52,9 +52,7 @@ import ( // TODO: emitting events to deployment target may fail if corresponding rbac privileges are missing; either this should be pre-discovered or we // should stop emitting events to remote targets at all; howerver pre-discovering is difficult (may vary from object to object); one option could // be to send events only if we are cluster-admin -// TODO: allow to override namespace auto-creation on a per-component level -// that is: consider adding them to the PolicyConfiguration interface? -// TODO: allow to override namespace auto-creation on a per-object level +// TODO: allow to override namespace auto-creation on a per-object level? // TODO: run admission webhooks (if present) in reconcile (e.g. as post-read hook) // TODO: improve overall log output // TODO: finalizer and fieldowner should be made more configurable (instead of just using the reconciler name) @@ -100,9 +98,6 @@ type ReconcilerOptions struct { // otherwise no impersonation happens by default, and the controller's own service account is used. // Of course, components can still customize impersonation by implementing the ImpersonationConfiguration interface. DefaultServiceAccount *string - // Whether namespaces are auto-created if missing. - // If unspecified, true is assumed. - CreateMissingNamespaces *bool // How to react if a dependent object exists but has no or a different owner. // If unspecified, AdoptionPolicyIfUnowned is assumed. // Can be overridden by annotation on object level. @@ -115,6 +110,9 @@ type ReconcilerOptions struct { // If unspecified, DeletePolicyDelete is assumed. // Can be overridden by annotation on object level. DeletePolicy *reconciler.DeletePolicy + // Whether namespaces are auto-created if missing. + // If unspecified, MissingNamespacesPolicyCreate is assumed. + MissingNamespacesPolicy *reconciler.MissingNamespacesPolicy // SchemeBuilder allows to define additional schemes to be made available in the // target client. SchemeBuilder types.SchemeBuilder @@ -148,16 +146,14 @@ type Reconciler[T Component] struct { func NewReconciler[T Component](name string, resourceGenerator manifests.Generator, options ReconcilerOptions) *Reconciler[T] { // TOOD: validate options // TODO: currently, the defaulting here is identical to the defaulting in the underlying reconciler.Reconciler; - // 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 + // 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 if options.FieldOwner == nil { options.FieldOwner = &name } if options.Finalizer == nil { options.Finalizer = &name } - if options.CreateMissingNamespaces == nil { - options.CreateMissingNamespaces = ref(true) - } if options.AdoptionPolicy == nil { options.AdoptionPolicy = ref(reconciler.AdoptionPolicyIfUnowned) } @@ -167,6 +163,9 @@ func NewReconciler[T Component](name string, resourceGenerator manifests.Generat if options.DeletePolicy == nil { options.DeletePolicy = ref(reconciler.DeletePolicyDelete) } + if options.MissingNamespacesPolicy == nil { + options.MissingNamespacesPolicy = ref(reconciler.MissingNamespacesPolicyCreate) + } return &Reconciler[T]{ name: name, @@ -664,10 +663,10 @@ func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.Reconcile options := reconciler.ReconcilerOptions{ FieldOwner: r.options.FieldOwner, Finalizer: r.options.Finalizer, - CreateMissingNamespaces: r.options.CreateMissingNamespaces, AdoptionPolicy: r.options.AdoptionPolicy, UpdatePolicy: r.options.UpdatePolicy, DeletePolicy: r.options.DeletePolicy, + MissingNamespacesPolicy: r.options.MissingNamespacesPolicy, StatusAnalyzer: r.statusAnalyzer, Metrics: reconciler.ReconcilerMetrics{ ReadCounter: metrics.Operations.WithLabelValues(r.controllerName, "read"), diff --git a/pkg/component/types.go b/pkg/component/types.go index 8b5aef9..717a72c 100644 --- a/pkg/component/types.go +++ b/pkg/component/types.go @@ -96,6 +96,9 @@ type PolicyConfiguration interface { // Get delete policy. // Must return a valid DeletePolicy, or the empty string (then the reconciler/framework default applies). GetDeletePolicy() reconciler.DeletePolicy + // Get namspace auto-creation policy. + // Must return a valid MissingNamespacesPolicy, or the empty string (then the reconciler/framework default applies). + GetMissingNamespacesPolicy() reconciler.MissingNamespacesPolicy } // +kubebuilder:object:generate=true @@ -190,6 +193,8 @@ type PolicySpec struct { UpdatePolicy reconciler.UpdatePolicy `json:"updatePolicy,omitempty"` // +kubebuilder:validation:Enum=Delete;Orphan DeletePolicy reconciler.DeletePolicy `json:"deletePolicy,omitempty"` + // +kubebuilder:validation:Enum=DoNotCreate;Create + MissingNamespacesPolicy reconciler.MissingNamespacesPolicy `json:"missingNamespacesPolicy,omitempty"` } var _ PolicyConfiguration = &PolicySpec{} diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index da573b9..92a18b7 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -90,9 +90,6 @@ type ReconcilerOptions struct { // Which finalizer to use. // If unspecified, the reconciler name is used. Finalizer *string - // Whether namespaces are auto-created if missing. - // If unspecified, true is assumed. - CreateMissingNamespaces *bool // How to react if a dependent object exists but has no or a different owner. // If unspecified, AdoptionPolicyIfUnowned is assumed. // Can be overridden by annotation on object level. @@ -105,6 +102,9 @@ type ReconcilerOptions struct { // If unspecified, DeletePolicyDelete is assumed. // Can be overridden by annotation on object level. DeletePolicy *DeletePolicy + // Whether namespaces are auto-created if missing. + // If unspecified, MissingNamespacesPolicyCreate is assumed. + MissingNamespacesPolicy *MissingNamespacesPolicy // How to analyze the state of the dependent objects. // If unspecified, an optimized kstatus based implementation is used. StatusAnalyzer status.StatusAnalyzer @@ -128,11 +128,11 @@ type Reconciler struct { client cluster.Client statusAnalyzer status.StatusAnalyzer metrics ReconcilerMetrics - createMissingNamespaces bool adoptionPolicy AdoptionPolicy reconcilePolicy ReconcilePolicy updatePolicy UpdatePolicy deletePolicy DeletePolicy + missingNamespacesPolicy MissingNamespacesPolicy labelKeyOwnerId string annotationKeyOwnerId string annotationKeyDigest string @@ -156,9 +156,6 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions) if options.Finalizer == nil { options.Finalizer = &name } - if options.CreateMissingNamespaces == nil { - options.CreateMissingNamespaces = ref(true) - } if options.AdoptionPolicy == nil { options.AdoptionPolicy = ref(AdoptionPolicyIfUnowned) } @@ -168,6 +165,9 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions) if options.DeletePolicy == nil { options.DeletePolicy = ref(DeletePolicyDelete) } + if options.MissingNamespacesPolicy == nil { + options.MissingNamespacesPolicy = ref(MissingNamespacesPolicyCreate) + } if options.StatusAnalyzer == nil { options.StatusAnalyzer = status.NewStatusAnalyzer(name) } @@ -178,11 +178,11 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions) client: clnt, statusAnalyzer: options.StatusAnalyzer, metrics: options.Metrics, - createMissingNamespaces: *options.CreateMissingNamespaces, adoptionPolicy: *options.AdoptionPolicy, reconcilePolicy: ReconcilePolicyOnObjectChange, updatePolicy: *options.UpdatePolicy, deletePolicy: *options.DeletePolicy, + missingNamespacesPolicy: *options.MissingNamespacesPolicy, labelKeyOwnerId: name + "/" + types.LabelKeySuffixOwnerId, annotationKeyOwnerId: name + "/" + types.AnnotationKeySuffixOwnerId, annotationKeyDigest: name + "/" + types.AnnotationKeySuffixDigest, @@ -546,7 +546,8 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // - PhaseDeleting // create missing namespaces - if r.createMissingNamespaces { + // TODO: we should exclude objects which are anyway about to be deleted + if r.missingNamespacesPolicy == MissingNamespacesPolicyCreate { for _, namespace := range findMissingNamespaces(objects) { if err := r.client.Get(ctx, apitypes.NamespacedName{Name: namespace}, &corev1.Namespace{}); err != nil { if !apierrors.IsNotFound(err) { diff --git a/pkg/reconciler/types.go b/pkg/reconciler/types.go index 342e219..5ee72b0 100644 --- a/pkg/reconciler/types.go +++ b/pkg/reconciler/types.go @@ -79,6 +79,16 @@ const ( DeletePolicyOrphan DeletePolicy = "Orphan" ) +// MissingNamespacesPolicy defines what the reconciler does if namespaces of dependent objects are not existing. +type MissingNamespacesPolicy string + +const ( + // Do not create missing namespaces. + MissingNamespacesPolicyDoNotCreate MissingNamespacesPolicy = "DoNotCreate" + // Create missing namespaces. + MissingNamespacesPolicyCreate MissingNamespacesPolicy = "Create" +) + // +kubebuilder:object:generate=true // InventoryItem represents a dependent object managed by this operator. diff --git a/website/content/en/docs/concepts/reconciler.md b/website/content/en/docs/concepts/reconciler.md index 420790c..626c21f 100644 --- a/website/content/en/docs/concepts/reconciler.md +++ b/website/content/en/docs/concepts/reconciler.md @@ -36,9 +36,18 @@ The passed type parameter `T Component` is the concrete runtime type of the comp // ReconcilerOptions are creation options for a Reconciler. type ReconcilerOptions struct { - // Whether namespaces are auto-created if missing. - // If unspecified, true is assumed. - CreateMissingNamespaces *bool + // Which field manager to use in API calls. + // If unspecified, the reconciler name is used. + FieldOwner *string + // Which finalizer to use. + // If unspecified, the reconciler name is used. + Finalizer *string + // Default service account used for impersonation of the target client. + // If set this service account (in the namespace of the reconciled component) will be used + // to default the impersonation of the target client (that is, the client used to manage dependents); + // otherwise no impersonation happens by default, and the controller's own service account is used. + // Of course, components can still customize impersonation by implementing the ImpersonationConfiguration interface. + DefaultServiceAccount *string // How to react if a dependent object exists but has no or a different owner. // If unspecified, AdoptionPolicyIfUnowned is assumed. // Can be overridden by annotation on object level. @@ -51,6 +60,9 @@ The passed type parameter `T Component` is the concrete runtime type of the comp // If unspecified, DeletePolicyDelete is assumed. // Can be overridden by annotation on object level. DeletePolicy *reconciler.DeletePolicy + // Whether namespaces are auto-created if missing. + // If unspecified, MissingNamespacesPolicyCreate is assumed. + MissingNamespacesPolicy *reconciler.MissingNamespacesPolicy // SchemeBuilder allows to define additional schemes to be made available in the // target client. SchemeBuilder types.SchemeBuilder From f1124d161a10c48c6d2b9f70bc1152944568015f Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Fri, 24 Jan 2025 15:14:06 +0100 Subject: [PATCH 05/13] fix MissingNamespacesPolicy handling --- pkg/component/reconciler.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 5a773a0..9737ad2 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -686,6 +686,9 @@ func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.Reconcile if deletePolicy := policyConfiguration.GetDeletePolicy(); deletePolicy != "" { options.DeletePolicy = &deletePolicy } + if missingNamespacesPolicy := policyConfiguration.GetMissingNamespacesPolicy(); missingNamespacesPolicy != "" { + options.MissingNamespacesPolicy = &missingNamespacesPolicy + } } return options } From 1b338698663e4af596520f1068b9eda3a51b5316 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Fri, 24 Jan 2025 16:20:08 +0100 Subject: [PATCH 06/13] clm: populate generation context with new values for component name/namespace --- clm/internal/manifests/generate.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/clm/internal/manifests/generate.go b/clm/internal/manifests/generate.go index 4bacda3..0c55821 100644 --- a/clm/internal/manifests/generate.go +++ b/clm/internal/manifests/generate.go @@ -87,11 +87,14 @@ func Generate(manifestSources []string, valuesSources []string, reconcilerName s return nil, err } - // TODO: what about component and component digest + releaseComponent := componentFromRelease(release, allValues) + // TODO: what about component digest generateCtx := component.NewContext(context.TODO()). WithReconcilerName(reconcilerName). WithClient(clnt). - WithComponent(componentFromRelease(release, allValues)). + WithComponent(releaseComponent). + WithComponentName(releaseComponent.GetName()). + WithComponentNamespace(releaseComponent.GetNamespace()). WithComponentDigest("") objects, err := generator.Generate(generateCtx, release.GetNamespace(), release.GetName(), types.UnstructurableMap(allValues)) if err != nil { From f4c57b24fdf364fb3002067b52280bba43395792 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Fri, 24 Jan 2025 16:21:58 +0100 Subject: [PATCH 07/13] cleanup --- pkg/reconciler/reconciler.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 92a18b7..e53cf5b 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -546,7 +546,6 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // - PhaseDeleting // create missing namespaces - // TODO: we should exclude objects which are anyway about to be deleted if r.missingNamespacesPolicy == MissingNamespacesPolicyCreate { for _, namespace := range findMissingNamespaces(objects) { if err := r.client.Get(ctx, apitypes.NamespacedName{Name: namespace}, &corev1.Namespace{}); err != nil { From 44475bc41823345f89c8f59bd8e444fd8406570c Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Fri, 24 Jan 2025 18:10:03 +0100 Subject: [PATCH 08/13] update docs --- website/config.toml | 2 +- .../content/en/docs/concepts/reconciler.md | 44 +++++++++++++++---- website/content/en/docs/generators/helm.md | 7 ++- .../content/en/docs/generators/kustomize.md | 22 ++++++++-- .../content/en/docs/getting-started/_index.md | 28 +++++------- .../usage/{overview.md => kubebuilder.md} | 25 +++++------ website/content/en/docs/usage/scaffolder.md | 18 ++++---- 7 files changed, 90 insertions(+), 56 deletions(-) rename website/content/en/docs/usage/{overview.md => kubebuilder.md} (90%) diff --git a/website/config.toml b/website/config.toml index 23700bc..73018e3 100644 --- a/website/config.toml +++ b/website/config.toml @@ -73,7 +73,7 @@ weight = 1 unsafe = true [markup.highlight] # See a complete list of available styles at https://xyproto.github.io/splash/docs/all.html - style = "monokai" + style = "monokailight" # Uncomment if you want your chosen highlight style used for code blocks without a specified language # guessSyntax = "true" diff --git a/website/content/en/docs/concepts/reconciler.md b/website/content/en/docs/concepts/reconciler.md index 626c21f..2355805 100644 --- a/website/content/en/docs/concepts/reconciler.md +++ b/website/content/en/docs/concepts/reconciler.md @@ -26,7 +26,7 @@ func NewReconciler[T Component]( ) *Reconciler[T] ``` -The passed type parameter `T Component` is the concrete runtime type of the component's custom resource type. Furthermore, +The passed type parameter `T Component` is the concrete runtime type of the component's custom resource type (respectively, a pointer to that). Furthermore, - `name` is supposed to be a unique name (typically a DNS name) identifying this component operator in the cluster; ìt will be used in annotations, labels, for leader election, ... - `resourceGenerator` is an implementation of the `Generator` interface, describing how the dependent objects are rendered from the component's spec. - `options` can be used to tune the behavior of the reconciler: @@ -149,7 +149,7 @@ will be used instead of the backoff. Implementations should use pacakge types func NewRetriableError(err error, retryAfter *time.Duration) RetriableError { - return RetriableError{err: err, retryAfter: retryAfter} + return RetriableError{err: err, retryAfter: retryAfter} } ``` @@ -163,8 +163,8 @@ package component // The RetryConfiguration interface is meant to be implemented by components (or their spec) which offer // tweaking the retry interval (by default, it would be the value of the requeue interval). type RetryConfiguration interface { - // Get retry interval. Should be greater than 1 minute. - GetRetryInterval() time.Duration + // Get retry interval. Should be greater than 1 minute. + GetRetryInterval() time.Duration } ``` @@ -181,8 +181,8 @@ package component // The RequeueConfiguration interface is meant to be implemented by components (or their spec) which offer // tweaking the requeue interval (by default, it would be 10 minutes). type RequeueConfiguration interface { - // Get requeue interval. Should be greater than 1 minute. - GetRequeueInterval() time.Duration + // Get requeue interval. Should be greater than 1 minute. + GetRequeueInterval() time.Duration } ``` @@ -201,9 +201,37 @@ package component // The TimeoutConfiguration interface is meant to be implemented by components (or their spec) which offer // tweaking the processing timeout (by default, it would be the value of the requeue interval). type TimeoutConfiguration interface { - // Get timeout. Should be greater than 1 minute. - GetTimeout() time.Duration + // Get timeout. Should be greater than 1 minute. + GetTimeout() time.Duration } ``` interface. + +## Tuning the handling of dependent objects + +The reconciler allows to tweak how dependent objects are applied to or deleted from the cluster. +To change the shipped framework defaults, a component type can implement the + +```go +package component + +// The PolicyConfiguration interface is meant to be implemented by compoments (or their spec) which offer +// tweaking policies affecting the dependents handling. +type PolicyConfiguration interface { + // Get adoption policy. + // Must return a valid AdoptionPolicy, or the empty string (then the reconciler/framework default applies). + GetAdoptionPolicy() reconciler.AdoptionPolicy + // Get update policy. + // Must return a valid UpdatePolicy, or the empty string (then the reconciler/framework default applies). + GetUpdatePolicy() reconciler.UpdatePolicy + // Get delete policy. + // Must return a valid DeletePolicy, or the empty string (then the reconciler/framework default applies). + GetDeletePolicy() reconciler.DeletePolicy + // Get namspace auto-creation policy. + // Must return a valid MissingNamespacesPolicy, or the empty string (then the reconciler/framework default applies). + GetMissingNamespacesPolicy() reconciler.MissingNamespacesPolicy +} +``` + +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 diff --git a/website/content/en/docs/generators/helm.md b/website/content/en/docs/generators/helm.md index 609b051..b3b31d0 100644 --- a/website/content/en/docs/generators/helm.md +++ b/website/content/en/docs/generators/helm.md @@ -15,19 +15,18 @@ package helm func NewHelmGenerator( fsys fs.FS, chartPath string, - client client.Client, + clnt client.Client, ) (*HelmGenerator, error) ``` Here: - `fsys` must be an implementation of `fs.FS`, such as `embed.FS`; or it can be passed as nil; then, all file operations will be executed on the current OS filesystem. - `chartPath` is the directory containing the used Helm chart; if `fsys` was provided, this has to be a relative path; otherwise, it will be interpreted with respect to the OS filesystem (as an absolute path, or relative to the current working directory of the controller). -- `client` should be a client for the local cluster (i.e. the cluster where the component object exists). +- `clnt` should be a client for the local cluster (i.e. the cluster where the component object exists). It should be noted that `HelmGenerator` does not use the Helm SDK; instead it tries to emulate the Helm behavior as good as possible. A few differences and restrictions arise from this: -- Not all Helm template functions are supported. To be exact, `toToml`, `fromYamlArray`, `fromJsonArray` are not supported; - the functions `toYaml`, `fromYaml`, `toJson`, `fromJson` are supported, but will behave more strictly in error situtations. +- Not all Helm template functions are supported. To be exact, `toToml` is not supported; all other functions should be supported, but may behave more strictly in error situtations. - Not all builtin variables are supported; the following restrictions apply: - for the `.Release` builtin, only `.Release.Namespace`, `.Release.Name`, `.Release.Service`, `.Release.IsInstall`, `.Release.IsUpgrade` are supported; note that - since this framework does not really distinguish between installations and upgrades - `Release.IsInstall` is always set to `true`, and `Release.IsUpgrade` is always set to `false` - for the `.Chart` builtin, only `.Chart.Name`, `.Chart.Version`, `.Chart.Type`, `.Chart.AppVersion`, `.Chart.Dependencies` are supported diff --git a/website/content/en/docs/generators/kustomize.md b/website/content/en/docs/generators/kustomize.md index d27f163..414b80a 100644 --- a/website/content/en/docs/generators/kustomize.md +++ b/website/content/en/docs/generators/kustomize.md @@ -25,15 +25,29 @@ package kustomize func NewKustomizeGenerator( fsys fs.FS, kustomizationPath string, - templateSuffix string, - client client.Client + clnt client.Client, + options KustomizeGeneratorOptions ) (*KustomizeGenerator, error) { ``` Here: - `fsys` must be an implementation of `fs.FS`, such as `embed.FS`; or it can be passed as nil; then, all file operations will be executed on the current OS filesystem. - `kustomizationPath` is the directory containing the (potentially templatized) kustomatization; if `fsys` was provided, this has to be a relative path; otherwise, it will be interpreted with respect to the OS filesystem (as an absolute path, or relative to the current working directory of the controller). -- `templateSuffx` is optional; if empty, all files under `kustomizationPath` will be subject to go templating; otherwise, only files matching the specified suffix will be considered as templates. -- `client` should be a client for the local cluster (i.e. the cluster where the component object exists). +- `clnt` should be a client for the local cluster (i.e. the cluster where the component object exists). +- `options` allows to tweak the generator: + ```go + package kustomize + + type KustomizeGeneratorOptions struct { + // If defined, only files with that suffix will be subject to templating. + TemplateSuffix *string + // If defined, the given left delimiter will be used to parse go templates; + // otherwise, defaults to '{{' + LeftTemplateDelimiter *string + // If defined, the given right delimiter will be used to parse go templates; + // otherwise, defaults to '}}' + RightTemplateDelimiter *string + } + ``` As of now, the specified kustomization must not reference files or paths outside `kustomizationPath`. Remote references are generally not supported. \ No newline at end of file diff --git a/website/content/en/docs/getting-started/_index.md b/website/content/en/docs/getting-started/_index.md index 8bb9437..2fbf007 100644 --- a/website/content/en/docs/getting-started/_index.md +++ b/website/content/en/docs/getting-started/_index.md @@ -17,15 +17,14 @@ Then, a git repository for the operator code is needed; in this example, we call We assume that you have cloned the empty repository to your local desktop, and have changed the current directory to the checked out repository. -We assume here that you are implementing a [Kyma module operator](https://github.com/kyma-project/template-operator), and that -the managed component shall be represented by a Kubernetes type called `MyComponent`. Then run: +Then, to scaffold a component operator for a component type `MyComponent` in group `group.my-domain.io`, just run: ```bash scaffold-component-operator \ - --group-name operator.kyma-project.io \ + --group-name group.my-domain.io \ --group-version v1alpha1 \ --kind MyComponent \ - --operator-name mycomponent-operator.kyma-project.io \ + --operator-name mycomponent-operator.group.my-domain.io \ --go-module github.com/myorg/mycomponent-operator \ --image mycomponent-operator:latest \ . @@ -35,15 +34,15 @@ This will give you a syntactically correct Go module. In order to start the oper custom resource definition into your development (e.g. [kind](https://kind.sigs.k8s.io/)) cluster: ```bash -kubectl apply -f crds/operator.kyma-project.io_mycomponents.yaml +kubectl apply -f crds ``` -Then, after copying or linking the cluster's kubeconfig to `./tmp/kubeconfig` (no worries, it will not submitted to git because `./tmp` is excluded by `.gitignore`), you can use the generated `./vscode/launch.json` to start the +Then, after copying or linking the cluster's kubeconfig to `./tmp/kubeconfig` (no worries, it is not submitted to git because `./tmp` is excluded by `.gitignore`), you can use the generated `./vscode/launch.json` to start the operator against your cluster with your Visual Studio Code. Now you are ready to instantiate your component: ```bash kubectl apply -f - < - General implementation principles (doing it the hard way) + General implementation principles --- This framework is based on the [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime/) project. @@ -13,12 +13,12 @@ project, such as ```bash kubebuilder init \ - --domain kyma-project.io \ + --domain my-domain.io \ --repo github.com/myorg/mycomponent-operator \ --project-name=mycomponent-operator kubebuilder create api \ - --group operator \ + --group group \ --version v1alpha1 \ --kind MyComponent \ --resource \ @@ -71,7 +71,7 @@ func (c *MyComponent) GetStatus() *component.Status { } ``` -Now we are settled to replace the controller generated by kubebuilder with the component-operator-runtime reconciler in the scaffolded `main.go`: +Now we are settled to replace the controller generated by kubebuilder with the component-operator-runtime reconciler in the scaffolded `cmd/main.go`: ```go // Replace this by a real resource generator (e.g. HelmGenerator or KustomizeGenerator, or your own one). @@ -81,13 +81,10 @@ if err != nil { os.Exit(1) } -if err := component.NewReconciler[*operatorv1alpha1.MyComponent]( - "mycomponent-operator.kyma-project.io", - nil, - nil, - nil, - nil, +if err := component.NewReconciler[*groupv1alpha1.MyComponent]( + "mycomponent-operator.group.my-domain.io", resourceGenerator, + component.ReconcilerOptions{}, ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MyComponent") os.Exit(1) @@ -102,7 +99,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(apiregistrationv1.AddToScheme(scheme)) - utilruntime.Must(operatorv1alpha1.AddToScheme(scheme)) + utilruntime.Must(groupv1alpha1.AddToScheme(scheme)) } ``` @@ -114,7 +111,7 @@ mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Client: client.Options{ Cache: &client.CacheOptions{ DisableFor: []client.Object{ - &operatorv1alpha1.MyComponent{}, + &groupv1alpha1.MyComponent{}, &apiextensionsv1.CustomResourceDefinition{}, &apiregistrationv1.APIService{}, }, diff --git a/website/content/en/docs/usage/scaffolder.md b/website/content/en/docs/usage/scaffolder.md index 887d078..90feafe 100644 --- a/website/content/en/docs/usage/scaffolder.md +++ b/website/content/en/docs/usage/scaffolder.md @@ -14,9 +14,9 @@ After installing the scaffolder, a new project can be created like this: ```bash scaffold \ - --group-name operator.kyma-project.io \ + --group-name operator.group.my-domain.io \ --kind MyComponent \ - --operator-name mycomponent-operator.kyma-project.io \ + --operator-name mycomponent-operator.group.my-domain.io \ --go-module github.com/myorg/mycomponent-operator \ --image mycomponent-operator:latest \ @@ -38,14 +38,14 @@ Usage: scaffold [options] [output directory] --operator-name string Unique name for this operator, used e.g. for leader election and labels; should be a valid DNS hostname --with-validating-webhook Whether to scaffold validating webhook --with-mutating-webhook Whether to scaffold mutating webhook - --go-version string Go version to be used (default "1.21") + --go-version string Go version to be used (default "1.23.4") --go-module string Name of the Go module, as written to the go.mod file - --kubernetes-version string Kubernetes go-client version to be used (default "v0.28.1") - --controller-runtime-version string Controller-runtime version to be used (default "v0.16.0") - --controller-tools-version string Controller-tools version to be used (default "v0.13.0") - --code-generator-version string Code-generator version to be used (default "v0.28.1") - --admission-webhook-runtime-version string Admission-webhook-runtime version to be used (default "v0.1.0") - --envtest-kubernetes-version string Kubernetes version to be used by envtest (default "1.27.1") + --kubernetes-version string Kubernetes go-client version to be used (default "v0.32.0") + --controller-runtime-version string Controller-runtime version to be used (default "v0.19.3") + --controller-tools-version string Controller-tools version to be used (default "v0.16.5") + --code-generator-version string Code-generator version to be used (default "v0.32.0") + --admission-webhook-runtime-version string Admission-webhook-runtime version to be used (default "v0.1.52") + --envtest-kubernetes-version string Kubernetes version to be used by envtest (default "1.30.3") --image string Name of the Docker/OCI image produced by this project (default "controller:latest") --skip-post-processing Skip post-processing ``` From f589daf7b96a4e36b870fefde44f1d41f640acaf Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Fri, 24 Jan 2025 19:10:25 +0100 Subject: [PATCH 09/13] update hugo/docsy --- .github/workflows/publish-website.yaml | 2 +- website/content/en/_index.html | 2 +- website/themes/docsy | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-website.yaml b/.github/workflows/publish-website.yaml index 0a39e5d..7ace6d4 100644 --- a/.github/workflows/publish-website.yaml +++ b/.github/workflows/publish-website.yaml @@ -27,7 +27,7 @@ jobs: build: runs-on: ubuntu-24.04 env: - HUGO_VERSION: 0.111.2 + HUGO_VERSION: 0.142.0 steps: - name: Install Hugo diff --git a/website/content/en/_index.html b/website/content/en/_index.html index 6800cf5..faab9f2 100644 --- a/website/content/en/_index.html +++ b/website/content/en/_index.html @@ -4,7 +4,7 @@ +++ -{{< blocks/cover title="component-operator-runtime" image_anchor="top" height="full" color="blue" >}} +{{< blocks/cover title="component-operator-runtime" image_anchor="top" height="full" color="primary" >}}
A Kubernetes Component Operator Framework


}}"> diff --git a/website/themes/docsy b/website/themes/docsy index 5597d43..cf0c68f 160000 --- a/website/themes/docsy +++ b/website/themes/docsy @@ -1 +1 @@ -Subproject commit 5597d435dc74ce68240e0c3871addf24567493b0 +Subproject commit cf0c68f041daac066a0292d521461dbd092d7c31 From a12fb7971699b5cdabeaf3cf48de98274876cf62 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Fri, 24 Jan 2025 20:45:08 +0100 Subject: [PATCH 10/13] update comments --- pkg/component/reconciler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 9737ad2..69625af 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -97,6 +97,7 @@ type ReconcilerOptions struct { // to default the impersonation of the target client (that is, the client used to manage dependents); // otherwise no impersonation happens by default, and the controller's own service account is used. // Of course, components can still customize impersonation by implementing the ImpersonationConfiguration interface. + // Note that this setting has no meaning if the reconciled component specifies a kubeconfig. DefaultServiceAccount *string // How to react if a dependent object exists but has no or a different owner. // If unspecified, AdoptionPolicyIfUnowned is assumed. From 5920a5cf0b7a32face9c91de0723757ad8ea6567 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Sun, 26 Jan 2025 14:10:36 +0100 Subject: [PATCH 11/13] fix impersonation logic, handle local client better --- clm/internal/manifests/generate.go | 5 +- pkg/component/context.go | 14 ++++ pkg/component/reconciler.go | 74 ++++++++++++++----- pkg/component/target.go | 5 +- pkg/manifests/helm/generator.go | 20 ++--- pkg/manifests/kustomize/generator.go | 16 ++-- website/content/en/docs/concepts/clients.md | 12 +++ .../content/en/docs/concepts/reconciler.md | 9 +-- website/content/en/docs/concepts/types.md | 3 +- 9 files changed, 113 insertions(+), 45 deletions(-) create mode 100644 website/content/en/docs/concepts/clients.md diff --git a/clm/internal/manifests/generate.go b/clm/internal/manifests/generate.go index 0c55821..9f0457a 100644 --- a/clm/internal/manifests/generate.go +++ b/clm/internal/manifests/generate.go @@ -74,12 +74,12 @@ func Generate(manifestSources []string, valuesSources []string, reconcilerName s var generator manifests.Generator if _, err = fs.Stat(fsys, "Chart.yaml"); err == nil { - generator, err = helm.NewHelmGenerator(fsys, "", clnt) + generator, err = helm.NewHelmGenerator(fsys, "", nil) if err != nil { return nil, err } } else if errors.Is(err, fs.ErrNotExist) { - generator, err = kustomize.NewKustomizeGenerator(fsys, "", clnt, kustomize.KustomizeGeneratorOptions{}) + generator, err = kustomize.NewKustomizeGenerator(fsys, "", nil, kustomize.KustomizeGeneratorOptions{}) if err != nil { return nil, err } @@ -91,6 +91,7 @@ func Generate(manifestSources []string, valuesSources []string, reconcilerName s // TODO: what about component digest generateCtx := component.NewContext(context.TODO()). WithReconcilerName(reconcilerName). + WithLocalClient(clnt). WithClient(clnt). WithComponent(releaseComponent). WithComponentName(releaseComponent.GetName()). diff --git a/pkg/component/context.go b/pkg/component/context.go index 3639d83..727437b 100644 --- a/pkg/component/context.go +++ b/pkg/component/context.go @@ -14,6 +14,7 @@ import ( type ( reconcilerNameContextKeyType struct{} + localClientContextKeyType struct{} clientContextKeyType struct{} componentContextKeyType struct{} componentNameContextKeyType struct{} @@ -23,6 +24,7 @@ type ( var ( reconcilerNameContextKey = reconcilerNameContextKeyType{} + localClientContextKey = localClientContextKeyType{} clientContextKey = clientContextKeyType{} componentContextKey = componentContextKeyType{} componentNameContextKey = componentNameContextKeyType{} @@ -33,6 +35,7 @@ var ( type Context interface { context.Context WithReconcilerName(reconcilerName string) Context + WithLocalClient(clnt cluster.Client) Context WithClient(clnt cluster.Client) Context WithComponent(component Component) Context WithComponentName(componentName string) Context @@ -52,6 +55,10 @@ func (c *contextImpl) WithReconcilerName(reconcilerName string) Context { return &contextImpl{Context: context.WithValue(c, reconcilerNameContextKey, reconcilerName)} } +func (c *contextImpl) WithLocalClient(clnt cluster.Client) Context { + return &contextImpl{Context: context.WithValue(c, localClientContextKey, clnt)} +} + func (c *contextImpl) WithClient(clnt cluster.Client) Context { return &contextImpl{Context: context.WithValue(c, clientContextKey, clnt)} } @@ -79,6 +86,13 @@ func ReconcilerNameFromContext(ctx context.Context) (string, error) { return "", fmt.Errorf("reconciler name not found in context") } +func LocalClientFromContext(ctx context.Context) (cluster.Client, error) { + if clnt, ok := ctx.Value(localClientContextKey).(cluster.Client); ok { + return clnt, nil + } + return nil, fmt.Errorf("local client not found in context") +} + func ClientFromContext(ctx context.Context) (cluster.Client, error) { if clnt, ok := ctx.Value(clientContextKey).(cluster.Client); ok { return clnt, nil diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 69625af..6241c16 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -92,12 +92,8 @@ type ReconcilerOptions struct { // Which finalizer to use. // If unspecified, the reconciler name is used. Finalizer *string - // Default service account used for impersonation of the target client. - // If set this service account (in the namespace of the reconciled component) will be used - // to default the impersonation of the target client (that is, the client used to manage dependents); - // otherwise no impersonation happens by default, and the controller's own service account is used. + // Default service account used for impersonation of clients. // Of course, components can still customize impersonation by implementing the ImpersonationConfiguration interface. - // Note that this setting has no meaning if the reconciled component specifies a kubeconfig. DefaultServiceAccount *string // How to react if a dependent object exists but has no or a different owner. // If unspecified, AdoptionPolicyIfUnowned is assumed. @@ -351,15 +347,22 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result } // setup target + localClient, err := r.getLocalClientForComponent(component) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "error getting local client for component") + } targetClient, err := r.getClientForComponent(component) if err != nil { return ctrl.Result{}, errors.Wrap(err, "error getting client for component") } targetOptions := r.getOptionsForComponent(component) - target := newReconcileTarget[T](r.name, r.id, targetClient, r.resourceGenerator, targetOptions) + target := newReconcileTarget[T](r.name, r.id, localClient, targetClient, r.resourceGenerator, targetOptions) // TODO: enhance ctx with tailored logger and event recorder // TODO: enhance ctx with the local client - hookCtx = NewContext(ctx).WithReconcilerName(r.name).WithClient(targetClient) + hookCtx = NewContext(ctx). + WithReconcilerName(r.name). + WithLocalClient(localClient). + WithClient(targetClient) // do the reconciliation if component.GetDeletionTimestamp().IsZero() { @@ -623,8 +626,49 @@ func (r *Reconciler[T]) SetupWithManager(mgr ctrl.Manager) error { ) } +func (r *Reconciler[T]) getLocalClientForComponent(component T) (cluster.Client, error) { + impersonationConfiguration, haveImpersonationConfiguration := assertImpersonationConfiguration(component) + + var impersonationUser string + var impersonationGroups []string + if haveImpersonationConfiguration { + impersonationUser = impersonationConfiguration.GetImpersonationUser() + impersonationGroups = impersonationConfiguration.GetImpersonationGroups() + // note: the following is needed due to the implementation of ImpersonationSpec + if m := regexp.MustCompile(`^(system:serviceaccount):(.*):(.+)$`).FindStringSubmatch(impersonationUser); m != nil { + if m[2] == "" { + impersonationUser = fmt.Sprintf("%s:%s:%s", m[1], component.GetNamespace(), m[3]) + } + } + } + if impersonationUser == "" && len(impersonationGroups) == 0 && r.options.DefaultServiceAccount != nil { + impersonationUser = fmt.Sprintf("system:serviceaccount:%s:%s", component.GetNamespace(), *r.options.DefaultServiceAccount) + } + clnt, err := r.clients.Get(nil, impersonationUser, impersonationGroups) + if err != nil { + return nil, errors.Wrap(err, "error getting local client") + } + return clnt, nil +} + func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, error) { - placementConfiguration, havePlacementConfiguration := assertPlacementConfiguration(component) + /* + // we could also write it like this: + clientConfiguration, haveClientConfiguration := assertClientConfiguration(component) + + var kubeConfig []byte + if haveClientConfiguration { + kubeConfig = clientConfiguration.GetKubeConfig() + } + if len(kubeConfig) > 0 { + clnt, err := r.clients.Get(kubeConfig, "", nil) + if err != nil { + return nil, errors.Wrap(err, "error getting target client") + } + return clnt, nil + } + return r.getLocalClientForComponent(component) + */ clientConfiguration, haveClientConfiguration := assertClientConfiguration(component) impersonationConfiguration, haveImpersonationConfiguration := assertImpersonationConfiguration(component) @@ -634,19 +678,13 @@ func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, erro if haveClientConfiguration { kubeConfig = clientConfiguration.GetKubeConfig() } - if haveImpersonationConfiguration { + if len(kubeConfig) == 0 && haveImpersonationConfiguration { impersonationUser = impersonationConfiguration.GetImpersonationUser() impersonationGroups = impersonationConfiguration.GetImpersonationGroups() + // note: the following is needed due to the implementation of ImpersonationSpec if m := regexp.MustCompile(`^(system:serviceaccount):(.*):(.+)$`).FindStringSubmatch(impersonationUser); m != nil { if m[2] == "" { - namespace := "" - if havePlacementConfiguration { - namespace = placementConfiguration.GetDeploymentNamespace() - } - if namespace == "" { - namespace = component.GetNamespace() - } - impersonationUser = fmt.Sprintf("%s:%s:%s", m[1], namespace, m[3]) + impersonationUser = fmt.Sprintf("%s:%s:%s", m[1], component.GetNamespace(), m[3]) } } } @@ -655,7 +693,7 @@ func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, erro } clnt, err := r.clients.Get(kubeConfig, impersonationUser, impersonationGroups) if err != nil { - return nil, errors.Wrap(err, "error getting remote or impersonated client") + return nil, errors.Wrap(err, "error getting target client") } return clnt, nil } diff --git a/pkg/component/target.go b/pkg/component/target.go index 33884b8..1ae240d 100644 --- a/pkg/component/target.go +++ b/pkg/component/target.go @@ -19,15 +19,17 @@ type reconcileTarget[T Component] struct { reconciler *reconciler.Reconciler reconcilerName string reconcilerId string + localClient cluster.Client client cluster.Client resourceGenerator manifests.Generator } -func newReconcileTarget[T Component](reconcilerName string, reconcilerId string, clnt cluster.Client, resourceGenerator manifests.Generator, options reconciler.ReconcilerOptions) *reconcileTarget[T] { +func newReconcileTarget[T Component](reconcilerName string, reconcilerId string, localClient cluster.Client, clnt cluster.Client, resourceGenerator manifests.Generator, options reconciler.ReconcilerOptions) *reconcileTarget[T] { return &reconcileTarget[T]{ reconcilerName: reconcilerName, reconcilerId: reconcilerId, reconciler: reconciler.NewReconciler(reconcilerName, clnt, options), + localClient: localClient, client: clnt, resourceGenerator: resourceGenerator, } @@ -54,6 +56,7 @@ func (t *reconcileTarget[T]) Apply(ctx context.Context, component T) (bool, stri // TODO: enhance ctx with local client generateCtx := NewContext(ctx). WithReconcilerName(t.reconcilerName). + WithLocalClient(t.localClient). WithClient(t.client). WithComponent(component). WithComponentName(component.GetName()). diff --git a/pkg/manifests/helm/generator.go b/pkg/manifests/helm/generator.go index 4a1eeda..81d6ad2 100644 --- a/pkg/manifests/helm/generator.go +++ b/pkg/manifests/helm/generator.go @@ -25,11 +25,10 @@ import ( // HelmGenerator is a Generator implementation that basically renders a given Helm chart. // A few restrictions apply to the provided Helm chart: it must not contain any subcharts, some template functions are not supported, // some bultin variables are not supported, and hooks are processed in a slightly different fashion. -// Note: HelmGenerator's Generate() method expects client and reconciler name to be set in the passed context; -// see: Context.WithClient() and Context.WithReconcilerName() in package pkg/component. +// Note: HelmGenerator's Generate() method expects local client, client and reconciler name to be set in the passed context; +// see: Context.WithLocalClient(), Context.WithClient() and Context.WithReconcilerName() in package pkg/component. type HelmGenerator struct { - client client.Client - chart *helm.Chart + chart *helm.Chart } var _ manifests.Generator = &HelmGenerator{} @@ -37,18 +36,17 @@ var _ manifests.Generator = &HelmGenerator{} // TODO: add a way to pass custom template functions // Create a new HelmGenerator. -// The parameter client should be a client for the local cluster (i.e. the cluster where the component object resides); -// it is used by the localLookup and mustLocalLookup template functions. +// The client parameter is deprecated (ignored) and will be removed in a future release. // If fsys is nil, the local operating system filesystem will be used, and chartPath can be an absolute or relative path (in the latter case it will be considered // relative to the current working directory). If fsys is non-nil, then chartPath should be a relative path; if an absolute path is supplied, it will be turned // An empty chartPath will be treated like ".". -func NewHelmGenerator(fsys fs.FS, chartPath string, clnt client.Client) (*HelmGenerator, error) { +func NewHelmGenerator(fsys fs.FS, chartPath string, _ client.Client) (*HelmGenerator, error) { chart, err := helm.ParseChart(fsys, chartPath, nil) if err != nil { return nil, err } - return &HelmGenerator{client: clnt, chart: chart}, nil + return &HelmGenerator{chart: chart}, nil } // Create a new HelmGenerator as TransformableGenerator. @@ -86,13 +84,17 @@ func (g *HelmGenerator) Generate(ctx context.Context, namespace string, name str if err != nil { return nil, err } + localClient, err := component.LocalClientFromContext(ctx) + if err != nil { + return nil, err + } clnt, err := component.ClientFromContext(ctx) if err != nil { return nil, err } renderedObjects, err := g.chart.Render(helm.RenderContext{ - LocalClient: g.client, + LocalClient: localClient, Client: clnt, DiscoveryClient: clnt.DiscoveryClient(), Release: &helm.Release{ diff --git a/pkg/manifests/kustomize/generator.go b/pkg/manifests/kustomize/generator.go index df4cb08..cbb20d6 100644 --- a/pkg/manifests/kustomize/generator.go +++ b/pkg/manifests/kustomize/generator.go @@ -53,8 +53,8 @@ type KustomizeGeneratorOptions struct { } // KustomizeGenerator is a Generator implementation that basically renders a given Kustomization. -// Note: KustomizeGenerator's Generate() method expects client and component to be set in the passed context; -// see: Context.WithClient() and Context.WithComponent() in package pkg/component. +// Note: KustomizeGenerator's Generate() method expects local client, client and component to be set in the passed context; +// see: Context.WithLocalClient(), Context.WithClient() and Context.WithComponent() in package pkg/component. type KustomizeGenerator struct { kustomizer *krusty.Kustomizer files map[string][]byte @@ -66,12 +66,11 @@ var _ manifests.Generator = &KustomizeGenerator{} // TODO: add a way to pass custom template functions // Create a new KustomizeGenerator. -// The parameter client should be a client for the local cluster (i.e. the cluster where the component object resides); -// it is used by the localLookup and mustLocalLookup template functions. +// The client parameter is deprecated (ignored) and will be removed in a future release. // If fsys is nil, the local operating system filesystem will be used, and kustomizationPath can be an absolute or relative path (in the latter case it will be considered // relative to the current working directory). If fsys is non-nil, then kustomizationPath should be a relative path; if an absolute path is supplied, it will be turned // An empty kustomizationPath will be treated like ".". -func NewKustomizeGenerator(fsys fs.FS, kustomizationPath string, clnt client.Client, options KustomizeGeneratorOptions) (*KustomizeGenerator, error) { +func NewKustomizeGenerator(fsys fs.FS, kustomizationPath string, _ client.Client, options KustomizeGeneratorOptions) (*KustomizeGenerator, error) { if options.TemplateSuffix == nil { options.TemplateSuffix = ref("") } @@ -139,7 +138,7 @@ func NewKustomizeGenerator(fsys fs.FS, kustomizationPath string, clnt client.Cli Funcs(sprig.TxtFuncMap()). Funcs(templatex.FuncMap()). Funcs(templatex.FuncMapForTemplate(nil)). - Funcs(templatex.FuncMapForLocalClient(clnt)). + Funcs(templatex.FuncMapForLocalClient(nil)). Funcs(templatex.FuncMapForClient(nil)). Funcs(funcMapForGenerateContext(nil, nil, "", "")) } else { @@ -190,6 +189,10 @@ func NewKustomizeGeneratorWithObjectTransformer(fsys fs.FS, kustomizationPath st func (g *KustomizeGenerator) Generate(ctx context.Context, namespace string, name string, parameters types.Unstructurable) ([]client.Object, error) { var objects []client.Object + localClient, err := component.LocalClientFromContext(ctx) + if err != nil { + return nil, err + } clnt, err := component.ClientFromContext(ctx) if err != nil { return nil, err @@ -221,6 +224,7 @@ func (g *KustomizeGenerator) Generate(ctx context.Context, namespace string, nam } t0.Option("missingkey=zero"). Funcs(templatex.FuncMapForTemplate(t0)). + Funcs(templatex.FuncMapForLocalClient(localClient)). Funcs(templatex.FuncMapForClient(clnt)). Funcs(funcMapForGenerateContext(serverInfo, component, namespace, name)) } diff --git a/website/content/en/docs/concepts/clients.md b/website/content/en/docs/concepts/clients.md new file mode 100644 index 0000000..dae2e21 --- /dev/null +++ b/website/content/en/docs/concepts/clients.md @@ -0,0 +1,12 @@ +--- +title: "Kubernetes Clients" +linkTitle: "Kubernetes Clients" +weight: 40 +type: "docs" +description: > + How the framework connects to Kubernetes clusters +--- + +When a component resource is reconciled, two Kubernetes API clients are constructed: +- The local client; it always points to the cluster where the component resides. If the component implements impersonation (that is, the component type or its spec implements the `ImpersonationConfiguration` interface), and an impersonation user or groups are specified by the component resource, then the specified user and groups are used to impersonate the controller's kubeconfig. Otherwise, if a `DefaultServiceAccount` is defined in the reconciler's options, then that service account (relative to the components `metadata.namespace` ) is used to impersonate the controller's kubeconfig. Otherwise, the controller's kubeconfig itself is used to build the local client. The local client is passed to generators via their context. For example, the `HelmGenerator` and `KustomizeGenerator` provided by component-operator-runtime use the local client to realize the `localLookup` and `mustLocalLookup` template functions. +- The target client; if the component specifies a kubeconfig (by implementing the `ClientConfiguration` interface), then that kubeconfig is used to build the target client. Otherwise, a local client is used (possibly impersonated), created according the the logic described above. The target client is used to manage dependent objects, and is passed to generators via their context. For example, the `HelmGenerator` and `KustomizeGenerator` provided by component-operator-runtime use the target client to realize the `lookup` and `mustLookup` template functions. \ No newline at end of file diff --git a/website/content/en/docs/concepts/reconciler.md b/website/content/en/docs/concepts/reconciler.md index 2355805..ad667de 100644 --- a/website/content/en/docs/concepts/reconciler.md +++ b/website/content/en/docs/concepts/reconciler.md @@ -42,10 +42,7 @@ The passed type parameter `T Component` is the concrete runtime type of the comp // Which finalizer to use. // If unspecified, the reconciler name is used. Finalizer *string - // Default service account used for impersonation of the target client. - // If set this service account (in the namespace of the reconciled component) will be used - // to default the impersonation of the target client (that is, the client used to manage dependents); - // otherwise no impersonation happens by default, and the controller's own service account is used. + // Default service account used for impersonation of clients. // Of course, components can still customize impersonation by implementing the ImpersonationConfiguration interface. DefaultServiceAccount *string // How to react if a dependent object exists but has no or a different owner. @@ -134,9 +131,7 @@ func (r *Reconciler[T]) WithPreDeleteHook(hook HookFunc[T]) *Reconciler[T] func (r *Reconciler[T]) WithPostDeleteHook(hook HookFunc[T]) *Reconciler[T] ``` -Note that the client passed to the hook functions is the client of the manager that was used when calling `SetupWithManager()` -(that is, the return value of that manager's `GetClient()` method). In addition, reconcile and delete hooks (that is, all except the -post-read hook) can retrieve a client for the deployment target by calling `ClientFromContext()`. +Note that the client passed to the hook functions is the client of the manager that was used when calling `SetupWithManager()` (that is, the return value of that manager's `GetClient()` method). In addition, reconcile and delete hooks (that is, all except the post-read hook) can retrieve a client for the deployment target by calling `ClientFromContext()`. ## Tuning the retry behavior diff --git a/website/content/en/docs/concepts/types.md b/website/content/en/docs/concepts/types.md index 54a4a29..98d63e6 100644 --- a/website/content/en/docs/concepts/types.md +++ b/website/content/en/docs/concepts/types.md @@ -118,8 +118,7 @@ type ImpersonationConfiguration interface { } ``` -to use different user/groups for the deployment of dependent objects. Implementing both `ClientConfiguration` and `ImpersonationConfiguration` means that -the provided kubeconfig will be impersonated as specified. +to use different user/groups for the deployment of dependent objects. Note that, as mentioned above, the interfaces `PlacementConfiguration`, `ClientConfiguration` and `ImpersonationConfiguration` can be implemented by the component itself as well as by its spec type. In the theoretical case that both is the case, the implementation on the component level takes higher precedence. From 69ccdb386d0eaffdf6142170c0851bdeeffef6ea Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Sun, 26 Jan 2025 14:27:22 +0100 Subject: [PATCH 12/13] cleanup --- pkg/manifests/helm/generator.go | 12 ++++++------ pkg/manifests/kustomize/generator.go | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/manifests/helm/generator.go b/pkg/manifests/helm/generator.go index 81d6ad2..ade297a 100644 --- a/pkg/manifests/helm/generator.go +++ b/pkg/manifests/helm/generator.go @@ -50,8 +50,8 @@ func NewHelmGenerator(fsys fs.FS, chartPath string, _ client.Client) (*HelmGener } // Create a new HelmGenerator as TransformableGenerator. -func NewTransformableHelmGenerator(fsys fs.FS, chartPath string, clnt client.Client) (manifests.TransformableGenerator, error) { - g, err := NewHelmGenerator(fsys, chartPath, clnt) +func NewTransformableHelmGenerator(fsys fs.FS, chartPath string, _ client.Client) (manifests.TransformableGenerator, error) { + g, err := NewHelmGenerator(fsys, chartPath, nil) if err != nil { return nil, err } @@ -59,8 +59,8 @@ func NewTransformableHelmGenerator(fsys fs.FS, chartPath string, clnt client.Cli } // Create a new HelmGenerator with a ParameterTransformer attached (further transformers can be attached to the returned generator object). -func NewHelmGeneratorWithParameterTransformer(fsys fs.FS, chartPath string, clnt client.Client, transformer manifests.ParameterTransformer) (manifests.TransformableGenerator, error) { - g, err := NewTransformableHelmGenerator(fsys, chartPath, clnt) +func NewHelmGeneratorWithParameterTransformer(fsys fs.FS, chartPath string, _ client.Client, transformer manifests.ParameterTransformer) (manifests.TransformableGenerator, error) { + g, err := NewTransformableHelmGenerator(fsys, chartPath, nil) if err != nil { return nil, err } @@ -68,8 +68,8 @@ func NewHelmGeneratorWithParameterTransformer(fsys fs.FS, chartPath string, clnt } // Create a new HelmGenerator with an ObjectTransformer attached (further transformers can be attached to the returned generator object). -func NewHelmGeneratorWithObjectTransformer(fsys fs.FS, chartPath string, clnt client.Client, transformer manifests.ObjectTransformer) (manifests.TransformableGenerator, error) { - g, err := NewTransformableHelmGenerator(fsys, chartPath, clnt) +func NewHelmGeneratorWithObjectTransformer(fsys fs.FS, chartPath string, _ client.Client, transformer manifests.ObjectTransformer) (manifests.TransformableGenerator, error) { + g, err := NewTransformableHelmGenerator(fsys, chartPath, nil) if err != nil { return nil, err } diff --git a/pkg/manifests/kustomize/generator.go b/pkg/manifests/kustomize/generator.go index cbb20d6..b558a1d 100644 --- a/pkg/manifests/kustomize/generator.go +++ b/pkg/manifests/kustomize/generator.go @@ -159,8 +159,8 @@ func NewKustomizeGenerator(fsys fs.FS, kustomizationPath string, _ client.Client } // Create a new KustomizeGenerator as TransformableGenerator. -func NewTransformableKustomizeGenerator(fsys fs.FS, kustomizationPath string, clnt client.Client, options KustomizeGeneratorOptions) (manifests.TransformableGenerator, error) { - g, err := NewKustomizeGenerator(fsys, kustomizationPath, clnt, options) +func NewTransformableKustomizeGenerator(fsys fs.FS, kustomizationPath string, _ client.Client, options KustomizeGeneratorOptions) (manifests.TransformableGenerator, error) { + g, err := NewKustomizeGenerator(fsys, kustomizationPath, nil, options) if err != nil { return nil, err } @@ -168,8 +168,8 @@ func NewTransformableKustomizeGenerator(fsys fs.FS, kustomizationPath string, cl } // Create a new KustomizeGenerator with a ParameterTransformer attached (further transformers can be attached to the returned generator object). -func NewKustomizeGeneratorWithParameterTransformer(fsys fs.FS, kustomizationPath string, clnt client.Client, options KustomizeGeneratorOptions, transformer manifests.ParameterTransformer) (manifests.TransformableGenerator, error) { - g, err := NewTransformableKustomizeGenerator(fsys, kustomizationPath, clnt, options) +func NewKustomizeGeneratorWithParameterTransformer(fsys fs.FS, kustomizationPath string, _ client.Client, options KustomizeGeneratorOptions, transformer manifests.ParameterTransformer) (manifests.TransformableGenerator, error) { + g, err := NewTransformableKustomizeGenerator(fsys, kustomizationPath, nil, options) if err != nil { return nil, err } @@ -177,8 +177,8 @@ func NewKustomizeGeneratorWithParameterTransformer(fsys fs.FS, kustomizationPath } // Create a new KustomizeGenerator with an ObjectTransformer attached (further transformers can be attached to the returned generator object). -func NewKustomizeGeneratorWithObjectTransformer(fsys fs.FS, kustomizationPath string, clnt client.Client, options KustomizeGeneratorOptions, transformer manifests.ObjectTransformer) (manifests.TransformableGenerator, error) { - g, err := NewTransformableKustomizeGenerator(fsys, kustomizationPath, clnt, options) +func NewKustomizeGeneratorWithObjectTransformer(fsys fs.FS, kustomizationPath string, _ client.Client, options KustomizeGeneratorOptions, transformer manifests.ObjectTransformer) (manifests.TransformableGenerator, error) { + g, err := NewTransformableKustomizeGenerator(fsys, kustomizationPath, nil, options) if err != nil { return nil, err } From 89b80ab1d9c3db1a4b7cf04d2e89566f2c5ac994 Mon Sep 17 00:00:00 2001 From: Christoph Barbian Date: Sun, 26 Jan 2025 15:18:49 +0100 Subject: [PATCH 13/13] fix handling of empty DefaultServiceAccount --- pkg/component/reconciler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/component/reconciler.go b/pkg/component/reconciler.go index 6241c16..9765451 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -641,7 +641,7 @@ func (r *Reconciler[T]) getLocalClientForComponent(component T) (cluster.Client, } } } - if impersonationUser == "" && len(impersonationGroups) == 0 && r.options.DefaultServiceAccount != nil { + if impersonationUser == "" && len(impersonationGroups) == 0 && r.options.DefaultServiceAccount != nil && *r.options.DefaultServiceAccount != "" { impersonationUser = fmt.Sprintf("system:serviceaccount:%s:%s", component.GetNamespace(), *r.options.DefaultServiceAccount) } clnt, err := r.clients.Get(nil, impersonationUser, impersonationGroups) @@ -688,7 +688,7 @@ func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, erro } } } - if len(kubeConfig) == 0 && impersonationUser == "" && len(impersonationGroups) == 0 && r.options.DefaultServiceAccount != nil { + if len(kubeConfig) == 0 && impersonationUser == "" && len(impersonationGroups) == 0 && r.options.DefaultServiceAccount != nil && *r.options.DefaultServiceAccount != "" { impersonationUser = fmt.Sprintf("system:serviceaccount:%s:%s", component.GetNamespace(), *r.options.DefaultServiceAccount) } clnt, err := r.clients.Get(kubeConfig, impersonationUser, impersonationGroups)