@@ -1611,3 +1611,189 @@ func TestGetInstalledBundleHistory(t *testing.T) {
16111611 }
16121612 }
16131613}
1614+
1615+ // TestResolutionFallbackToInstalledBundle tests the catalog deletion resilience fallback logic
1616+ func TestResolutionFallbackToInstalledBundle (t * testing.T ) {
1617+ t .Run ("falls back when catalog unavailable and no version change" , func (t * testing.T ) {
1618+ cl , reconciler := newClientAndReconciler (t , func (d * deps ) {
1619+ // Resolver fails (simulating catalog unavailable)
1620+ d .Resolver = resolve .Func (func (_ context.Context , _ * ocv1.ClusterExtension , _ * ocv1.BundleMetadata ) (* declcfg.Bundle , * bundle.VersionRelease , * declcfg.Deprecation , error ) {
1621+ return nil , nil , nil , fmt .Errorf ("catalog unavailable" )
1622+ })
1623+ // Applier succeeds (resources maintained)
1624+ d .Applier = & MockApplier {
1625+ installCompleted : true ,
1626+ installStatus : "" ,
1627+ err : nil ,
1628+ }
1629+ d .RevisionStatesGetter = & MockRevisionStatesGetter {
1630+ RevisionStates : & controllers.RevisionStates {
1631+ Installed : & controllers.RevisionMetadata {
1632+ Package : "test-pkg" ,
1633+ BundleMetadata : ocv1.BundleMetadata {Name : "test.1.0.0" , Version : "1.0.0" },
1634+ Image : "test-image:1.0.0" ,
1635+ },
1636+ },
1637+ }
1638+ })
1639+
1640+ ctx := context .Background ()
1641+ extKey := types.NamespacedName {Name : fmt .Sprintf ("test-%s" , rand .String (8 ))}
1642+
1643+ // Create ClusterExtension with no version specified
1644+ ext := & ocv1.ClusterExtension {
1645+ ObjectMeta : metav1.ObjectMeta {Name : extKey .Name },
1646+ Spec : ocv1.ClusterExtensionSpec {
1647+ Source : ocv1.SourceConfig {
1648+ SourceType : "Catalog" ,
1649+ Catalog : & ocv1.CatalogFilter {
1650+ PackageName : "test-pkg" ,
1651+ // No version - should fall back
1652+ },
1653+ },
1654+ Namespace : "default" ,
1655+ ServiceAccount : ocv1.ServiceAccountReference {Name : "default" },
1656+ },
1657+ }
1658+ require .NoError (t , cl .Create (ctx , ext ))
1659+
1660+ // Reconcile should succeed (fallback to installed, then apply succeeds)
1661+ // Catalog watch will trigger reconciliation when catalog becomes available again
1662+ res , err := reconciler .Reconcile (ctx , ctrl.Request {NamespacedName : extKey })
1663+ require .NoError (t , err )
1664+ require .Equal (t , ctrl.Result {}, res )
1665+
1666+ // Verify status shows successful reconciliation
1667+ require .NoError (t , cl .Get (ctx , extKey , ext ))
1668+
1669+ // Progressing should be Succeeded (apply completed successfully)
1670+ progCond := apimeta .FindStatusCondition (ext .Status .Conditions , ocv1 .TypeProgressing )
1671+ require .NotNil (t , progCond )
1672+ require .Equal (t , metav1 .ConditionTrue , progCond .Status )
1673+ require .Equal (t , ocv1 .ReasonSucceeded , progCond .Reason )
1674+
1675+ // Installed should be True (maintaining current version)
1676+ instCond := apimeta .FindStatusCondition (ext .Status .Conditions , ocv1 .TypeInstalled )
1677+ require .NotNil (t , instCond )
1678+ require .Equal (t , metav1 .ConditionTrue , instCond .Status )
1679+ require .Equal (t , ocv1 .ReasonSucceeded , instCond .Reason )
1680+ })
1681+
1682+ t .Run ("fails when version upgrade requested without catalog" , func (t * testing.T ) {
1683+ cl , reconciler := newClientAndReconciler (t , func (d * deps ) {
1684+ d .Resolver = resolve .Func (func (_ context.Context , _ * ocv1.ClusterExtension , _ * ocv1.BundleMetadata ) (* declcfg.Bundle , * bundle.VersionRelease , * declcfg.Deprecation , error ) {
1685+ return nil , nil , nil , fmt .Errorf ("catalog unavailable" )
1686+ })
1687+ d .RevisionStatesGetter = & MockRevisionStatesGetter {
1688+ RevisionStates : & controllers.RevisionStates {
1689+ Installed : & controllers.RevisionMetadata {
1690+ BundleMetadata : ocv1.BundleMetadata {Name : "test.1.0.0" , Version : "1.0.0" },
1691+ },
1692+ },
1693+ }
1694+ })
1695+
1696+ ctx := context .Background ()
1697+ extKey := types.NamespacedName {Name : fmt .Sprintf ("test-%s" , rand .String (8 ))}
1698+
1699+ // Create ClusterExtension requesting version upgrade
1700+ ext := & ocv1.ClusterExtension {
1701+ ObjectMeta : metav1.ObjectMeta {Name : extKey .Name },
1702+ Spec : ocv1.ClusterExtensionSpec {
1703+ Source : ocv1.SourceConfig {
1704+ SourceType : "Catalog" ,
1705+ Catalog : & ocv1.CatalogFilter {
1706+ PackageName : "test-pkg" ,
1707+ Version : "1.0.1" , // Requesting upgrade
1708+ },
1709+ },
1710+ Namespace : "default" ,
1711+ ServiceAccount : ocv1.ServiceAccountReference {Name : "default" },
1712+ },
1713+ }
1714+ require .NoError (t , cl .Create (ctx , ext ))
1715+
1716+ // Reconcile should fail (can't upgrade without catalog)
1717+ res , err := reconciler .Reconcile (ctx , ctrl.Request {NamespacedName : extKey })
1718+ require .Error (t , err )
1719+ require .Equal (t , ctrl.Result {}, res )
1720+
1721+ // Verify status shows Retrying
1722+ require .NoError (t , cl .Get (ctx , extKey , ext ))
1723+ cond := apimeta .FindStatusCondition (ext .Status .Conditions , ocv1 .TypeProgressing )
1724+ require .NotNil (t , cond )
1725+ require .Equal (t , metav1 .ConditionTrue , cond .Status )
1726+ require .Equal (t , ocv1 .ReasonRetrying , cond .Reason )
1727+ })
1728+
1729+ t .Run ("auto-updates when catalog becomes available after fallback" , func (t * testing.T ) {
1730+ resolveAttempt := 0
1731+ cl , reconciler := newClientAndReconciler (t , func (d * deps ) {
1732+ // First attempt: catalog unavailable, then becomes available
1733+ d .Resolver = resolve .Func (func (_ context.Context , _ * ocv1.ClusterExtension , _ * ocv1.BundleMetadata ) (* declcfg.Bundle , * bundle.VersionRelease , * declcfg.Deprecation , error ) {
1734+ resolveAttempt ++
1735+ if resolveAttempt == 1 {
1736+ // First reconcile: catalog unavailable
1737+ return nil , nil , nil , fmt .Errorf ("catalog temporarily unavailable" )
1738+ }
1739+ // Second reconcile (triggered by catalog watch): catalog available with new version
1740+ v := bundle.VersionRelease {Version : bsemver .MustParse ("2.0.0" )}
1741+ return & declcfg.Bundle {
1742+ Name : "test.2.0.0" ,
1743+ Package : "test-pkg" ,
1744+ Image : "test-image:2.0.0" ,
1745+ }, & v , nil , nil
1746+ })
1747+ d .RevisionStatesGetter = & MockRevisionStatesGetter {
1748+ RevisionStates : & controllers.RevisionStates {
1749+ Installed : & controllers.RevisionMetadata {
1750+ Package : "test-pkg" ,
1751+ BundleMetadata : ocv1.BundleMetadata {Name : "test.1.0.0" , Version : "1.0.0" },
1752+ Image : "test-image:1.0.0" ,
1753+ },
1754+ },
1755+ }
1756+ d .ImagePuller = & imageutil.MockPuller {ImageFS : fstest.MapFS {}}
1757+ d .Applier = & MockApplier {installCompleted : true }
1758+ })
1759+
1760+ ctx := context .Background ()
1761+ extKey := types.NamespacedName {Name : fmt .Sprintf ("test-%s" , rand .String (8 ))}
1762+
1763+ ext := & ocv1.ClusterExtension {
1764+ ObjectMeta : metav1.ObjectMeta {Name : extKey .Name },
1765+ Spec : ocv1.ClusterExtensionSpec {
1766+ Source : ocv1.SourceConfig {
1767+ SourceType : "Catalog" ,
1768+ Catalog : & ocv1.CatalogFilter {
1769+ PackageName : "test-pkg" ,
1770+ // No version - auto-update to latest
1771+ },
1772+ },
1773+ Namespace : "default" ,
1774+ ServiceAccount : ocv1.ServiceAccountReference {Name : "default" },
1775+ },
1776+ }
1777+ require .NoError (t , cl .Create (ctx , ext ))
1778+
1779+ // First reconcile: catalog unavailable, falls back to v1.0.0
1780+ res , err := reconciler .Reconcile (ctx , ctrl.Request {NamespacedName : extKey })
1781+ require .NoError (t , err )
1782+ require .Equal (t , ctrl.Result {}, res )
1783+
1784+ require .NoError (t , cl .Get (ctx , extKey , ext ))
1785+ require .Equal (t , "1.0.0" , ext .Status .Install .Bundle .Version )
1786+
1787+ // Second reconcile: simulating catalog watch trigger, catalog now available with v2.0.0
1788+ res , err = reconciler .Reconcile (ctx , ctrl.Request {NamespacedName : extKey })
1789+ require .NoError (t , err )
1790+ require .Equal (t , ctrl.Result {}, res )
1791+
1792+ // Should have upgraded to v2.0.0
1793+ require .NoError (t , cl .Get (ctx , extKey , ext ))
1794+ require .Equal (t , "2.0.0" , ext .Status .Install .Bundle .Version )
1795+
1796+ // Verify resolution was attempted twice (fallback, then success)
1797+ require .Equal (t , 2 , resolveAttempt )
1798+ })
1799+ }
0 commit comments