@@ -1320,6 +1320,282 @@ func TestSyncerProcessingSingleResourceWithStatus(t *testing.T) {
1320
1320
})
1321
1321
}
1322
1322
}
1323
+ func TestSyncerProcessingRelatedResources (t * testing.T ) {
1324
+ type testcase struct {
1325
+ name string
1326
+ remoteAPIGroup string
1327
+ localCRD * apiextensionsv1.CustomResourceDefinition
1328
+ pubRes * syncagentv1alpha1.PublishedResource
1329
+ remoteObject * unstructured.Unstructured
1330
+ localObject * unstructured.Unstructured
1331
+ existingState string
1332
+ performRequeues bool
1333
+ expectedRemoteObject * unstructured.Unstructured
1334
+ expectedLocalObject * unstructured.Unstructured
1335
+ expectedState string
1336
+ customVerification func (t * testing.T , requeue bool , processErr error , finalRemoteObject * unstructured.Unstructured , finalLocalObject * unstructured.Unstructured , testcase testcase )
1337
+ }
1338
+
1339
+ clusterName := logicalcluster .Name ("testcluster" )
1340
+
1341
+ remoteThingPR := & syncagentv1alpha1.PublishedResource {
1342
+ Spec : syncagentv1alpha1.PublishedResourceSpec {
1343
+ Resource : syncagentv1alpha1.SourceResourceDescriptor {
1344
+ APIGroup : dummyv1alpha1 .GroupName ,
1345
+ Version : dummyv1alpha1 .GroupVersion ,
1346
+ Kind : "Thing" ,
1347
+ },
1348
+ Projection : & syncagentv1alpha1.ResourceProjection {
1349
+ Kind : "RemoteThing" ,
1350
+ },
1351
+ // include explicit naming rules to be independent of possible changes to the defaults
1352
+ Naming : & syncagentv1alpha1.ResourceNaming {
1353
+ Name : "$remoteClusterName-$remoteName" , // Things are Cluster-scoped
1354
+ },
1355
+ Related : []syncagentv1alpha1.RelatedResourceSpec {
1356
+ {
1357
+ Identifier : "mandatory-secret" ,
1358
+ Origin : "service" ,
1359
+ Kind : "Thing" ,
1360
+ Reference : syncagentv1alpha1.RelatedResourceReference {
1361
+ Name : syncagentv1alpha1.ResourceLocator {
1362
+ Path : "spec.otherTest.name" ,
1363
+ },
1364
+ Namespace : & syncagentv1alpha1.ResourceLocator {
1365
+ Path : "spec.otherTest.namespace" ,
1366
+ },
1367
+ },
1368
+ Optional : false ,
1369
+ },
1370
+ {
1371
+ Identifier : "optional-secret" ,
1372
+ Origin : "kcp" ,
1373
+ Kind : "Thing" ,
1374
+ Reference : syncagentv1alpha1.RelatedResourceReference {
1375
+ Name : syncagentv1alpha1.ResourceLocator {
1376
+ Path : "spec.test.name" ,
1377
+ },
1378
+ Namespace : & syncagentv1alpha1.ResourceLocator {
1379
+ Path : "spec.test.namespace" ,
1380
+ },
1381
+ },
1382
+ Optional : true ,
1383
+ },
1384
+ },
1385
+ },
1386
+ }
1387
+
1388
+ testcases := []testcase {
1389
+ {
1390
+ name : "optional related resource does not exist" ,
1391
+ remoteAPIGroup : "remote.example.corp" ,
1392
+ localCRD : loadCRD ("things" ),
1393
+ pubRes : remoteThingPR ,
1394
+ performRequeues : true ,
1395
+
1396
+ remoteObject : newUnstructured (& dummyv1alpha1.Thing {
1397
+ ObjectMeta : metav1.ObjectMeta {
1398
+ Name : "my-test-thing" ,
1399
+ },
1400
+ Spec : dummyv1alpha1.ThingSpec {
1401
+ Username : "Colonel Mustard" ,
1402
+ },
1403
+ }, withGroupKind ("remote.example.corp" , "RemoteThing" )),
1404
+ localObject : nil ,
1405
+ existingState : "" ,
1406
+
1407
+ expectedRemoteObject : newUnstructured (& dummyv1alpha1.Thing {
1408
+ ObjectMeta : metav1.ObjectMeta {
1409
+ Name : "my-test-thing" ,
1410
+ Finalizers : []string {
1411
+ deletionFinalizer ,
1412
+ },
1413
+ },
1414
+ Spec : dummyv1alpha1.ThingSpec {
1415
+ Username : "Colonel Mustard" ,
1416
+ },
1417
+ }, withGroupKind ("remote.example.corp" , "RemoteThing" )),
1418
+ expectedLocalObject : newUnstructured (& dummyv1alpha1.Thing {
1419
+ ObjectMeta : metav1.ObjectMeta {
1420
+ Name : "testcluster-my-test-thing" ,
1421
+ Labels : map [string ]string {
1422
+ agentNameLabel : "textor-the-doctor" ,
1423
+ remoteObjectClusterLabel : "testcluster" ,
1424
+ remoteObjectNameHashLabel : "c346c8ceb5d104cc783d09b95e8ea7032c190948" ,
1425
+ },
1426
+ Annotations : map [string ]string {
1427
+ remoteObjectNameAnnotation : "my-test-thing" ,
1428
+ },
1429
+ },
1430
+ Spec : dummyv1alpha1.ThingSpec {
1431
+ Username : "Colonel Mustard" ,
1432
+ },
1433
+ }),
1434
+ expectedState : `{"apiVersion":"remote.example.corp/v1alpha1","kind":"RemoteThing","metadata":{"name":"my-test-thing"},"spec":{"username":"Colonel Mustard"}}` ,
1435
+ },
1436
+ {
1437
+ name : "mandatory related resource does not exist" ,
1438
+ remoteAPIGroup : "remote.example.corp" ,
1439
+ localCRD : loadCRD ("things" ),
1440
+ pubRes : remoteThingPR ,
1441
+ performRequeues : true ,
1442
+
1443
+ remoteObject : newUnstructured (& dummyv1alpha1.Thing {
1444
+ ObjectMeta : metav1.ObjectMeta {
1445
+ Name : "my-test-thing" ,
1446
+ },
1447
+ Spec : dummyv1alpha1.ThingSpec {
1448
+ Username : "Colonel Mustard" ,
1449
+ },
1450
+ }, withGroupKind ("remote.example.corp" , "RemoteThing" )),
1451
+ localObject : nil ,
1452
+ existingState : "" ,
1453
+
1454
+ expectedRemoteObject : newUnstructured (& dummyv1alpha1.Thing {
1455
+ ObjectMeta : metav1.ObjectMeta {
1456
+ Name : "my-test-thing" ,
1457
+ Finalizers : []string {
1458
+ deletionFinalizer ,
1459
+ },
1460
+ },
1461
+ Spec : dummyv1alpha1.ThingSpec {
1462
+ Username : "Colonel Mustard" ,
1463
+ },
1464
+ }, withGroupKind ("remote.example.corp" , "RemoteThing" )),
1465
+ expectedLocalObject : newUnstructured (& dummyv1alpha1.Thing {
1466
+ ObjectMeta : metav1.ObjectMeta {
1467
+ Name : "testcluster-my-test-thing" ,
1468
+ Labels : map [string ]string {
1469
+ agentNameLabel : "textor-the-doctor" ,
1470
+ remoteObjectClusterLabel : "testcluster" ,
1471
+ remoteObjectNameHashLabel : "c346c8ceb5d104cc783d09b95e8ea7032c190948" ,
1472
+ },
1473
+ Annotations : map [string ]string {
1474
+ remoteObjectNameAnnotation : "my-test-thing" ,
1475
+ },
1476
+ },
1477
+ Spec : dummyv1alpha1.ThingSpec {
1478
+ Username : "Colonel Mustard" ,
1479
+ },
1480
+ }),
1481
+ expectedState : `{"apiVersion":"remote.example.corp/v1alpha1","kind":"RemoteThing","metadata":{"name":"my-test-thing"},"spec":{"username":"Colonel Mustard"}}` ,
1482
+ },
1483
+ }
1484
+
1485
+ const stateNamespace = "kcp-system"
1486
+
1487
+ for _ , testcase := range testcases {
1488
+ t .Run (testcase .name , func (t * testing.T ) {
1489
+ localClient := buildFakeClient (testcase .localObject )
1490
+ remoteClient := buildFakeClient (testcase .remoteObject )
1491
+
1492
+ syncer , err := NewResourceSyncer (
1493
+ // zap.Must(zap.NewDevelopment()).Sugar(),
1494
+ zap .NewNop ().Sugar (),
1495
+ localClient ,
1496
+ remoteClient ,
1497
+ testcase .pubRes ,
1498
+ testcase .localCRD ,
1499
+ testcase .remoteAPIGroup ,
1500
+ nil ,
1501
+ stateNamespace ,
1502
+ "textor-the-doctor" ,
1503
+ )
1504
+ if err != nil {
1505
+ t .Fatalf ("Failed to create syncer: %v" , err )
1506
+ }
1507
+
1508
+ localCtx := context .Background ()
1509
+ remoteCtx := kontext .WithCluster (localCtx , clusterName )
1510
+ ctx := NewContext (localCtx , remoteCtx )
1511
+
1512
+ // setup a custom state backend that we can prime
1513
+ var backend * kubernetesBackend
1514
+ syncer .newObjectStateStore = func (primaryObject , stateCluster syncSide ) ObjectStateStore {
1515
+ // .Process() is called multiple times, but we want the state to persist between reconciles.
1516
+ if backend == nil {
1517
+ backend = newKubernetesBackend (stateNamespace , primaryObject , stateCluster )
1518
+ if testcase .existingState != "" {
1519
+ if err := backend .Put (testcase .remoteObject , clusterName , []byte (testcase .existingState )); err != nil {
1520
+ t .Fatalf ("Failed to prime state store: %v" , err )
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ return & objectStateStore {
1526
+ backend : backend ,
1527
+ }
1528
+ }
1529
+
1530
+ var requeue bool
1531
+
1532
+ if testcase .performRequeues {
1533
+ target := testcase .remoteObject .DeepCopy ()
1534
+
1535
+ for i := 0 ; true ; i ++ {
1536
+ if i > 20 {
1537
+ t .Fatalf ("Detected potential infinite loop, stopping after %d requeues." , i )
1538
+ }
1539
+
1540
+ requeue , err = syncer .Process (ctx , target )
1541
+ if err != nil {
1542
+ break
1543
+ }
1544
+
1545
+ if ! requeue {
1546
+ break
1547
+ }
1548
+
1549
+ if err = remoteClient .Get (remoteCtx , ctrlruntimeclient .ObjectKeyFromObject (target ), target ); err != nil {
1550
+ // it's possible for the processing to have deleted the remote object,
1551
+ // so a NotFound is valid here
1552
+ if apierrors .IsNotFound (err ) {
1553
+ break
1554
+ }
1555
+
1556
+ t .Fatalf ("Failed to get updated remote object: %v" , err )
1557
+ }
1558
+ }
1559
+ } else {
1560
+ requeue , err = syncer .Process (ctx , testcase .remoteObject )
1561
+ }
1562
+
1563
+ finalRemoteObject , getErr := getFinalObjectVersion (remoteCtx , remoteClient , testcase .remoteObject , testcase .expectedRemoteObject )
1564
+ if getErr != nil {
1565
+ t .Fatalf ("Failed to get final remote object: %v" , getErr )
1566
+ }
1567
+
1568
+ finalLocalObject , getErr := getFinalObjectVersion (localCtx , localClient , testcase .localObject , testcase .expectedLocalObject )
1569
+ if getErr != nil {
1570
+ t .Fatalf ("Failed to get final local object: %v" , getErr )
1571
+ }
1572
+
1573
+ if testcase .customVerification != nil {
1574
+ testcase .customVerification (t , requeue , err , finalRemoteObject , finalLocalObject , testcase )
1575
+ } else {
1576
+ if err != nil {
1577
+ t .Fatalf ("Processing failed: %v" , err )
1578
+ }
1579
+
1580
+ assertObjectsEqual (t , "local" , testcase .expectedLocalObject , finalLocalObject )
1581
+ assertObjectsEqual (t , "remote" , testcase .expectedRemoteObject , finalRemoteObject )
1582
+
1583
+ if testcase .expectedState != "" {
1584
+ if backend == nil {
1585
+ t .Fatal ("Cannot check object state, state store was never instantiated." )
1586
+ }
1587
+
1588
+ finalState , err := backend .Get (testcase .expectedRemoteObject , clusterName )
1589
+ if err != nil {
1590
+ t .Fatalf ("Failed to get final state: %v" , err )
1591
+ } else if ! bytes .Equal (finalState , []byte (testcase .expectedState )) {
1592
+ t .Fatalf ("States do not match:\n %s" , diff .StringDiff (testcase .expectedState , string (finalState )))
1593
+ }
1594
+ }
1595
+ }
1596
+ })
1597
+ }
1598
+ }
1323
1599
1324
1600
func assertObjectsEqual (t * testing.T , kind string , expected , actual * unstructured.Unstructured ) {
1325
1601
if expected == nil {
0 commit comments