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