@@ -30,6 +30,7 @@ import (
3030	"github.com/kcp-dev/logicalcluster/v3" 
3131	"github.com/stretchr/testify/require" 
3232
33+ 	rbacv1 "k8s.io/api/rbac/v1" 
3334	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 
3435	"k8s.io/apimachinery/pkg/api/errors" 
3536	"k8s.io/apimachinery/pkg/api/meta" 
@@ -64,6 +65,10 @@ var scenarios = []testScenario{
6465	{"TestReplicateAPIResourceSchemaNegative" , replicateAPIResourceSchemaNegativeScenario },
6566	{"TestReplicateWorkspaceType" , replicateWorkspaceTypeScenario },
6667	{"TestReplicateWorkspaceTypeNegative" , replicateWorkspaceTypeNegativeScenario },
68+ 	{"TestReplicateWorkloadsClusterRole" , replicateWorkloadsClusterRoleScenario },
69+ 	{"TestReplicateWorkloadsClusterRoleNegative" , replicateWorkloadsClusterRoleNegativeScenario },
70+ 	{"TestReplicateWorkloadsClusterRoleBinding" , replicateWorkloadsClusterRoleBindingScenario },
71+ 	{"TestReplicateWorkloadsClusterRoleBindingNegative" , replicateWorkloadsClusterRoleBindingNegativeScenario },
6772}
6873
6974// disruptiveScenarios contains a list of scenarios that will be run in a private environment 
@@ -330,7 +335,9 @@ func replicateResource(ctx context.Context, t *testing.T,
330335	kind  string , /*kind for the given resource*/ 
331336	gvr  schema.GroupVersionResource , /*gvr for the given resource*/ 
332337	res  runtime.Object , /*a strongly typed resource object that will be created*/ 
333- 	resWithModifiedSpec  runtime.Object  /*a strongly typed resource obj with modified spec only, will be used for an update*/ ) {
338+ 	resWithModifiedSpec  runtime.Object , /*a strongly typed resource obj with modified spec only, will be used for an update*/ 
339+ 	prepares  ... func (* replicateResourceScenario ), /*additional functions that allow preparing the context of the source resource before expecting replication*/ 
340+ ) {
334341	t .Helper ()
335342
336343	orgPath , _  :=  framework .NewOrganizationFixture (t , server )
@@ -343,6 +350,10 @@ func replicateResource(ctx context.Context, t *testing.T,
343350	resourceName  :=  resMeta .GetName ()
344351	scenario  :=  & replicateResourceScenario {resourceName : resourceName , kind : kind , gvr : gvr , cluster : clusterName , server : server , kcpShardClusterDynamicClient : kcpShardClusterDynamicClient , cacheKcpClusterDynamicClient : cacheKcpClusterDynamicClient }
345352
353+ 	for  _ , prepare  :=  range  prepares  {
354+ 		prepare (scenario )
355+ 	}
356+ 
346357	t .Logf ("Create source %s %s/%s on the root shard for replication" , kind , clusterName , resourceName )
347358	scenario .CreateSourceResource (ctx , t , res )
348359	t .Logf ("Verify that the source %s %s/%s was replicated to the cache server" , kind , clusterName , resourceName )
@@ -383,7 +394,9 @@ func replicateResourceNegative(ctx context.Context, t *testing.T,
383394	kind  string , /*kind for the given resource*/ 
384395	gvr  schema.GroupVersionResource , /*gvr for the given resource*/ 
385396	res  runtime.Object , /*a strongly typed resource object that will be created*/ 
386- 	resWithModifiedSpec  runtime.Object  /*a strongly typed resource obj with modified spec only, will be used for an update*/ ) {
397+ 	resWithModifiedSpec  runtime.Object , /*a strongly typed resource obj with modified spec only, will be used for an update*/ 
398+ 	prepares  ... func (* replicateResourceScenario ), /*additional functions that allow preparing the context of the source resource before expecting replication*/ 
399+ ) {
387400	t .Helper ()
388401
389402	orgPath , _  :=  framework .NewOrganizationFixture (t , server )
@@ -396,6 +409,10 @@ func replicateResourceNegative(ctx context.Context, t *testing.T,
396409	resourceName  :=  resMeta .GetName ()
397410	scenario  :=  & replicateResourceScenario {resourceName : resourceName , kind : kind , gvr : gvr , cluster : clusterName , server : server , kcpShardClusterDynamicClient : kcpShardClusterDynamicClient , cacheKcpClusterDynamicClient : cacheKcpClusterDynamicClient }
398411
412+ 	for  _ , prepare  :=  range  prepares  {
413+ 		prepare (scenario )
414+ 	}
415+ 
399416	t .Logf ("Create source %s %s/%s on the root shard for replication" , kind , clusterName , resourceName )
400417	scenario .CreateSourceResource (ctx , t , res )
401418	t .Logf ("Verify that the source %s %s/%s was replicated to the cache server" , kind , clusterName , resourceName )
@@ -486,6 +503,14 @@ type replicateResourceScenario struct {
486503	cacheKcpClusterDynamicClient  kcpdynamic.ClusterInterface 
487504}
488505
506+ func  (b  * replicateResourceScenario ) CreateAdditionalResource (ctx  context.Context , t  * testing.T , res  runtime.Object , kind  string , gvr  schema.GroupVersionResource ) {
507+ 	t .Helper ()
508+ 	resUnstructured , err  :=  toUnstructured (res , kind , gvr )
509+ 	require .NoError (t , err )
510+ 	_ , err  =  b .kcpShardClusterDynamicClient .Resource (gvr ).Cluster (b .cluster .Path ()).Create (ctx , resUnstructured , metav1.CreateOptions {})
511+ 	require .NoError (t , err )
512+ }
513+ 
489514func  (b  * replicateResourceScenario ) CreateSourceResource (ctx  context.Context , t  * testing.T , res  runtime.Object ) {
490515	t .Helper ()
491516	resUnstructured , err  :=  toUnstructured (res , b .kind , b .gvr )
@@ -678,14 +703,22 @@ func (b *replicateResourceScenario) verifyResourceReplicationHelper(ctx context.
678703		}
679704		unstructured .RemoveNestedField (originalResource .Object , "metadata" , "resourceVersion" )
680705		unstructured .RemoveNestedField (cachedResource .Object , "metadata" , "resourceVersion" )
706+ 
707+ 		// TODO(davidfestal): find out why the generation is not the same especially for rbacv1. Is it a characteristic of all 
708+ 		// internal KCP resources (which are not backed by CRDs) ? 
709+ 		if  b .gvr .Group  ==  rbacv1 .SchemeGroupVersion .Group  {
710+ 			unstructured .RemoveNestedField (originalResource .Object , "metadata" , "generation" )
711+ 			unstructured .RemoveNestedField (cachedResource .Object , "metadata" , "generation" )
712+ 		}
713+ 
681714		unstructured .RemoveNestedField (cachedResource .Object , "metadata" , "annotations" , genericapirequest .AnnotationKey )
682715		if  cachedStatus , ok  :=  cachedResource .Object ["status" ]; ok  &&  cachedStatus  ==  nil  ||  (cachedStatus  !=  nil  &&  len (cachedStatus .(map [string ]interface {})) ==  0 ) {
683716			// TODO: worth investigating: 
684717			// for some reason cached resources have an empty status set whereas the original resources don't 
685718			unstructured .RemoveNestedField (cachedResource .Object , "status" )
686719		}
687720		if  diff  :=  cmp .Diff (cachedResource .Object , originalResource .Object ); len (diff ) >  0  {
688- 			return  false , fmt .Sprintf ("replicated %s root|%s/%s is different from the original" , b .gvr , cluster , cachedResourceMeta .GetName ())
721+ 			return  false , fmt .Sprintf ("replicated %s root|%s/%s is different from the original: %s " , b .gvr , cluster , cachedResourceMeta .GetName (),  diff )
689722		}
690723		return  true , "" 
691724	}, wait .ForeverTestTimeout , 100 * time .Millisecond )
@@ -732,3 +765,167 @@ func createCacheClientConfigForEnvironment(ctx context.Context, t *testing.T, kc
732765	require .NoError (t , err )
733766	return  cacheServerRestConfig 
734767}
768+ 
769+ // replicateWorkloadsClusterRoleScenario tests if a ClusterRole related to workloads API is propagated to the cache server. 
770+ // The test exercises creation, modification and removal of the Shard object. 
771+ func  replicateWorkloadsClusterRoleScenario (ctx  context.Context , t  * testing.T , server  framework.RunningServer , kcpShardClusterDynamicClient  kcpdynamic.ClusterInterface , cacheKcpClusterDynamicClient  kcpdynamic.ClusterInterface ) {
772+ 	t .Helper ()
773+ 	replicateResource (ctx ,
774+ 		t ,
775+ 		server ,
776+ 		kcpShardClusterDynamicClient ,
777+ 		cacheKcpClusterDynamicClient ,
778+ 		"" ,
779+ 		"ClusterRole" ,
780+ 		rbacv1 .SchemeGroupVersion .WithResource ("clusterroles" ),
781+ 		& rbacv1.ClusterRole {
782+ 			ObjectMeta : metav1.ObjectMeta {
783+ 				Name : withPseudoRandomSuffix ("syncer" ),
784+ 			},
785+ 			Rules : []rbacv1.PolicyRule {
786+ 				{
787+ 					Verbs :         []string {"sync" },
788+ 					APIGroups :     []string {"workload.kcp.io" },
789+ 					Resources :     []string {"synctargets" },
790+ 					ResourceNames : []string {"asynctarget" },
791+ 				},
792+ 			},
793+ 		},
794+ 		nil ,
795+ 	)
796+ }
797+ 
798+ // replicateWorkloadsClusterRoleNegativeScenario checks if modified or even deleted cached ClusterRole (related to workloads API) will be reconciled to match the original object. 
799+ func  replicateWorkloadsClusterRoleNegativeScenario (ctx  context.Context , t  * testing.T , server  framework.RunningServer , kcpShardClusterDynamicClient  kcpdynamic.ClusterInterface , cacheKcpClusterDynamicClient  kcpdynamic.ClusterInterface ) {
800+ 	t .Helper ()
801+ 	replicateResourceNegative (
802+ 		ctx ,
803+ 		t ,
804+ 		server ,
805+ 		kcpShardClusterDynamicClient ,
806+ 		cacheKcpClusterDynamicClient ,
807+ 		"" ,
808+ 		"ClusterRole" ,
809+ 		rbacv1 .SchemeGroupVersion .WithResource ("clusterroles" ),
810+ 		& rbacv1.ClusterRole {
811+ 			ObjectMeta : metav1.ObjectMeta {
812+ 				Name : withPseudoRandomSuffix ("syncer" ),
813+ 			},
814+ 			Rules : []rbacv1.PolicyRule {
815+ 				{
816+ 					Verbs :         []string {"sync" },
817+ 					APIGroups :     []string {"workload.kcp.io" },
818+ 					Resources :     []string {"synctargets" },
819+ 					ResourceNames : []string {"asynctarget" },
820+ 				},
821+ 			},
822+ 		},
823+ 		nil ,
824+ 	)
825+ }
826+ 
827+ // replicateWorkloadsClusterRoleBindingScenario tests if a ClusterRoleBinding related to workloads API is propagated to the cache server. 
828+ // The test exercises creation, modification and removal of the Shard object. 
829+ func  replicateWorkloadsClusterRoleBindingScenario (ctx  context.Context , t  * testing.T , server  framework.RunningServer , kcpShardClusterDynamicClient  kcpdynamic.ClusterInterface , cacheKcpClusterDynamicClient  kcpdynamic.ClusterInterface ) {
830+ 	t .Helper ()
831+ 
832+ 	clusterRole  :=  & rbacv1.ClusterRole {
833+ 		ObjectMeta : metav1.ObjectMeta {
834+ 			Name : withPseudoRandomSuffix ("syncer" ),
835+ 		},
836+ 		Rules : []rbacv1.PolicyRule {
837+ 			{
838+ 				Verbs :         []string {"sync" },
839+ 				APIGroups :     []string {"workload.kcp.io" },
840+ 				Resources :     []string {"synctargets" },
841+ 				ResourceNames : []string {"asynctarget" },
842+ 			},
843+ 		},
844+ 	}
845+ 
846+ 	replicateResource (ctx ,
847+ 		t ,
848+ 		server ,
849+ 		kcpShardClusterDynamicClient ,
850+ 		cacheKcpClusterDynamicClient ,
851+ 		"" ,
852+ 		"ClusterRoleBinding" ,
853+ 		rbacv1 .SchemeGroupVersion .WithResource ("clusterrolebindings" ),
854+ 		& rbacv1.ClusterRoleBinding {
855+ 			ObjectMeta : metav1.ObjectMeta {
856+ 				Name : withPseudoRandomSuffix ("syncer" ),
857+ 			},
858+ 			RoleRef : rbacv1.RoleRef {
859+ 				APIGroup : rbacv1 .SchemeGroupVersion .Group ,
860+ 				Kind :     "ClusterRole" ,
861+ 				Name :     clusterRole .Name ,
862+ 			},
863+ 			Subjects : []rbacv1.Subject {
864+ 				{
865+ 					Kind :      "ServiceAccount" ,
866+ 					APIGroup :  "" ,
867+ 					Name :      "kcp-syncer-0000" ,
868+ 					Namespace : "kcp-syncer-namespace" ,
869+ 				},
870+ 			},
871+ 		},
872+ 		nil ,
873+ 		func (scenario  * replicateResourceScenario ) {
874+ 			t .Logf ("Create additional ClusterRole %s on the root shard for replication" , clusterRole .Name )
875+ 			scenario .CreateAdditionalResource (ctx , t , clusterRole , "ClusterRole" , rbacv1 .SchemeGroupVersion .WithResource ("clusterroles" ))
876+ 		},
877+ 	)
878+ }
879+ 
880+ // replicateWorkloadsClusterRoleNegativeScenario checks if modified or even deleted cached ClusterRole (related to workloads API) will be reconciled to match the original object. 
881+ func  replicateWorkloadsClusterRoleBindingNegativeScenario (ctx  context.Context , t  * testing.T , server  framework.RunningServer , kcpShardClusterDynamicClient  kcpdynamic.ClusterInterface , cacheKcpClusterDynamicClient  kcpdynamic.ClusterInterface ) {
882+ 	t .Helper ()
883+ 
884+ 	clusterRole  :=  & rbacv1.ClusterRole {
885+ 		ObjectMeta : metav1.ObjectMeta {
886+ 			Name : withPseudoRandomSuffix ("syncer" ),
887+ 		},
888+ 		Rules : []rbacv1.PolicyRule {
889+ 			{
890+ 				Verbs :         []string {"sync" },
891+ 				APIGroups :     []string {"workload.kcp.io" },
892+ 				Resources :     []string {"synctargets" },
893+ 				ResourceNames : []string {"asynctarget" },
894+ 			},
895+ 		},
896+ 	}
897+ 
898+ 	replicateResourceNegative (
899+ 		ctx ,
900+ 		t ,
901+ 		server ,
902+ 		kcpShardClusterDynamicClient ,
903+ 		cacheKcpClusterDynamicClient ,
904+ 		"" ,
905+ 		"ClusterRoleBinding" ,
906+ 		rbacv1 .SchemeGroupVersion .WithResource ("clusterrolebindings" ),
907+ 		& rbacv1.ClusterRoleBinding {
908+ 			ObjectMeta : metav1.ObjectMeta {
909+ 				Name : withPseudoRandomSuffix ("syncer" ),
910+ 			},
911+ 			RoleRef : rbacv1.RoleRef {
912+ 				APIGroup : rbacv1 .SchemeGroupVersion .Group ,
913+ 				Kind :     "ClusterRole" ,
914+ 				Name :     clusterRole .Name ,
915+ 			},
916+ 			Subjects : []rbacv1.Subject {
917+ 				{
918+ 					Kind :      "ServiceAccount" ,
919+ 					APIGroup :  "" ,
920+ 					Name :      "kcp-syncer-0000" ,
921+ 					Namespace : "kcp-syncer-namespace" ,
922+ 				},
923+ 			},
924+ 		},
925+ 		nil ,
926+ 		func (scenario  * replicateResourceScenario ) {
927+ 			t .Logf ("Create additional ClusterRole %s on the root shard for replication" , clusterRole .Name )
928+ 			scenario .CreateAdditionalResource (ctx , t , clusterRole , "ClusterRole" , rbacv1 .SchemeGroupVersion .WithResource ("clusterroles" ))
929+ 		},
930+ 	)
931+ }
0 commit comments