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

Issue:
Make Cluster UID available within Cluster Builtins
### What would you like to be added (User Story)?

As a runtime extension developer, I'd like to be able to set the Cluster object as the owner reference for any external resources.  

### Detailed Description

As part of the patch generation within runtime extensions, we want to create certain external resources, say `Secret` objects. To ensure that these resources get garbage collected during Cluster deletion, we want to set the Cluster as the owner of these resources. Since UID is a mandatory field when setting an owner reference, it'd be great if we could have the Cluster object's UID present in the Cluster builtins available during patch gen.

### Anything else you would like to add?

_No response_

### Label(s) to be applied

/kind feature



Discussion:
Sounds good!

/triage accepted
/area clusterclass



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 9aa5d2571d8f..a3fba68ed989 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
@@ -532,7 +532,7 @@ spec:
 
 In addition to variables specified in the ClusterClass, the following builtin variables can be 
 referenced in patches:
-- `builtin.cluster.{name,namespace}`
+- `builtin.cluster.{name,namespace,uid}`
 - `builtin.cluster.topology.{version,class}`
 - `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.
diff --git a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go
index 37b5e7507500..e5d28e7259b0 100644
--- a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go
+++ b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go
@@ -16,6 +16,8 @@ limitations under the License.
 
 package v1alpha1
 
+import "k8s.io/apimachinery/pkg/types"
+
 // BuiltinsName is the name of the builtin variable.
 const BuiltinsName = "builtin"
 
@@ -35,6 +37,9 @@ type ClusterBuiltins struct {
 	// Namespace is the namespace of the cluster.
 	Namespace string `json:"namespace,omitempty"`
 
+	// UID is the unqiue identifier of the cluster.
+	UID types.UID `json:"uid,omitempty"`
+
 	// Topology represents the cluster topology variables.
 	Topology *ClusterTopologyBuiltins `json:"topology,omitempty"`
 
diff --git a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go
index 471bb5ee6ab9..81de1ae71798 100644
--- a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go
+++ b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go
@@ -781,6 +781,13 @@ func schema_runtime_hooks_api_v1alpha1_ClusterBuiltins(ref common.ReferenceCallb
 							Format:      "",
 						},
 					},
+					"uid": {
+						SchemaProps: spec.SchemaProps{
+							Description: "UID is the unqiue identifier of the cluster.",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
 					"topology": {
 						SchemaProps: spec.SchemaProps{
 							Description: "Topology represents the cluster topology variables.",
diff --git a/internal/controllers/topology/cluster/patches/variables/variables.go b/internal/controllers/topology/cluster/patches/variables/variables.go
index 9d3cd1ac7a68..70ad946f4bb3 100644
--- a/internal/controllers/topology/cluster/patches/variables/variables.go
+++ b/internal/controllers/topology/cluster/patches/variables/variables.go
@@ -61,6 +61,7 @@ func Global(clusterTopology *clusterv1.Topology, cluster *clusterv1.Cluster, def
 		Cluster: &runtimehooksv1.ClusterBuiltins{
 			Name:      cluster.Name,
 			Namespace: cluster.Namespace,
+			UID:       cluster.UID,
 			Topology: &runtimehooksv1.ClusterTopologyBuiltins{
 				Version: cluster.Spec.Topology.Version,
 				Class:   cluster.GetClassKey().Name,
diff --git a/internal/webhooks/patch_validation.go b/internal/webhooks/patch_validation.go
index f7e2166ee458..2fee9ea73913 100644
--- a/internal/webhooks/patch_validation.go
+++ b/internal/webhooks/patch_validation.go
@@ -471,6 +471,7 @@ var builtinVariables = sets.Set[string]{}.Insert(
 	"builtin.cluster",
 	"builtin.cluster.name",
 	"builtin.cluster.namespace",
+	"builtin.cluster.uid",
 
 	// ClusterTopology builtins.
 	"builtin.cluster.topology",


Test patch:
diff --git a/internal/controllers/topology/cluster/patches/engine_test.go b/internal/controllers/topology/cluster/patches/engine_test.go
index a288df6dc36c..563c5731c951 100644
--- a/internal/controllers/topology/cluster/patches/engine_test.go
+++ b/internal/controllers/topology/cluster/patches/engine_test.go
@@ -28,6 +28,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/uuid"
 	utilfeature "k8s.io/component-base/featuregate/testing"
 	"k8s.io/utils/ptr"
 	. "sigs.k8s.io/controller-runtime/pkg/envtest/komega"
@@ -1073,6 +1074,7 @@ func setupTestObjects() (*scope.ClusterBlueprint, *scope.ClusterState) {
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "cluster1",
 			Namespace: metav1.NamespaceDefault,
+			UID:       uuid.NewUUID(),
 		},
 		Spec: clusterv1.ClusterSpec{
 			Paused: false,
diff --git a/internal/controllers/topology/cluster/patches/variables/variables_test.go b/internal/controllers/topology/cluster/patches/variables/variables_test.go
index f564df166393..1e3c0286dd27 100644
--- a/internal/controllers/topology/cluster/patches/variables/variables_test.go
+++ b/internal/controllers/topology/cluster/patches/variables/variables_test.go
@@ -25,6 +25,7 @@ import (
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/utils/ptr"
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -34,6 +35,7 @@ import (
 )
 
 func TestGlobal(t *testing.T) {
+	clusterUID := "8a35f406-6b9b-4b78-8c93-a7f878d90623"
 	tests := []struct {
 		name                        string
 		clusterTopology             *clusterv1.Topology
@@ -68,6 +70,7 @@ func TestGlobal(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "cluster1",
 					Namespace: metav1.NamespaceDefault,
+					UID:       types.UID(clusterUID),
 				},
 				Spec: clusterv1.ClusterSpec{
 					Topology: &clusterv1.Topology{
@@ -100,6 +103,7 @@ func TestGlobal(t *testing.T) {
 					"cluster":{
 						"name": "cluster1",
   						"namespace": "default",
+						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
  						 "topology":{
   						  	"version": "v1.21.1",
  						   	"class": "clusterClass1"
@@ -149,6 +153,7 @@ func TestGlobal(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "cluster1",
 					Namespace: metav1.NamespaceDefault,
+					UID:       types.UID(clusterUID),
 				},
 				Spec: clusterv1.ClusterSpec{
 					Topology: &clusterv1.Topology{
@@ -181,6 +186,7 @@ func TestGlobal(t *testing.T) {
 					"cluster":{
 						"name": "cluster1",
   						"namespace": "default",
+						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
  						 "topology":{
   						  	"version": "v1.21.1",
  						   	"class": "clusterClass1"
@@ -221,6 +227,7 @@ func TestGlobal(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "cluster1",
 					Namespace: metav1.NamespaceDefault,
+					UID:       types.UID(clusterUID),
 				},
 				Spec: clusterv1.ClusterSpec{
 					Topology: &clusterv1.Topology{
@@ -252,6 +259,7 @@ func TestGlobal(t *testing.T) {
 					"cluster":{
 						"name": "cluster1",
   						"namespace": "default",
+						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
  						 "topology":{
   						  	"version": "v1.21.1",
  						   	"class": "clusterClass1"
@@ -291,6 +299,7 @@ func TestGlobal(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "cluster1",
 					Namespace: metav1.NamespaceDefault,
+					UID:       types.UID(clusterUID),
 				},
 				Spec: clusterv1.ClusterSpec{
 					Topology: &clusterv1.Topology{
@@ -319,6 +328,7 @@ func TestGlobal(t *testing.T) {
 					"cluster":{
   						"name": "cluster1",
   						"namespace": "default",
+						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
  						"topology":{
     						"version": "v1.21.1",
     						"class": "clusterClass1"
@@ -357,6 +367,7 @@ func TestGlobal(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "cluster1",
 					Namespace: metav1.NamespaceDefault,
+					UID:       types.UID(clusterUID),
 				},
 				Spec: clusterv1.ClusterSpec{
 					Topology: &clusterv1.Topology{
@@ -381,6 +392,7 @@ func TestGlobal(t *testing.T) {
 					"cluster":{
   						"name": "cluster1",
   						"namespace": "default",
+						"uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623",
   						"topology":{
 							"version": "v1.21.1",
    						 	"class": "clusterClass1"