Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ type boxcutterReconcilerConfigurator struct {
mgr manager.Manager
preflights []applier.Preflight
regv1ManifestProvider applier.ManifestProvider
resolver resolve.Resolver
bundleSources controllers.Resolvers
imageCache imageutil.Cache
imagePuller imageutil.Puller
finalizers crfinalizer.Finalizers
Expand All @@ -119,7 +119,7 @@ type helmReconcilerConfigurator struct {
mgr manager.Manager
preflights []applier.Preflight
regv1ManifestProvider applier.ManifestProvider
resolver resolve.Resolver
bundleSources controllers.Resolvers
imageCache imageutil.Cache
imagePuller imageutil.Puller
finalizers crfinalizer.Finalizers
Expand Down Expand Up @@ -450,8 +450,12 @@ func run() error {
}
}

bundleSources := controllers.Resolvers{
ocv1.SourceTypeCatalog: &controllers.CatalogSource{Resolver: resolver, Client: cl},
}
ceReconciler := &controllers.ClusterExtensionReconciler{
Client: cl,
Client: cl,
Resolvers: bundleSources,
}
if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) {
_, err = ceReconciler.SetupWithManagerForBoxcutter(mgr)
Expand All @@ -478,7 +482,7 @@ func run() error {
mgr: mgr,
preflights: preflights,
regv1ManifestProvider: regv1ManifestProvider,
resolver: resolver,
bundleSources: bundleSources,
imageCache: imageCache,
imagePuller: imagePuller,
finalizers: clusterExtensionFinalizers,
Expand All @@ -488,7 +492,7 @@ func run() error {
mgr: mgr,
preflights: preflights,
regv1ManifestProvider: regv1ManifestProvider,
resolver: resolver,
bundleSources: bundleSources,
imageCache: imageCache,
imagePuller: imagePuller,
finalizers: clusterExtensionFinalizers,
Expand Down Expand Up @@ -681,14 +685,15 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
RevisionGenerator: rg,
FieldOwner: fieldOwner,
}
ceReconciler.Resolvers = c.bundleSources
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
controllers.HandleFinalizers(c.finalizers),
controllers.ValidateClusterExtension(
controllers.ServiceAccountDeprecationWarning(),
),
controllers.MigrateStorage(storageMigrator),
controllers.RetrieveRevisionStates(revisionStatesGetter),
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
controllers.ResolveBundle(c.bundleSources),
controllers.UnpackBundle(c.imagePuller, c.imageCache),
controllers.ValidateInstallNamespace(coreClient),
controllers.ApplyBundleWithBoxcutter(appl.Apply),
Expand Down Expand Up @@ -739,13 +744,14 @@ func (c *helmReconcilerConfigurator) Configure(ceReconciler *controllers.Cluster
TrackingCache: c.trackingCache,
}
revisionStatesGetter := &controllers.HelmRevisionStatesGetter{ActionClientGetter: acg}
ceReconciler.Resolvers = c.bundleSources
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
controllers.HandleFinalizers(c.finalizers),
controllers.ValidateClusterExtension(
controllers.ServiceAccountDeprecationWarning(),
),
controllers.RetrieveRevisionStates(revisionStatesGetter),
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
controllers.ResolveBundle(c.bundleSources),
controllers.UnpackBundle(c.imagePuller, c.imageCache),
controllers.ApplyBundle(appl),
}
Expand Down
95 changes: 95 additions & 0 deletions internal/operator-controller/controllers/bundle_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
Copyright 2026.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"context"
"fmt"
"reflect"

ocv1 "github.com/operator-framework/operator-controller/api/v1"
)

// Resolver is the mandatory behavior of an object that can resolve a bundle
// from a source. The shared reconciliation steps own engagement policy:
// ValidateSource runs before rollout continuation and resolution, Resolve is
// called only at steady state with no rollout in progress,
// RecoverFromResolutionError is invoked only when Resolve fails, and
// CanMaintainWithoutContent is consulted only when bundle content cannot be
// pulled. Implementations must not hide preconditions or fallbacks inside
// Resolve.
//
// Deprecation evidence is deliberately not part of this contract: sources
// that can provide it implement the optional deprecation interfaces in the
// resolve package (see resolve.PackageDeprecationSource), and consumers use
// type assertions against those interfaces to collect evidence on every
// reconcile. A source without deprecation data is reported exactly like a
// source whose evaluation found no deprecations.
type Resolver interface {
// ValidateSource verifies the extension's source payload is complete
// enough to reconcile. It runs before rollout continuation and
// resolution, so malformed payloads fail closed without backend calls.
ValidateSource(ext *ocv1.ClusterExtension) error

// Resolve selects the bundle to install or upgrade to. It is called only
// at steady state, with no rollout in progress.
Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*RevisionMetadata, error)

// RecoverFromResolutionError is invoked only when Resolve returned an
// error. Returning a nil error with a non-nil result authorizes
// maintaining the installed bundle (see maintainInstalled); returning an
// error classifies the failure for retry.
RecoverFromResolutionError(ctx context.Context, ext *ocv1.ClusterExtension, revisions *RevisionStates, resolveErr error) (*RevisionMetadata, error)

// CanMaintainWithoutContent authorizes maintaining the current
// installation when bundle content cannot be pulled.
CanMaintainWithoutContent(ext *ocv1.ClusterExtension, revisions *RevisionStates, resolved *RevisionMetadata) bool

// ReconcileOnCatalogChange reports whether a ClusterCatalog event should
// enqueue reconciles for extensions served by this source.
ReconcileOnCatalogChange(ext *ocv1.ClusterExtension, catalog *ocv1.ClusterCatalog) bool
}

// Resolvers selects a source policy using spec.source.sourceType.
type Resolvers map[string]Resolver

// For returns the configured policy or an error before any source is invoked.
func (sources Resolvers) For(ext *ocv1.ClusterExtension) (Resolver, error) {
source := sources[ext.Spec.Source.SourceType]
if source == nil || nilResolver(source) {
return nil, fmt.Errorf("unsupported sourceType %q: bundle source is not configured", ext.Spec.Source.SourceType)
}
return source, nil
}

// maintainInstalled is the general installed-bundle fallback action: it
// targets the installed revision so the shared unpack and apply pipeline
// maintains the existing installation.
func maintainInstalled(revisions *RevisionStates) *RevisionMetadata {
return revisions.Installed
}

// Interfaces can contain typed nil implementations; reject these at lookup too.
func nilResolver(source Resolver) bool {
v := reflect.ValueOf(source)
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
default:
return false
}
}
Loading
Loading