Repository: kubernetes-sigs/cluster-api (Go)

Issue:
Make classNamespace available in ClusterBuiltins
### What would you like to be added (User Story)?

As a runtime extension developer, I'd like to be able to access the Cluster topology's classNamespace field during patch generation.

### Detailed Description

As part of the patch generation within runtime extensions, we want to ensure that the classNamespace field that points to the namespace of the CC in use is available.

### Anything else you would like to add?

Similar issues in the past:
- https://github.com/kubernetes-sigs/cluster-api/issues/10836
- https://github.com/kubernetes-sigs/cluster-api/issues/10823
- https://github.com/kubernetes-sigs/cluster-api/issues/12009

### Label(s) to be applied

/kind feature
/area clusterclass


Discussion:
Makes sense

/triage accepted



Patch:
diff --git a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md
index 76b91cad8dbb..341cfabb40be 100644
--- a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md
+++ b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md
@@ -609,7 +609,7 @@ spec:
 In addition to variables specified in the ClusterClass, the following builtin variables can be 
 referenced in patches:
 - `builtin.cluster.{name,namespace,uid,metadata.labels,metadata.annotations}`
-- `builtin.cluster.topology.{version,class}`
+- `builtin.cluster.topology.{version,class,classNamespace}`
 - `builtin.cluster.network.{serviceDomain,services,pods,ipFamily}`
     - Note: ipFamily is deprecated and will be removed in a future release. see https://github.com/kubernetes-sigs/cluster-api/issues/7521.
 - `builtin.controlPlane.{replicas,version,name,metadata.labels,metadata.annotations}`
diff --git a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go
index 33a92445b32d..a7adb476d00e 100644
--- a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go
+++ b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go
@@ -79,6 +79,10 @@ type ClusterTopologyBuiltins struct {
 	// class is the name of the ClusterClass of the Cluster.
 	// +optional
 	Class string `json:"class,omitempty"`
+
+	// classNamespace is the namespace of the ClusterClass of the Cluster.
+	// +optional
+	ClassNamespace string `json:"classNamespace,omitempty"`
 }
 
 // ClusterNetworkBuiltins represents builtin cluster network variables.
diff --git a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go
index dfbbfdc39dea..627c45b105eb 100644
--- a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go
+++ b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go
@@ -890,6 +890,13 @@ func schema_runtime_hooks_api_v1alpha1_ClusterTopologyBuiltins(ref common.Refere
 							Format:      "",
 						},
 					},
+					"classNamespace": {
+						SchemaProps: spec.SchemaProps{
+							Description: "classNamespace is the namespace of the ClusterClass of the Cluster.",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
 				},
 			},
 		},
diff --git a/internal/controllers/topology/cluster/patches/variables/variables.go b/internal/controllers/topology/cluster/patches/variables/variables.go
index 8755e7e0cf4d..594b81fcb994 100644
--- a/internal/controllers/topology/cluster/patches/variables/variables.go
+++ b/internal/controllers/topology/cluster/patches/variables/variables.go
@@ -56,8 +56,9 @@ func Global(clusterTopology *clusterv1.Topology, cluster *clusterv1.Cluster, pat
 			Namespace: cluster.Namespace,
 			UID:       cluster.UID,
 			Topology: &runtimehooksv1.ClusterTopologyBuiltins{
-				Version: cluster.Spec.Topology.Version,
-				Class:   cluster.GetClassKey().Name,
+				Version:        cluster.Spec.Topology.Version,
+				Class:          cluster.GetClassKey().Name,
+				ClassNamespace: cluster.GetClassKey().Namespace,
 			},
 		},
 	}
diff --git a/internal/webhooks/patch_validation.go b/internal/webhooks/patch_validation.go
index d4535aed900f..c3447084217d 100644
--- a/internal/webhooks/patch_validation.go
+++ b/internal/webhooks/patch_validation.go
@@ -478,6 +478,7 @@ var builtinVariables = sets.Set[string]{}.Insert(
 	// ClusterTopology builtins.
 	"builtin.cluster.topology",
 	"builtin.cluster.topology.class",
+	"builtin.cluster.topology.classNamespace",
 	"builtin.cluster.topology.version",
 
 	// ClusterNetwork builtins


Test patch:
diff --git a/internal/controllers/topology/cluster/patches/variables/variables_test.go b/internal/controllers/topology/cluster/patches/variables/variables_test.go
index 88ef4c73ca84..35646e255a1d 100644
--- a/internal/controllers/topology/cluster/patches/variables/variables_test.go
+++ b/internal/controllers/topology/cluster/patches/variables/variables_test.go
@@ -107,7 +107,8 @@ func TestGlobal(t *testing.T) {
 						"metadata": {"labels":{"foo":"bar"}, "annotations":{"fizz":"buzz"}},
  						 "topology":{
   						  	"version": "v1.21.1",
- 						   	"class": "clusterClass1"
+							"class": "clusterClass1",
+							"classNamespace": "default"
   						},
   						"network":{
 							"serviceDomain":"cluster.local",
@@ -179,7 +180,8 @@ func TestGlobal(t *testing.T) {
 						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
  						 "topology":{
   						  	"version": "v1.21.1",
- 						   	"class": "clusterClass1"
+							"class": "clusterClass1",
+							"classNamespace": "default"
   						},
   						"network":{
 							"serviceDomain":"cluster.local",
@@ -250,8 +252,9 @@ func TestGlobal(t *testing.T) {
   						"namespace": "default",
 						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
  						 "topology":{
-  						  	"version": "v1.21.1",
- 						   	"class": "clusterClass1"
+							"version": "v1.21.1",
+							"class": "clusterClass1",
+							"classNamespace": "default"
   						},
   						"network":{
   						 	"services":["10.10.10.1/24"],
@@ -318,8 +321,9 @@ func TestGlobal(t *testing.T) {
   						"namespace": "default",
 						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
  						"topology":{
-    						"version": "v1.21.1",
-    						"class": "clusterClass1"
+							"version": "v1.21.1",
+							"class": "clusterClass1",
+							"classNamespace": "default"
   						},
   						"network":{
     						"serviceDomain":"cluster.local",
@@ -382,7 +386,8 @@ func TestGlobal(t *testing.T) {
 						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
   						"topology":{
 							"version": "v1.21.1",
-   						 	"class": "clusterClass1"
+							"class": "clusterClass1",
+							"classNamespace": "default"
 						}
 					}}`),
 				},