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/clm/internal/manifests/generate.go b/clm/internal/manifests/generate.go index 4bacda3..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 } @@ -87,11 +87,15 @@ 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). + WithLocalClient(clnt). 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 { 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/context.go b/pkg/component/context.go index b8f0237..727437b 100644 --- a/pkg/component/context.go +++ b/pkg/component/context.go @@ -13,24 +13,33 @@ import ( ) type ( - reconcilerNameContextKeyType struct{} - clientContextKeyType struct{} - componentContextKeyType struct{} - componentDigestContextKeyType struct{} + reconcilerNameContextKeyType struct{} + localClientContextKeyType struct{} + clientContextKeyType struct{} + componentContextKeyType struct{} + componentNameContextKeyType struct{} + componentNamespaceContextKeyType struct{} + componentDigestContextKeyType struct{} ) var ( - reconcilerNameContextKey = reconcilerNameContextKeyType{} - clientContextKey = clientContextKeyType{} - componentContextKey = componentContextKeyType{} - componentDigestContextKey = componentDigestContextKeyType{} + reconcilerNameContextKey = reconcilerNameContextKeyType{} + localClientContextKey = localClientContextKeyType{} + clientContextKey = clientContextKeyType{} + componentContextKey = componentContextKeyType{} + componentNameContextKey = componentNameContextKeyType{} + componentNamespaceContextKey = componentNamespaceContextKeyType{} + componentDigestContextKey = componentDigestContextKeyType{} ) 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 + WithComponentNamespace(componentNamespace string) Context WithComponentDigest(componentDigest string) Context } @@ -46,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)} } @@ -54,6 +67,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)} } @@ -65,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 @@ -72,6 +100,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 +108,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 4dd49cb..9765451 100644 --- a/pkg/component/reconciler.go +++ b/pkg/component/reconciler.go @@ -52,11 +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 and reconcile policy 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: 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) @@ -90,9 +86,15 @@ type HookFunc[T Component] func(ctx context.Context, clnt client.Client, compone // 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 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. // If unspecified, AdoptionPolicyIfUnowned is assumed. // Can be overridden by annotation on object level. @@ -105,6 +107,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 @@ -137,10 +142,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; - // 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.CreateMissingNamespaces == nil { - options.CreateMissingNamespaces = ref(true) + // 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.AdoptionPolicy == nil { options.AdoptionPolicy = ref(reconciler.AdoptionPolicyIfUnowned) @@ -151,6 +160,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, @@ -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{} } @@ -335,22 +347,29 @@ 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() { // 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 +434,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 +457,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") } } @@ -536,6 +555,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() @@ -602,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 && *r.options.DefaultServiceAccount != "" { + 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) @@ -613,35 +678,34 @@ 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]) } } } + 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) 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 } func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.ReconcilerOptions { options := reconciler.ReconcilerOptions{ - CreateMissingNamespaces: r.options.CreateMissingNamespaces, + FieldOwner: r.options.FieldOwner, + Finalizer: r.options.Finalizer, 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"), @@ -661,6 +725,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 } diff --git a/pkg/component/target.go b/pkg/component/target.go index d695dc3..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,8 +56,11 @@ 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()). + WithComponentNamespace(component.GetNamespace()). WithComponentDigest(componentDigest) objects, err := t.resourceGenerator.Generate(generateCtx, namespace, name, component.GetSpec()) if err != nil { 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/manifests/helm/generator.go b/pkg/manifests/helm/generator.go index 4a1eeda..ade297a 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,23 +36,22 @@ 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. -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 } @@ -61,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 } @@ -70,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 } @@ -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..b558a1d 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 { @@ -160,8 +159,8 @@ func NewKustomizeGenerator(fsys fs.FS, kustomizationPath string, clnt client.Cli } // 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 } @@ -169,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 } @@ -178,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 } @@ -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/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index ac21871..e53cf5b 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -84,9 +84,12 @@ var deletePolicyByAnnotation = map[string]DeletePolicy{ // 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 // 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. @@ -99,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 @@ -117,15 +123,16 @@ 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 - createMissingNamespaces bool adoptionPolicy AdoptionPolicy reconcilePolicy ReconcilePolicy updatePolicy UpdatePolicy deletePolicy DeletePolicy + missingNamespacesPolicy MissingNamespacesPolicy labelKeyOwnerId string annotationKeyOwnerId string annotationKeyDigest string @@ -143,8 +150,11 @@ 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.CreateMissingNamespaces == nil { - options.CreateMissingNamespaces = ref(true) + if options.FieldOwner == nil { + options.FieldOwner = &name + } + if options.Finalizer == nil { + options.Finalizer = &name } if options.AdoptionPolicy == nil { options.AdoptionPolicy = ref(AdoptionPolicyIfUnowned) @@ -155,20 +165,24 @@ 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) } return &Reconciler{ - name: name, + fieldOwner: *options.FieldOwner, + finalizer: *options.Finalizer, 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, @@ -532,13 +546,13 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj // - PhaseDeleting // create missing namespaces - if r.createMissingNamespaces { + 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) { 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 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/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/_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/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 420790c..ad667de 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: @@ -36,9 +36,15 @@ 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 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. // If unspecified, AdoptionPolicyIfUnowned is assumed. // Can be overridden by annotation on object level. @@ -51,6 +57,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 @@ -122,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 @@ -137,7 +144,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} } ``` @@ -151,8 +158,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 } ``` @@ -169,8 +176,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 } ``` @@ -189,9 +196,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/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. 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 ``` 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