Skip to content

Commit 7d0955d

Browse files
committed
add related resources tests
Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> On-behalf-of: @SAP karol.szwaj@sap.com
1 parent 2f5479b commit 7d0955d

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"
@@ -1321,6 +1322,293 @@ func TestSyncerProcessingSingleResourceWithStatus(t *testing.T) {
13211322
}
13221323
}
13231324

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+
13241612
func assertObjectsEqual(t *testing.T, kind string, expected, actual *unstructured.Unstructured) {
13251613
if expected == nil {
13261614
if actual != nil {

0 commit comments

Comments
 (0)