Skip to content

Commit 05e013c

Browse files
committed
add related resources tests
Signed-off-by: Karol Szwaj <[email protected]> On-behalf-of: @SAP [email protected]
1 parent a47eef5 commit 05e013c

File tree

2 files changed

+292
-0
lines changed

2 files changed

+292
-0
lines changed

internal/sync/init_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
dummyv1alpha1 "github.com/kcp-dev/api-syncagent/internal/sync/apis/dummy/v1alpha1"
2323

24+
corev1 "k8s.io/api/core/v1"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2526
"k8s.io/apimachinery/pkg/runtime"
2627
)
@@ -32,6 +33,9 @@ func init() {
3233
if err := dummyv1alpha1.AddToScheme(testScheme); err != nil {
3334
panic(err)
3435
}
36+
if err := corev1.AddToScheme(testScheme); err != nil {
37+
panic(err)
38+
}
3539
}
3640

3741
var nonEmptyTime = metav1.Time{

internal/sync/syncer_test.go

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/kcp-dev/api-syncagent/internal/test/diff"
3131
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
3232

33+
corev1 "k8s.io/api/core/v1"
3334
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
3435
apierrors "k8s.io/apimachinery/pkg/api/errors"
3536
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -1305,6 +1306,293 @@ func TestSyncerProcessingSingleResourceWithStatus(t *testing.T) {
13051306
}
13061307
}
13071308

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

0 commit comments

Comments
 (0)