@@ -19,6 +19,7 @@ limitations under the License.
1919package e2e
2020
2121import (
22+ "context"
2223 "encoding/base64"
2324 "fmt"
2425 "path/filepath"
@@ -27,14 +28,18 @@ import (
2728
2829 "github.com/k0sproject/k0smotron/e2e/util"
2930 "github.com/stretchr/testify/require"
31+ apierrors "k8s.io/apimachinery/pkg/api/errors"
3032 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3133 "k8s.io/apimachinery/pkg/runtime"
34+ "k8s.io/apimachinery/pkg/util/wait"
3235 "k8s.io/utils/ptr"
3336 "sigs.k8s.io/cluster-api/test/framework"
3437 capiframework "sigs.k8s.io/cluster-api/test/framework"
3538 "sigs.k8s.io/cluster-api/test/framework/bootstrap"
3639 "sigs.k8s.io/cluster-api/test/framework/clusterctl"
3740 capiutil "sigs.k8s.io/cluster-api/util"
41+ "sigs.k8s.io/cluster-api/util/secret"
42+ crclient "sigs.k8s.io/controller-runtime/pkg/client"
3843 "sigs.k8s.io/kind/pkg/apis/config/v1alpha4"
3944 "sigs.k8s.io/kind/pkg/cluster"
4045
@@ -144,6 +149,85 @@ func remoteHCPSpec(t *testing.T) {
144149 })
145150 require .NoError (t , err )
146151 fmt .Println ("Worker nodes are reeady!" )
152+
153+ // Verify resources in the hosting cluster and their owner references, then validate GC on deletion
154+ ownerName := fmt .Sprintf ("%s-root-owner" , clusterName )
155+
156+ // Pre-delete: ensure external owner and etcd cert secrets exist with correct OwnerReferences
157+ {
158+ cm := & corev1.ConfigMap {}
159+ require .NoError (t , hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : ownerName }, cm ))
160+
161+ apiserverEtcdClientSecret := & corev1.Secret {}
162+ etcdServerSecret := & corev1.Secret {}
163+ etcdPeerSecret := & corev1.Secret {}
164+
165+ require .NoError (t , hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : secret .Name (clusterName , secret .APIServerEtcdClient )}, apiserverEtcdClientSecret ))
166+ require .NoError (t , hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : secret .Name (clusterName , "etcd-server" )}, etcdServerSecret ))
167+ require .NoError (t , hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : secret .Name (clusterName , "etcd-peer" )}, etcdPeerSecret ))
168+
169+ assertOwnedByRoot := func (obj metav1.Object ) {
170+ found := false
171+ for _ , or := range obj .GetOwnerReferences () {
172+ if or .Controller != nil && * or .Controller && or .Kind == "ConfigMap" && or .Name == ownerName {
173+ found = true
174+ break
175+ }
176+ }
177+ require .True (t , found , fmt .Sprintf ("%s should be controlled by %s" , obj .GetName (), ownerName ))
178+ }
179+
180+ assertOwnedByRoot (apiserverEtcdClientSecret )
181+ assertOwnedByRoot (etcdServerSecret )
182+ assertOwnedByRoot (etcdPeerSecret )
183+ }
184+
185+ // Delete the management Cluster and verify garbage collection in the hosting cluster
186+ {
187+ fmt .Println ("Deleting Cluster and waiting for GC of hosting-cluster resources" )
188+ require .NoError (t , bootstrapClusterProxy .GetClient ().Delete (ctx , cluster ))
189+
190+ // Wait for Cluster to be deleted in the management cluster
191+ require .NoError (t , wait .PollUntilContextTimeout (ctx , 1 * time .Second , 3 * time .Minute , true , func (ctx context.Context ) (bool , error ) {
192+ cl := cluster .DeepCopy ()
193+ err := bootstrapClusterProxy .GetClient ().Get (ctx , crclient .ObjectKeyFromObject (cluster ), cl )
194+ return apierrors .IsNotFound (err ), nil
195+ }))
196+
197+ // Wait for external owner to be deleted in hosting cluster
198+ require .NoError (t , wait .PollUntilContextTimeout (ctx , 1 * time .Second , 3 * time .Minute , true , func (ctx context.Context ) (bool , error ) {
199+ cm := & corev1.ConfigMap {}
200+ err := hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : ownerName }, cm )
201+ return apierrors .IsNotFound (err ), nil
202+ }))
203+
204+ // Ensure etcd cert Secrets are garbage collected in hosting cluster
205+ for _ , name := range []string {
206+ secret .Name (clusterName , secret .APIServerEtcdClient ),
207+ secret .Name (clusterName , "etcd-server" ),
208+ secret .Name (clusterName , "etcd-peer" ),
209+ } {
210+ require .NoError (t , wait .PollUntilContextTimeout (ctx , 1 * time .Second , 2 * time .Minute , true , func (ctx context.Context ) (bool , error ) {
211+ sec := & corev1.Secret {}
212+ err := hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : name }, sec )
213+ return apierrors .IsNotFound (err ), nil
214+ }))
215+ }
216+
217+ // Optionally ensure key workload resources are GC'd as well (etcd StatefulSet & Service)
218+ etcdStsName := fmt .Sprintf ("kmc-%s-etcd" , clusterName )
219+ etcdSvcName := fmt .Sprintf ("kmc-%s-etcd" , clusterName )
220+ require .NoError (t , wait .PollUntilContextTimeout (ctx , 1 * time .Second , 2 * time .Minute , true , func (ctx context.Context ) (bool , error ) {
221+ sts := & appsv1.StatefulSet {}
222+ err := hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : etcdStsName }, sts )
223+ return apierrors .IsNotFound (err ), nil
224+ }))
225+ require .NoError (t , wait .PollUntilContextTimeout (ctx , 1 * time .Second , 2 * time .Minute , true , func (ctx context.Context ) (bool , error ) {
226+ svc := & corev1.Service {}
227+ err := hostingClusterProxy .GetClient ().Get (ctx , crclient.ObjectKey {Namespace : namespace .Name , Name : etcdSvcName }, svc )
228+ return apierrors .IsNotFound (err ), nil
229+ }))
230+ }
147231}
148232
149233// deployHostingCluster deploys a cluster for hosting control planes.
0 commit comments