@@ -18,6 +18,8 @@ package controllers
1818
1919import (
2020 "context"
21+ "crypto/sha256"
22+ "encoding/json"
2123 "errors"
2224 "fmt"
2325
@@ -37,6 +39,49 @@ import (
3739 imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
3840)
3941
42+ // resolutionInputs captures the catalog filter fields that affect bundle resolution.
43+ // Changes to any of these fields require re-resolution.
44+ type resolutionInputs struct {
45+ Version string `json:"version"`
46+ Channels []string `json:"channels,omitempty"`
47+ Selector string `json:"selector,omitempty"`
48+ UpgradeConstraintPolicy string `json:"upgradeConstraintPolicy,omitempty"`
49+ }
50+
51+ // calculateResolutionDigest computes a SHA256 hash of the catalog filter inputs
52+ // that affect bundle resolution. This digest enables detection of spec changes that
53+ // require re-resolution versus when a rolling-out revision can be reused.
54+ func calculateResolutionDigest (catalog * ocv1.CatalogFilter ) (string , error ) {
55+ if catalog == nil {
56+ return "" , nil
57+ }
58+
59+ inputs := resolutionInputs {
60+ Version : catalog .Version ,
61+ Channels : catalog .Channels ,
62+ UpgradeConstraintPolicy : string (catalog .UpgradeConstraintPolicy ),
63+ }
64+
65+ // Convert selector to a canonical string representation
66+ if catalog .Selector != nil {
67+ selectorBytes , err := json .Marshal (catalog .Selector )
68+ if err != nil {
69+ return "" , fmt .Errorf ("marshaling selector: %w" , err )
70+ }
71+ inputs .Selector = string (selectorBytes )
72+ }
73+
74+ // Marshal to JSON for consistent hashing
75+ inputsBytes , err := json .Marshal (inputs )
76+ if err != nil {
77+ return "" , fmt .Errorf ("marshaling resolution inputs: %w" , err )
78+ }
79+
80+ // Compute SHA256 hash and return hex string
81+ hash := sha256 .Sum256 (inputsBytes )
82+ return fmt .Sprintf ("%x" , hash ), nil
83+ }
84+
4085func HandleFinalizers (f finalizer.Finalizer ) ReconcileStepFunc {
4186 return func (ctx context.Context , state * reconcileState , ext * ocv1.ClusterExtension ) (* ctrl.Result , error ) {
4287 l := log .FromContext (ctx )
@@ -141,22 +186,22 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc {
141186 l := log .FromContext (ctx )
142187
143188 // If already rolling out, check if the latest rolling-out revision matches the current spec.
144- // If spec has changed (e.g., version updated ), we need to resolve a new bundle instead of
145- // reusing the rolling-out revision.
189+ // If spec has changed (e.g., version, channels, selector, upgradeConstraintPolicy ), we need
190+ // to resolve a new bundle instead of reusing the rolling-out revision.
146191 //
147192 // Note: RollingOut slice is sorted oldest→newest, so use the last element (most recent).
148193 // This avoids re-resolving when a newer revision already matches the spec, even if an
149194 // older revision is still stuck rolling out.
150195 if len (state .revisionStates .RollingOut ) > 0 {
151196 latestRollingOutRevision := state .revisionStates .RollingOut [len (state .revisionStates .RollingOut )- 1 ]
152- specVersion := ""
153- if ext .Spec .Source .Catalog != nil {
154- specVersion = ext .Spec .Source .Catalog .Version
155- }
156197
157- // If spec version matches latest rolling-out revision version (or no version specified),
158- // reuse the existing rolling-out revision
159- if specVersion == "" || specVersion == latestRollingOutRevision .Version {
198+ // Calculate digest of current spec's catalog filter
199+ currentDigest , err := calculateResolutionDigest (ext .Spec .Source .Catalog )
200+ if err != nil {
201+ l .Error (err , "failed to calculate resolution digest from spec" )
202+ // On digest calculation error, fall through to re-resolve for safety
203+ } else if currentDigest == latestRollingOutRevision .ResolutionDigest {
204+ // Resolution inputs haven't changed - reuse the rolling-out revision
160205 installedBundleName := ""
161206 if state .revisionStates .Installed != nil {
162207 installedBundleName = state .revisionStates .Installed .Name
@@ -165,7 +210,7 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc {
165210 state .resolvedRevisionMetadata = latestRollingOutRevision
166211 return nil , nil
167212 }
168- // Spec version changed - fall through to resolve new bundle from catalog
213+ // Resolution inputs changed - fall through to resolve new bundle from catalog
169214 }
170215
171216 // Resolve a new bundle from the catalog
@@ -205,9 +250,17 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc {
205250 return handleResolutionError (ctx , c , state , ext , err )
206251 }
207252
253+ // Calculate digest of the resolution inputs for change detection
254+ digest , err := calculateResolutionDigest (ext .Spec .Source .Catalog )
255+ if err != nil {
256+ l .Error (err , "failed to calculate resolution digest, continuing without it" )
257+ digest = ""
258+ }
259+
208260 state .resolvedRevisionMetadata = & RevisionMetadata {
209- Package : resolvedBundle .Package ,
210- Image : resolvedBundle .Image ,
261+ Package : resolvedBundle .Package ,
262+ Image : resolvedBundle .Image ,
263+ ResolutionDigest : digest ,
211264 // TODO: Right now, operator-controller only supports registry+v1 bundles and has no concept
212265 // of a "release" field. If/when we add a release field concept or a new bundle format
213266 // we need to re-evaluate use of `AsLegacyRegistryV1Version` so that we avoid propagating
@@ -426,10 +479,11 @@ func ApplyBundle(a Applier) ReconcileStepFunc {
426479 return func (ctx context.Context , state * reconcileState , ext * ocv1.ClusterExtension ) (* ctrl.Result , error ) {
427480 l := log .FromContext (ctx )
428481 revisionAnnotations := map [string ]string {
429- labels .BundleNameKey : state .resolvedRevisionMetadata .Name ,
430- labels .PackageNameKey : state .resolvedRevisionMetadata .Package ,
431- labels .BundleVersionKey : state .resolvedRevisionMetadata .Version ,
432- labels .BundleReferenceKey : state .resolvedRevisionMetadata .Image ,
482+ labels .BundleNameKey : state .resolvedRevisionMetadata .Name ,
483+ labels .PackageNameKey : state .resolvedRevisionMetadata .Package ,
484+ labels .BundleVersionKey : state .resolvedRevisionMetadata .Version ,
485+ labels .BundleReferenceKey : state .resolvedRevisionMetadata .Image ,
486+ labels .ResolutionDigestKey : state .resolvedRevisionMetadata .ResolutionDigest ,
433487 }
434488 objLbls := map [string ]string {
435489 labels .OwnerKindKey : ocv1 .ClusterExtensionKind ,
0 commit comments