Skip to content

Commit 5b0ea47

Browse files
fix: stale deprecation conditions after ClusterExtension upgrade
Refresh deprecation status after a successful apply so that upgrading from a deprecated bundle to a non-deprecated one clears Deprecated and BundleDeprecated conditions in the same reconciliation cycle. Generated-by: Cursor/Claude
1 parent c16a97b commit 5b0ea47

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ type reconcileState struct {
6363
revisionStates *RevisionStates
6464
resolvedRevisionMetadata *RevisionMetadata
6565
imageFS fs.FS
66+
resolvedDeprecation *declcfg.Deprecation
67+
hasCatalogData bool
6668
}
6769

6870
// ReconcileStepFunc represents a single step in the ClusterExtension reconciliation process.

internal/operator-controller/controllers/clusterextension_controller_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,108 @@ func TestClusterExtensionUpgradeShowsInstalledBundleDeprecation(t *testing.T) {
344344
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{}))
345345
}
346346

347+
// TestClusterExtensionUpgradeFromDeprecatedBundleClearsDeprecation verifies that after
348+
// a successful upgrade from a deprecated bundle to a non-deprecated bundle, the deprecation
349+
// conditions are updated in the SAME reconciliation cycle (no stale conditions).
350+
//
351+
// Scenario:
352+
// - Bundle v1.0.1 is installed and deprecated in the catalog
353+
// - Bundle v1.0.3 is resolved (not deprecated) and the applier succeeds (rolloutSucceeded=true)
354+
// - After the apply, BundleDeprecated should be False (reflecting the newly installed v1.0.3)
355+
// - Deprecated rollup should also be False
356+
//
357+
// This is the regression test for the bug where deprecation conditions remained stale
358+
// (showing the old deprecated bundle) after a successful upgrade until the next reconciliation.
359+
func TestClusterExtensionUpgradeFromDeprecatedBundleClearsDeprecation(t *testing.T) {
360+
ctx := context.Background()
361+
pkgName := fmt.Sprintf("upgrade-clear-%s", rand.String(6))
362+
installedBundleName := fmt.Sprintf("%s.v1.0.1", pkgName)
363+
resolvedBundleName := fmt.Sprintf("%s.v1.0.3", pkgName)
364+
deprecationMessage := fmt.Sprintf("%s is deprecated. Uninstall and install v1.0.3 for support.", installedBundleName)
365+
366+
cl, reconciler := newClientAndReconciler(t, func(d *deps) {
367+
d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) {
368+
v := bundle.VersionRelease{
369+
Version: bsemver.MustParse("1.0.3"),
370+
}
371+
return &declcfg.Bundle{
372+
Name: resolvedBundleName,
373+
Package: pkgName,
374+
Image: fmt.Sprintf("quay.io/example/%s@sha256:resolved103", pkgName),
375+
}, &v, &declcfg.Deprecation{
376+
Entries: []declcfg.DeprecationEntry{{
377+
Reference: declcfg.PackageScopedReference{
378+
Schema: declcfg.SchemaBundle,
379+
Name: installedBundleName,
380+
},
381+
Message: deprecationMessage,
382+
}},
383+
}, nil
384+
})
385+
d.RevisionStatesGetter = &MockRevisionStatesGetter{
386+
RevisionStates: &controllers.RevisionStates{
387+
Installed: &controllers.RevisionMetadata{
388+
Package: pkgName,
389+
BundleMetadata: ocv1.BundleMetadata{
390+
Name: installedBundleName,
391+
Version: "1.0.1",
392+
},
393+
Image: fmt.Sprintf("quay.io/example/%s@sha256:installed101", pkgName),
394+
},
395+
},
396+
}
397+
d.ImagePuller = &imageutil.MockPuller{ImageFS: fstest.MapFS{}}
398+
d.Applier = &MockApplier{installCompleted: true}
399+
})
400+
401+
extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))}
402+
clusterExtension := &ocv1.ClusterExtension{
403+
ObjectMeta: metav1.ObjectMeta{Name: extKey.Name},
404+
Spec: ocv1.ClusterExtensionSpec{
405+
Source: ocv1.SourceConfig{
406+
SourceType: "Catalog",
407+
Catalog: &ocv1.CatalogFilter{PackageName: pkgName},
408+
},
409+
Namespace: "default",
410+
ServiceAccount: ocv1.ServiceAccountReference{Name: "default"},
411+
},
412+
}
413+
require.NoError(t, cl.Create(ctx, clusterExtension))
414+
415+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
416+
require.Equal(t, ctrl.Result{}, res)
417+
require.NoError(t, err)
418+
419+
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
420+
421+
// After a successful upgrade to v1.0.3, deprecation should reflect the NEW bundle
422+
bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated)
423+
require.NotNil(t, bundleCond)
424+
require.Equal(t, metav1.ConditionFalse, bundleCond.Status, "newly installed bundle v1.0.3 is NOT deprecated")
425+
require.Equal(t, ocv1.ReasonNotDeprecated, bundleCond.Reason)
426+
427+
deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated)
428+
require.NotNil(t, deprecatedCond)
429+
require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status, "no deprecation exists after upgrade")
430+
require.Equal(t, ocv1.ReasonNotDeprecated, deprecatedCond.Reason)
431+
432+
pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated)
433+
require.NotNil(t, pkgCond)
434+
require.Equal(t, metav1.ConditionFalse, pkgCond.Status)
435+
436+
channelCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated)
437+
require.NotNil(t, channelCond)
438+
require.Equal(t, metav1.ConditionFalse, channelCond.Status)
439+
440+
// Verify the installed bundle IS v1.0.3
441+
require.NotNil(t, clusterExtension.Status.Install)
442+
require.Equal(t, resolvedBundleName, clusterExtension.Status.Install.Bundle.Name)
443+
require.Equal(t, "1.0.3", clusterExtension.Status.Install.Bundle.Version)
444+
445+
verifyInvariants(ctx, t, reconciler.Client, clusterExtension)
446+
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{}))
447+
}
448+
347449
// TestClusterExtensionResolutionFailsWithoutCatalogDeprecationData verifies deprecation status handling when catalog data is unavailable.
348450
//
349451
// Scenario:

internal/operator-controller/controllers/clusterextension_reconcile_steps.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc {
180180
// the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from
181181
// all catalogs? This needs a follow-up discussion and PR.
182182
hasCatalogData := err == nil || resolvedDeprecation != nil
183+
state.resolvedDeprecation = resolvedDeprecation
184+
state.hasCatalogData = hasCatalogData
183185
SetDeprecationStatus(ext, installedBundleName, resolvedDeprecation, hasCatalogData)
184186

185187
if err != nil {
@@ -437,6 +439,15 @@ func ApplyBundle(a Applier) ReconcileStepFunc {
437439
}
438440
setInstalledStatusFromRevisionStates(ext, state.revisionStates)
439441

442+
// After a successful rollout the installed bundle may have changed
443+
// (e.g. upgrade from a deprecated to a non-deprecated version).
444+
// Refresh deprecation conditions so they reflect the newly running
445+
// bundle instead of the pre-upgrade bundle that was used during
446+
// resolution.
447+
if rolloutSucceeded && state.revisionStates.Installed != nil {
448+
SetDeprecationStatus(ext, state.revisionStates.Installed.Name, state.resolvedDeprecation, state.hasCatalogData)
449+
}
450+
440451
// If there was an error applying the resolved bundle,
441452
// report the error via the Progressing condition.
442453
if err != nil {

0 commit comments

Comments
 (0)