Skip to content

Commit b988ace

Browse files
fix(Boxcutter): Re-resolve bundle when rollout is stuck
Implements catalog spec digest tracking to detect spec changes during stuck rollouts, enabling recovery by updating the ClusterExtension spec. **Problem**: When a ClusterExtension rollout gets stuck (e.g., bad image causing probe failures), updating the spec to a different version doesn't trigger re-resolution. The controller reuses the stuck revision indefinitely. **Solution**: Track a digest of resolution-relevant spec fields (packageName, version, channels, selector, upgradeConstraintPolicy) in ClusterObjectSet annotations. When spec changes during rollout, detect digest mismatch and trigger re-resolution to create a new revision. Generated-by: Claude
1 parent afa2e7a commit b988ace

10 files changed

Lines changed: 1475 additions & 76 deletions

internal/operator-controller/controllers/boxcutter_reconcile_steps.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,11 @@ func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, e
6262
// is fairly decoupled from this code where we get the annotations back out. We may want to co-locate
6363
// the set/get logic a bit better to make it more maintainable and less likely to get out of sync.
6464
rm := &RevisionMetadata{
65-
RevisionName: rev.Name,
66-
Package: rev.Annotations[labels.PackageNameKey],
67-
Image: rev.Annotations[labels.BundleReferenceKey],
68-
Conditions: rev.Status.Conditions,
65+
RevisionName: rev.Name,
66+
Package: rev.Annotations[labels.PackageNameKey],
67+
Image: rev.Annotations[labels.BundleReferenceKey],
68+
CatalogSpecDigest: rev.Annotations[labels.CatalogSpecDigestKey],
69+
Conditions: rev.Status.Conditions,
6970
BundleMetadata: ocv1.BundleMetadata{
7071
Name: rev.Annotations[labels.BundleNameKey],
7172
Version: rev.Annotations[labels.BundleVersionKey],
@@ -104,10 +105,11 @@ func ApplyBundleWithBoxcutter(apply func(ctx context.Context, contentFS fs.FS, e
104105
return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) {
105106
l := log.FromContext(ctx)
106107
revisionAnnotations := map[string]string{
107-
labels.BundleNameKey: state.resolvedRevisionMetadata.Name,
108-
labels.PackageNameKey: state.resolvedRevisionMetadata.Package,
109-
labels.BundleVersionKey: state.resolvedRevisionMetadata.Version,
110-
labels.BundleReferenceKey: state.resolvedRevisionMetadata.Image,
108+
labels.BundleNameKey: state.resolvedRevisionMetadata.Name,
109+
labels.PackageNameKey: state.resolvedRevisionMetadata.Package,
110+
labels.BundleVersionKey: state.resolvedRevisionMetadata.Version,
111+
labels.BundleReferenceKey: state.resolvedRevisionMetadata.Image,
112+
labels.CatalogSpecDigestKey: CatalogSpecDigest(ext),
111113
}
112114
if state.resolvedRevisionMetadata.Release != nil {
113115
revisionAnnotations[labels.BundleReleaseKey] = *state.resolvedRevisionMetadata.Release
@@ -154,11 +156,17 @@ func ApplyBundleWithBoxcutter(apply func(ctx context.Context, contentFS fs.FS, e
154156
apimeta.SetStatusCondition(&rs.Conditions, *cnd)
155157
}
156158
}
157-
// Mirror Progressing condition from the latest active revision
159+
// Mirror Progressing from the latest rolling revision only while the installed
160+
// revision does not already satisfy the current spec. Stale rolling revisions
161+
// (e.g. a failed upgrade left behind after recovery) must not overwrite Succeeded.
158162
if idx == len(state.revisionStates.RollingOut)-1 {
159-
if pcnd := apimeta.FindStatusCondition(r.Conditions, ocv1.ClusterObjectSetTypeProgressing); pcnd != nil {
160-
pcnd.ObservedGeneration = ext.GetGeneration()
161-
apimeta.SetStatusCondition(&ext.Status.Conditions, *pcnd)
163+
installedSatisfiesSpec := state.revisionStates.Installed != nil &&
164+
versionMatchesSpec(state.revisionStates.Installed.Version, ext)
165+
if !installedSatisfiesSpec {
166+
if pcnd := apimeta.FindStatusCondition(r.Conditions, ocv1.ClusterObjectSetTypeProgressing); pcnd != nil {
167+
pcnd.ObservedGeneration = ext.GetGeneration()
168+
apimeta.SetStatusCondition(&ext.Status.Conditions, *pcnd)
169+
}
162170
}
163171
}
164172
ext.Status.ActiveRevisions = append(ext.Status.ActiveRevisions, rs)

internal/operator-controller/controllers/boxcutter_reconcile_steps_apply_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"testing/fstest"
2424

2525
"github.com/stretchr/testify/require"
26+
apimeta "k8s.io/apimachinery/pkg/api/meta"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2728

2829
ocv1 "github.com/operator-framework/operator-controller/api/v1"
@@ -148,3 +149,72 @@ func TestApplyBundleWithBoxcutter(t *testing.T) {
148149
})
149150
}
150151
}
152+
153+
func TestApplyBundleWithBoxcutterInstalledSatisfiesSpecIgnoresStaleRollingProgressing(t *testing.T) {
154+
ctx := context.Background()
155+
ext := &ocv1.ClusterExtension{
156+
ObjectMeta: metav1.ObjectMeta{
157+
Name: "test-ext",
158+
Generation: 3,
159+
},
160+
Spec: ocv1.ClusterExtensionSpec{
161+
Source: ocv1.SourceConfig{
162+
Catalog: &ocv1.CatalogFilter{
163+
PackageName: "test",
164+
Version: "1.2.0",
165+
},
166+
},
167+
},
168+
}
169+
170+
state := &reconcileState{
171+
revisionStates: &RevisionStates{
172+
Installed: &RevisionMetadata{
173+
RevisionName: "test-ext-3",
174+
BundleMetadata: ocv1.BundleMetadata{
175+
Name: "test.v1.2.0",
176+
Version: "1.2.0",
177+
},
178+
Conditions: []metav1.Condition{
179+
{
180+
Type: ocv1.ClusterObjectSetTypeProgressing,
181+
Status: metav1.ConditionTrue,
182+
Reason: ocv1.ReasonSucceeded,
183+
},
184+
},
185+
},
186+
RollingOut: []*RevisionMetadata{{
187+
RevisionName: "test-ext-2",
188+
BundleMetadata: ocv1.BundleMetadata{
189+
Name: "test.v1.0.2",
190+
Version: "1.0.2",
191+
},
192+
Conditions: []metav1.Condition{
193+
{
194+
Type: ocv1.ClusterObjectSetTypeProgressing,
195+
Status: metav1.ConditionTrue,
196+
Reason: ocv1.ReasonRollingOut,
197+
},
198+
},
199+
}},
200+
},
201+
resolvedRevisionMetadata: &RevisionMetadata{
202+
BundleMetadata: ocv1.BundleMetadata{
203+
Name: "test.v1.2.0",
204+
Version: "1.2.0",
205+
},
206+
},
207+
imageFS: fstest.MapFS{},
208+
}
209+
210+
stepFunc := ApplyBundleWithBoxcutter(func(_ context.Context, _ fs.FS, _ *ocv1.ClusterExtension, _, _ map[string]string) (bool, string, error) {
211+
return true, "", nil
212+
})
213+
_, err := stepFunc(ctx, state, ext)
214+
require.NoError(t, err)
215+
216+
pcnd := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing)
217+
require.NotNil(t, pcnd)
218+
require.Equal(t, ocv1.ReasonSucceeded, pcnd.Reason,
219+
"stale rolling revision must not overwrite Progressing when installed matches spec")
220+
}

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -499,9 +499,10 @@ func clusterExtensionRequestsForCatalog(c client.Reader, logger logr.Logger) crh
499499
}
500500

501501
type RevisionMetadata struct {
502-
RevisionName string
503-
Package string
504-
Image string
502+
RevisionName string
503+
Package string
504+
Image string
505+
CatalogSpecDigest string
505506
ocv1.BundleMetadata
506507
Conditions []metav1.Condition
507508
}

0 commit comments

Comments
 (0)