Skip to content

Commit f404ab7

Browse files
committed
test: add e2e test for related resource cleanup on primary deletion
Adds a regression test that verifies syncagent.kcp.io/cleanup finalizers are properly removed from kcp-origin related resources when the primary object is deleted. Without the fix, the finalizer remains indefinitely, blocking namespace/workspace deletion. Signed-off-by: Igor Fominykh <ifdotpy@gmail.com>
1 parent b017934 commit f404ab7

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

test/e2e/sync/related_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,192 @@ func TestSyncNonStandardRelatedResourcesMultipleAPIExports(t *testing.T) {
15511551
}
15521552
}
15531553

1554+
// TestDeletePrimaryWithRelatedKcpResource verifies that when a primary object
1555+
// is deleted, related resources with origin:kcp are properly cleaned up:
1556+
// the local copy is deleted and the finalizer on the kcp-side source object
1557+
// is removed. This is a regression test for
1558+
// https://github.com/kcp-dev/api-syncagent/issues/116.
1559+
func TestDeletePrimaryWithRelatedKcpResource(t *testing.T) {
1560+
const apiExportName = "kcp.example.com"
1561+
1562+
ctrlruntime.SetLogger(logr.Discard())
1563+
1564+
ctx := t.Context()
1565+
1566+
// setup a test environment in kcp
1567+
orgKubconfig := utils.CreateOrganization(t, ctx, "delete-primary-related-kcp", apiExportName)
1568+
1569+
// start a service cluster
1570+
envtestKubeconfig, envtestClient, _ := utils.RunEnvtest(t, []string{
1571+
"test/crds/crontab.yaml",
1572+
})
1573+
1574+
// publish Crontabs with a related Secret (origin: kcp)
1575+
t.Log("Publishing CRDs…")
1576+
prCrontabs := &syncagentv1alpha1.PublishedResource{
1577+
ObjectMeta: metav1.ObjectMeta{
1578+
Name: "publish-crontabs",
1579+
},
1580+
Spec: syncagentv1alpha1.PublishedResourceSpec{
1581+
Resource: syncagentv1alpha1.SourceResourceDescriptor{
1582+
APIGroup: "example.com",
1583+
Version: "v1",
1584+
Kind: "CronTab",
1585+
},
1586+
Naming: &syncagentv1alpha1.ResourceNaming{
1587+
Name: "{{ .Object.metadata.name }}",
1588+
Namespace: "synced-{{ .Object.metadata.namespace }}",
1589+
},
1590+
Projection: &syncagentv1alpha1.ResourceProjection{
1591+
Group: "kcp.example.com",
1592+
},
1593+
Related: []syncagentv1alpha1.RelatedResourceSpec{
1594+
{
1595+
Identifier: "credentials",
1596+
Origin: syncagentv1alpha1.RelatedResourceOriginKcp,
1597+
Kind: "Secret",
1598+
Object: syncagentv1alpha1.RelatedResourceObject{
1599+
RelatedResourceObjectSpec: syncagentv1alpha1.RelatedResourceObjectSpec{
1600+
Template: &syncagentv1alpha1.TemplateExpression{
1601+
Template: "my-credentials",
1602+
},
1603+
},
1604+
},
1605+
},
1606+
},
1607+
},
1608+
}
1609+
1610+
if err := envtestClient.Create(ctx, prCrontabs); err != nil {
1611+
t.Fatalf("Failed to create PublishedResource: %v", err)
1612+
}
1613+
1614+
// start the agent
1615+
utils.RunAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, apiExportName, "")
1616+
1617+
// wait until the API is available
1618+
kcpClusterClient := utils.GetKcpAdminClusterClient(t)
1619+
1620+
teamClusterPath := logicalcluster.NewPath("root").Join("delete-primary-related-kcp").Join("team-1")
1621+
teamClient := kcpClusterClient.Cluster(teamClusterPath)
1622+
1623+
utils.WaitForBoundAPI(t, ctx, teamClient, schema.GroupVersionResource{
1624+
Group: apiExportName,
1625+
Version: "v1",
1626+
Resource: "crontabs",
1627+
})
1628+
1629+
// Step 1: Create a CronTab in kcp
1630+
t.Log("Creating CronTab in kcp…")
1631+
1632+
crontab := &unstructured.Unstructured{}
1633+
crontab.SetAPIVersion("kcp.example.com/v1")
1634+
crontab.SetKind("CronTab")
1635+
crontab.SetName("my-crontab")
1636+
crontab.SetNamespace("default")
1637+
if err := unstructured.SetNestedField(crontab.Object, "* * *", "spec", "cronSpec"); err != nil {
1638+
t.Fatalf("Failed to set cronSpec: %v", err)
1639+
}
1640+
1641+
if err := teamClient.Create(ctx, crontab); err != nil {
1642+
t.Fatalf("Failed to create CronTab in kcp: %v", err)
1643+
}
1644+
1645+
// Step 2: Create the related Secret in kcp (origin: kcp means the source is on the kcp side)
1646+
t.Log("Creating credential Secret in kcp…")
1647+
1648+
kcpSecret := &corev1.Secret{
1649+
ObjectMeta: metav1.ObjectMeta{
1650+
Name: "my-credentials",
1651+
Namespace: "default",
1652+
},
1653+
Data: map[string][]byte{
1654+
"password": []byte("hunter2"),
1655+
},
1656+
Type: corev1.SecretTypeOpaque,
1657+
}
1658+
1659+
if err := teamClient.Create(ctx, kcpSecret); err != nil {
1660+
t.Fatalf("Failed to create Secret in kcp: %v", err)
1661+
}
1662+
1663+
// Step 3: Wait for the Secret to be synced down to the service cluster
1664+
t.Log("Waiting for Secret to be synced to service cluster…")
1665+
1666+
localSecret := &corev1.Secret{}
1667+
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
1668+
return envtestClient.Get(ctx, types.NamespacedName{Name: "my-credentials", Namespace: "synced-default"}, localSecret) == nil, nil
1669+
})
1670+
if err != nil {
1671+
t.Fatalf("Secret was not synced to service cluster: %v", err)
1672+
}
1673+
1674+
// Step 4: Verify the kcp Secret has a finalizer (the agent should have added one)
1675+
t.Log("Verifying finalizer on kcp Secret…")
1676+
1677+
err = wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
1678+
secret := &corev1.Secret{}
1679+
if err := teamClient.Get(ctx, types.NamespacedName{Name: "my-credentials", Namespace: "default"}, secret); err != nil {
1680+
return false, nil
1681+
}
1682+
for _, f := range secret.Finalizers {
1683+
if f == "syncagent.kcp.io/cleanup" {
1684+
return true, nil
1685+
}
1686+
}
1687+
return false, nil
1688+
})
1689+
if err != nil {
1690+
t.Fatalf("kcp Secret never received the cleanup finalizer: %v", err)
1691+
}
1692+
1693+
// Step 5: Delete the primary CronTab
1694+
t.Log("Deleting CronTab in kcp…")
1695+
1696+
if err := teamClient.Delete(ctx, crontab); err != nil {
1697+
t.Fatalf("Failed to delete CronTab: %v", err)
1698+
}
1699+
1700+
// Step 6: Verify the finalizer on the kcp Secret is removed
1701+
t.Log("Waiting for finalizer to be removed from kcp Secret…")
1702+
1703+
err = wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 60*time.Second, false, func(ctx context.Context) (done bool, err error) {
1704+
secret := &corev1.Secret{}
1705+
if err := teamClient.Get(ctx, types.NamespacedName{Name: "my-credentials", Namespace: "default"}, secret); err != nil {
1706+
// Secret might have been deleted too, which is also fine
1707+
if apierrors.IsNotFound(err) {
1708+
return true, nil
1709+
}
1710+
return false, nil
1711+
}
1712+
for _, f := range secret.Finalizers {
1713+
if f == "syncagent.kcp.io/cleanup" {
1714+
return false, nil
1715+
}
1716+
}
1717+
return true, nil
1718+
})
1719+
if err != nil {
1720+
t.Fatalf("Finalizer was not removed from kcp Secret after primary deletion: %v", err)
1721+
}
1722+
1723+
// Step 7: Verify the local copy on the service cluster is also deleted
1724+
t.Log("Verifying local Secret copy is deleted…")
1725+
1726+
err = wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
1727+
err = envtestClient.Get(ctx, types.NamespacedName{Name: "my-credentials", Namespace: "synced-default"}, &corev1.Secret{})
1728+
if apierrors.IsNotFound(err) {
1729+
return true, nil
1730+
}
1731+
return false, nil
1732+
})
1733+
if err != nil {
1734+
t.Fatalf("Local Secret copy was not deleted after primary deletion: %v", err)
1735+
}
1736+
1737+
t.Log("Primary deletion correctly cleaned up related resources")
1738+
}
1739+
15541740
func toUnstructured(t *testing.T, obj ctrlruntimeclient.Object) *unstructured.Unstructured {
15551741
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
15561742
if err != nil {

0 commit comments

Comments
 (0)