Skip to content

Commit cb1e03d

Browse files
fix: stale deprecation conditions after ClusterExtension upgrade
When upgrading from a deprecated bundle to a non-deprecated one, deprecation conditions (Deprecated, BundleDeprecated) remained True with stale messages until a second reconciliation cycle. Fix by refreshing deprecation status after a successful apply using the newly installed bundle name. Also adds a Valid condition type to ClusterExtension status, set by the shared ValidateClusterExtension step for both Helm and Boxcutter applier pipelines. Generated-by: Cursor/Claude
1 parent c16a97b commit cb1e03d

7 files changed

Lines changed: 163 additions & 3 deletions

File tree

api/v1/clusterextension_types.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,11 @@ type RevisionStatus struct {
487487
type ClusterExtensionStatus struct {
488488
// conditions represents the current state of the ClusterExtension.
489489
//
490-
// The set of condition types which apply to all spec.source variations are Installed and Progressing.
490+
// The set of condition types which apply to all spec.source variations are Installed, Progressing, and Valid.
491+
//
492+
// The Valid condition represents whether the ClusterExtension's configuration passes all validation checks:
493+
// - When Valid is True and the Reason is Succeeded, the ClusterExtension configuration is valid (e.g., referenced ServiceAccount exists).
494+
// - When Valid is False and the Reason is Failed, the ClusterExtension configuration has validation errors.
491495
//
492496
// The Installed condition represents whether the bundle has been installed for this ClusterExtension:
493497
// - When Installed is True and the Reason is Succeeded, the bundle has been successfully installed.

api/v1/common_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package v1
1919
const (
2020
TypeInstalled = "Installed"
2121
TypeProgressing = "Progressing"
22+
TypeValid = "Valid"
2223

2324
// Installed reasons
2425
ReasonAbsent = "Absent"

internal/operator-controller/conditionsets/conditionsets.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var ConditionTypes = []string{
3131
ocv1.TypeChannelDeprecated,
3232
ocv1.TypeBundleDeprecated,
3333
ocv1.TypeProgressing,
34+
ocv1.TypeValid,
3435
}
3536

3637
var ConditionReasons = []string{

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 9 additions & 1 deletion
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.
@@ -175,7 +177,7 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req
175177
//nolint:unparam // reason parameter is designed to be flexible, even if current callers use the same value
176178
func ensureFailureConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.ConditionReason, message string) {
177179
for _, condType := range conditionsets.ConditionTypes {
178-
if isDeprecationCondition(condType) {
180+
if isDeprecationCondition(condType) || isValidationCondition(condType) {
179181
continue
180182
}
181183
cond := apimeta.FindStatusCondition(ext.Status.Conditions, condType)
@@ -353,6 +355,12 @@ func isDeprecationCondition(condType string) bool {
353355
}
354356
}
355357

358+
// isValidationCondition reports whether the given type is a validation
359+
// condition managed by the ValidateClusterExtension step.
360+
func isValidationCondition(condType string) bool {
361+
return condType == ocv1.TypeValid
362+
}
363+
356364
// deprecationInfo captures the deprecation data needed to update condition status.
357365
type deprecationInfo struct {
358366
PackageEntries []declcfg.DeprecationEntry

internal/operator-controller/controllers/clusterextension_controller_test.go

Lines changed: 114 additions & 1 deletion
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:
@@ -920,6 +1022,13 @@ func TestValidateClusterExtension(t *testing.T) {
9201022
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
9211023

9221024
t.Log("By checking the status conditions")
1025+
validCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeValid)
1026+
require.NotNil(t, validCond)
1027+
require.Equal(t, metav1.ConditionFalse, validCond.Status)
1028+
require.Equal(t, ocv1.ReasonFailed, validCond.Reason)
1029+
require.Contains(t, validCond.Message, "operation cannot proceed due to the following validation error(s)")
1030+
require.Contains(t, validCond.Message, tt.errorMessageIncludes)
1031+
9231032
installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
9241033
require.NotNil(t, installedCond)
9251034
require.Equal(t, metav1.ConditionUnknown, installedCond.Status)
@@ -935,7 +1044,11 @@ func TestValidateClusterExtension(t *testing.T) {
9351044
} else {
9361045
require.NoError(t, err)
9371046
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
938-
require.Empty(t, clusterExtension.Status.Conditions)
1047+
1048+
validCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeValid)
1049+
require.NotNil(t, validCond)
1050+
require.Equal(t, metav1.ConditionTrue, validCond.Status)
1051+
require.Equal(t, ocv1.ReasonSucceeded, validCond.Reason)
9391052
}
9401053
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{}))
9411054
})

internal/operator-controller/controllers/clusterextension_reconcile_steps.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ func ValidateClusterExtension(validators ...ClusterExtensionValidator) Reconcile
8787

8888
// If there are no validation errors, continue reconciliation
8989
if len(validationErrors) == 0 {
90+
setValidStatusConditionTrue(ext, "ClusterExtension passes all validations")
9091
return nil, nil
9192
}
9293

9394
// Set status conditions with the validation errors
9495
err := fmt.Errorf("operation cannot proceed due to the following validation error(s): %w", errors.Join(validationErrors...))
96+
setValidStatusConditionFalse(ext, err.Error())
9597
setInstalledStatusConditionUnknown(ext, err.Error())
9698
setStatusProgressing(ext, err)
9799
return nil, err
@@ -180,6 +182,8 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc {
180182
// the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from
181183
// all catalogs? This needs a follow-up discussion and PR.
182184
hasCatalogData := err == nil || resolvedDeprecation != nil
185+
state.resolvedDeprecation = resolvedDeprecation
186+
state.hasCatalogData = hasCatalogData
183187
SetDeprecationStatus(ext, installedBundleName, resolvedDeprecation, hasCatalogData)
184188

185189
if err != nil {
@@ -437,6 +441,15 @@ func ApplyBundle(a Applier) ReconcileStepFunc {
437441
}
438442
setInstalledStatusFromRevisionStates(ext, state.revisionStates)
439443

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

internal/operator-controller/controllers/common_controller.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,26 @@ func setInstallStatus(ext *ocv1.ClusterExtension, installStatus *ocv1.ClusterExt
145145
ext.Status.Install = installStatus
146146
}
147147

148+
func setValidStatusConditionTrue(ext *ocv1.ClusterExtension, message string) {
149+
SetStatusCondition(&ext.Status.Conditions, metav1.Condition{
150+
Type: ocv1.TypeValid,
151+
Status: metav1.ConditionTrue,
152+
Reason: ocv1.ReasonSucceeded,
153+
Message: message,
154+
ObservedGeneration: ext.GetGeneration(),
155+
})
156+
}
157+
158+
func setValidStatusConditionFalse(ext *ocv1.ClusterExtension, message string) {
159+
SetStatusCondition(&ext.Status.Conditions, metav1.Condition{
160+
Type: ocv1.TypeValid,
161+
Status: metav1.ConditionFalse,
162+
Reason: ocv1.ReasonFailed,
163+
Message: message,
164+
ObservedGeneration: ext.GetGeneration(),
165+
})
166+
}
167+
148168
func setStatusProgressing(ext *ocv1.ClusterExtension, err error) {
149169
progressingCond := metav1.Condition{
150170
Type: ocv1.TypeProgressing,

0 commit comments

Comments
 (0)