@@ -1344,6 +1344,260 @@ func (m *mockTrackingCache) Free(ctx context.Context, user client.Object) error
13441344 return nil
13451345}
13461346
1347+ func Test_ClusterExtensionRevisionReconciler_Reconcile_ForeignRevisionCollision (t * testing.T ) {
1348+ testScheme := newScheme (t )
1349+
1350+ for _ , tc := range []struct {
1351+ name string
1352+ reconcilingRevisionName string
1353+ existingObjs func () []client.Object
1354+ revisionResult machinery.RevisionResult
1355+ expectCollision bool
1356+ }{
1357+ {
1358+ name : "ActionProgressed with foreign controller ownerRef is treated as collision" ,
1359+ reconcilingRevisionName : "ext-B-1" ,
1360+ existingObjs : func () []client.Object {
1361+ extA := & ocv1.ClusterExtension {
1362+ ObjectMeta : metav1.ObjectMeta {Name : "ext-A" , UID : "ext-A-uid" },
1363+ Spec : ocv1.ClusterExtensionSpec {
1364+ Namespace : "ns-a" ,
1365+ ServiceAccount : ocv1.ServiceAccountReference {Name : "sa-a" },
1366+ Source : ocv1.SourceConfig {
1367+ SourceType : ocv1 .SourceTypeCatalog ,
1368+ Catalog : & ocv1.CatalogFilter {PackageName : "pkg" },
1369+ },
1370+ },
1371+ }
1372+ extB := & ocv1.ClusterExtension {
1373+ ObjectMeta : metav1.ObjectMeta {Name : "ext-B" , UID : "ext-B-uid" },
1374+ Spec : ocv1.ClusterExtensionSpec {
1375+ Namespace : "ns-b" ,
1376+ ServiceAccount : ocv1.ServiceAccountReference {Name : "sa-b" },
1377+ Source : ocv1.SourceConfig {
1378+ SourceType : ocv1 .SourceTypeCatalog ,
1379+ Catalog : & ocv1.CatalogFilter {PackageName : "pkg" },
1380+ },
1381+ },
1382+ }
1383+ // CER from ext-A's upgrade (revision 2) - this is the "foreign" owner
1384+ cerA2 := newTestClusterExtensionRevision (t , "ext-A-2" , extA , testScheme )
1385+
1386+ // CER from ext-B (revision 1) - the one being reconciled
1387+ cerB1 := newTestClusterExtensionRevision (t , "ext-B-1" , extB , testScheme )
1388+ cerB1 .Labels [labels .OwnerNameKey ] = "ext-B"
1389+
1390+ return []client.Object {extA , extB , cerA2 , cerB1 }
1391+ },
1392+ revisionResult : mockRevisionResult {
1393+ phases : []machinery.PhaseResult {
1394+ mockPhaseResult {
1395+ name : "everything" ,
1396+ objects : []machinery.ObjectResult {
1397+ mockObjectResult {
1398+ action : machinery .ActionProgressed ,
1399+ object : func () machinery.Object {
1400+ obj := & unstructured.Unstructured {
1401+ Object : map [string ]interface {}{
1402+ "apiVersion" : "apiextensions.k8s.io/v1" ,
1403+ "kind" : "CustomResourceDefinition" ,
1404+ "metadata" : map [string ]interface {}{
1405+ "name" : "widgets.example.com" ,
1406+ "ownerReferences" : []interface {}{
1407+ map [string ]interface {}{
1408+ "apiVersion" : ocv1 .GroupVersion .String (),
1409+ "kind" : ocv1 .ClusterExtensionRevisionKind ,
1410+ "name" : "ext-A-2" ,
1411+ "uid" : "ext-A-2" ,
1412+ "controller" : true ,
1413+ "blockOwnerDeletion" : true ,
1414+ },
1415+ },
1416+ },
1417+ },
1418+ }
1419+ return obj
1420+ }(),
1421+ probes : machinerytypes.ProbeResultContainer {
1422+ boxcutter .ProgressProbeType : {
1423+ Status : machinerytypes .ProbeStatusTrue ,
1424+ },
1425+ },
1426+ },
1427+ },
1428+ },
1429+ },
1430+ },
1431+ expectCollision : true ,
1432+ },
1433+ {
1434+ name : "ActionProgressed with sibling controller ownerRef is NOT a collision" ,
1435+ reconcilingRevisionName : "ext-A-1" ,
1436+ existingObjs : func () []client.Object {
1437+ extA := & ocv1.ClusterExtension {
1438+ ObjectMeta : metav1.ObjectMeta {Name : "ext-A" , UID : "ext-A-uid" },
1439+ Spec : ocv1.ClusterExtensionSpec {
1440+ Namespace : "ns-a" ,
1441+ ServiceAccount : ocv1.ServiceAccountReference {Name : "sa-a" },
1442+ Source : ocv1.SourceConfig {
1443+ SourceType : ocv1 .SourceTypeCatalog ,
1444+ Catalog : & ocv1.CatalogFilter {PackageName : "pkg" },
1445+ },
1446+ },
1447+ }
1448+ // CER from ext-A revision 1 (being reconciled, older)
1449+ cerA1 := newTestClusterExtensionRevision (t , "ext-A-1" , extA , testScheme )
1450+ // CER from ext-A revision 2 (newer, already progressed)
1451+ cerA2 := newTestClusterExtensionRevision (t , "ext-A-2" , extA , testScheme )
1452+
1453+ return []client.Object {extA , cerA1 , cerA2 }
1454+ },
1455+ revisionResult : mockRevisionResult {
1456+ phases : []machinery.PhaseResult {
1457+ mockPhaseResult {
1458+ name : "everything" ,
1459+ objects : []machinery.ObjectResult {
1460+ mockObjectResult {
1461+ action : machinery .ActionProgressed ,
1462+ object : func () machinery.Object {
1463+ obj := & unstructured.Unstructured {
1464+ Object : map [string ]interface {}{
1465+ "apiVersion" : "apiextensions.k8s.io/v1" ,
1466+ "kind" : "CustomResourceDefinition" ,
1467+ "metadata" : map [string ]interface {}{
1468+ "name" : "widgets.example.com" ,
1469+ "ownerReferences" : []interface {}{
1470+ map [string ]interface {}{
1471+ "apiVersion" : ocv1 .GroupVersion .String (),
1472+ "kind" : ocv1 .ClusterExtensionRevisionKind ,
1473+ "name" : "ext-A-2" ,
1474+ "uid" : "ext-A-2" ,
1475+ "controller" : true ,
1476+ "blockOwnerDeletion" : true ,
1477+ },
1478+ },
1479+ },
1480+ },
1481+ }
1482+ return obj
1483+ }(),
1484+ probes : machinerytypes.ProbeResultContainer {
1485+ boxcutter .ProgressProbeType : {
1486+ Status : machinerytypes .ProbeStatusTrue ,
1487+ },
1488+ },
1489+ },
1490+ },
1491+ },
1492+ },
1493+ },
1494+ expectCollision : false ,
1495+ },
1496+ {
1497+ name : "ActionProgressed with non-CER controller ownerRef is NOT a collision" ,
1498+ reconcilingRevisionName : "ext-B-1" ,
1499+ existingObjs : func () []client.Object {
1500+ extB := & ocv1.ClusterExtension {
1501+ ObjectMeta : metav1.ObjectMeta {Name : "ext-B" , UID : "ext-B-uid" },
1502+ Spec : ocv1.ClusterExtensionSpec {
1503+ Namespace : "ns-b" ,
1504+ ServiceAccount : ocv1.ServiceAccountReference {Name : "sa-b" },
1505+ Source : ocv1.SourceConfig {
1506+ SourceType : ocv1 .SourceTypeCatalog ,
1507+ Catalog : & ocv1.CatalogFilter {PackageName : "pkg" },
1508+ },
1509+ },
1510+ }
1511+ cerB1 := newTestClusterExtensionRevision (t , "ext-B-1" , extB , testScheme )
1512+ cerB1 .Labels [labels .OwnerNameKey ] = "ext-B"
1513+
1514+ return []client.Object {extB , cerB1 }
1515+ },
1516+ revisionResult : mockRevisionResult {
1517+ phases : []machinery.PhaseResult {
1518+ mockPhaseResult {
1519+ name : "everything" ,
1520+ objects : []machinery.ObjectResult {
1521+ mockObjectResult {
1522+ action : machinery .ActionProgressed ,
1523+ object : func () machinery.Object {
1524+ obj := & unstructured.Unstructured {
1525+ Object : map [string ]interface {}{
1526+ "apiVersion" : "v1" ,
1527+ "kind" : "ConfigMap" ,
1528+ "metadata" : map [string ]interface {}{
1529+ "name" : "some-cm" ,
1530+ "namespace" : "default" ,
1531+ "ownerReferences" : []interface {}{
1532+ map [string ]interface {}{
1533+ "apiVersion" : "apps/v1" ,
1534+ "kind" : "Deployment" ,
1535+ "name" : "some-deployment" ,
1536+ "uid" : "deploy-uid" ,
1537+ "controller" : true ,
1538+ "blockOwnerDeletion" : true ,
1539+ },
1540+ },
1541+ },
1542+ },
1543+ }
1544+ return obj
1545+ }(),
1546+ probes : machinerytypes.ProbeResultContainer {
1547+ boxcutter .ProgressProbeType : {
1548+ Status : machinerytypes .ProbeStatusTrue ,
1549+ },
1550+ },
1551+ },
1552+ },
1553+ },
1554+ },
1555+ },
1556+ expectCollision : false ,
1557+ },
1558+ } {
1559+ t .Run (tc .name , func (t * testing.T ) {
1560+ testClient := fake .NewClientBuilder ().
1561+ WithScheme (testScheme ).
1562+ WithStatusSubresource (& ocv1.ClusterExtensionRevision {}).
1563+ WithObjects (tc .existingObjs ()... ).
1564+ Build ()
1565+
1566+ mockEngine := & mockRevisionEngine {
1567+ reconcile : func (ctx context.Context , rev machinerytypes.Revision , opts ... machinerytypes.RevisionReconcileOption ) (machinery.RevisionResult , error ) {
1568+ return tc .revisionResult , nil
1569+ },
1570+ }
1571+ result , err := (& controllers.ClusterExtensionRevisionReconciler {
1572+ Client : testClient ,
1573+ RevisionEngineFactory : & mockRevisionEngineFactory {engine : mockEngine },
1574+ TrackingCache : & mockTrackingCache {client : testClient },
1575+ }).Reconcile (t .Context (), ctrl.Request {
1576+ NamespacedName : types.NamespacedName {
1577+ Name : tc .reconcilingRevisionName ,
1578+ },
1579+ })
1580+
1581+ if tc .expectCollision {
1582+ require .Equal (t , ctrl.Result {RequeueAfter : 10 * time .Second }, result )
1583+ require .NoError (t , err )
1584+
1585+ rev := & ocv1.ClusterExtensionRevision {}
1586+ require .NoError (t , testClient .Get (t .Context (), client.ObjectKey {Name : tc .reconcilingRevisionName }, rev ))
1587+ cond := meta .FindStatusCondition (rev .Status .Conditions , ocv1 .TypeProgressing )
1588+ require .NotNil (t , cond )
1589+ require .Equal (t , metav1 .ConditionTrue , cond .Status )
1590+ require .Equal (t , ocv1 .ClusterExtensionRevisionReasonRetrying , cond .Reason )
1591+ require .Contains (t , cond .Message , "revision object collisions" )
1592+ require .Contains (t , cond .Message , "Conflicting Owner" )
1593+ } else {
1594+ require .Equal (t , ctrl.Result {}, result )
1595+ require .NoError (t , err )
1596+ }
1597+ })
1598+ }
1599+ }
1600+
13471601func Test_effectiveCollisionProtection (t * testing.T ) {
13481602 for _ , tc := range []struct {
13491603 name string
0 commit comments