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" >}}