Repository: kubernetes-sigs/kueue

Issue:
v1beta2: add ClusterProfile to v1beta1 becaue it is not persisted


**What happened**:

MultiKueueCluster does not currently support ClusterProfile after the recent changes, because we dropped it from v1beta1 which is used as storage: https://github.com/kubernetes-sigs/kueue/blob/930a474e8a7d4c2bcc80b2f424b8f5aa3f1cdb58/apis/kueue/v1beta1/multikueue_types.go#L61-L64

**What you expected to happen**:

ClusterProfile is not lost

**How to reproduce it (as minimally and precisely as possible)**:

Create 
```yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: MultiKueueCluster
metadata:
  name: multikueue-test-worker1
spec:
  kubeConfig:
    locationType: Secret
    location: worker1-secret
```
it results in  (spec dropped)
```yaml
apiVersion: v1
items:
- apiVersion: kueue.x-k8s.io/v1beta2
  kind: MultiKueueCluster
  metadata:
    creationTimestamp: "2025-11-24T18:24:10Z"
    generation: 1
    name: multikueue-test-worker1
    resourceVersion: "1764008650133327022"
    uid: 5fe176f7-d860-468c-88d6-9266644438cf
  status:
    conditions:
    - lastTransitionTime: "2025-11-24T18:24:10Z"
      message: 'load client config failed: kubeconfig reference is nil'
      observedGeneration: 1
      reason: BadKubeConfig
      status: "False"
      type: Active
kind: List
metadata:
  resourceVersion: ""
```

**Anything else we need to know?**:

This is in RC1. I will prepare RC2 once fixed.

Hints from issue comments:
@mimowo: cc @hdp617 @mszadkow ptal@mimowo: cc @tenzen-y @mimowo: I think while it is ok to reorganize v1beta2 we still need to add the ClusterProfileRef to v1beta1 as a ptr, so that it can be stored, and not dropped.@hdp617: /assign @hdp617 

Solution patch:
diff --git a/apis/kueue/v1beta1/multikueue_conversion.go b/apis/kueue/v1beta1/multikueue_conversion.go
index 235388510..76b1b69c0 100644
--- a/apis/kueue/v1beta1/multikueue_conversion.go
+++ b/apis/kueue/v1beta1/multikueue_conversion.go
@@ -43,6 +43,11 @@ func Convert_v1beta2_MultiKueueClusterSpec_To_v1beta1_MultiKueueClusterSpec(in *
 			LocationType: LocationType(in.ClusterSource.KubeConfig.LocationType),
 		}
 	}
+	if in.ClusterSource.ClusterProfileRef != nil {
+		out.ClusterProfileRef = &ClusterProfileReference{
+			Name: in.ClusterSource.ClusterProfileRef.Name,
+		}
+	}
 	return autoConvert_v1beta2_MultiKueueClusterSpec_To_v1beta1_MultiKueueClusterSpec(in, out, s)
 }
 
@@ -55,5 +60,10 @@ func Convert_v1beta1_MultiKueueClusterSpec_To_v1beta2_MultiKueueClusterSpec(in *
 			},
 		}
 	}
+	if in.ClusterProfileRef != nil {
+		out.ClusterSource.ClusterProfileRef = &v1beta2.ClusterProfileReference{
+			Name: in.ClusterProfileRef.Name,
+		}
+	}
 	return autoConvert_v1beta1_MultiKueueClusterSpec_To_v1beta2_MultiKueueClusterSpec(in, out, s)
 }
diff --git a/apis/kueue/v1beta1/multikueue_types.go b/apis/kueue/v1beta1/multikueue_types.go
index 7d3cbfc68..f9297c7e7 100644
--- a/apis/kueue/v1beta1/multikueue_types.go
+++ b/apis/kueue/v1beta1/multikueue_types.go
@@ -61,6 +61,21 @@ type KubeConfig struct {
 type MultiKueueClusterSpec struct {
 	// kubeConfig is information on how to connect to the cluster.
 	KubeConfig KubeConfig `json:"kubeConfig"`
+
+	// clusterProfileRef is the reference to the ClusterProfile object used to connect to the cluster.
+	//
+	// This is only used to prevent data loss when converting between v1beta2 and v1beta1.
+	// It has no effect in v1beta1.
+	// +optional
+	ClusterProfileRef *ClusterProfileReference `json:"clusterProfileRef,omitempty"`
+}
+
+type ClusterProfileReference struct {
+	// name of the ClusterProfile.
+	// +kubebuilder:validation:MaxLength=256
+	// +kubebuilder:validation:MinLength=1
+	// +required
+	Name string `json:"name,omitempty"`
 }
 
 type MultiKueueClusterStatus struct {
diff --git a/apis/kueue/v1beta1/zz_generated.conversion.go b/apis/kueue/v1beta1/zz_generated.conversion.go
index 07203119e..cf59fc28c 100644
--- a/apis/kueue/v1beta1/zz_generated.conversion.go
+++ b/apis/kueue/v1beta1/zz_generated.conversion.go
@@ -143,6 +143,16 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*ClusterProfileReference)(nil), (*v1beta2.ClusterProfileReference)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_ClusterProfileReference_To_v1beta2_ClusterProfileReference(a.(*ClusterProfileReference), b.(*v1beta2.ClusterProfileReference), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*v1beta2.ClusterProfileReference)(nil), (*ClusterProfileReference)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta2_ClusterProfileReference_To_v1beta1_ClusterProfileReference(a.(*v1beta2.ClusterProfileReference), b.(*ClusterProfileReference), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*ClusterQueue)(nil), (*v1beta2.ClusterQueue)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_ClusterQueue_To_v1beta2_ClusterQueue(a.(*ClusterQueue), b.(*v1beta2.ClusterQueue), scope)
 	}); err != nil {
@@ -1065,6 +1075,26 @@ func Convert_v1beta2_BorrowWithinCohort_To_v1beta1_BorrowWithinCohort(in *v1beta
 	return autoConvert_v1beta2_BorrowWithinCohort_To_v1beta1_BorrowWithinCohort(in, out, s)
 }
 
+func autoConvert_v1beta1_ClusterProfileReference_To_v1beta2_ClusterProfileReference(in *ClusterProfileReference, out *v1beta2.ClusterProfileReference, s conversion.Scope) error {
+	out.Name = in.Name
+	return nil
+}
+
+// Convert_v1beta1_ClusterProfileReference_To_v1beta2_ClusterProfileReference is an autogenerated conversion function.
+func Convert_v1beta1_ClusterProfileReference_To_v1beta2_ClusterProfileReference(in *ClusterProfileReference, out *v1beta2.ClusterProfileReference, s conversion.Scope) error {
+	return autoConvert_v1beta1_ClusterProfileReference_To_v1beta2_ClusterProfileReference(in, out, s)
+}
+
+func autoConvert_v1beta2_ClusterProfileReference_To_v1beta1_ClusterProfileReference(in *v1beta2.ClusterProfileReference, out *ClusterProfileReference, s conversion.Scope) error {
+	out.Name = in.Name
+	return nil
+}
+
+// Convert_v1beta2_ClusterProfileReference_To_v1beta1_ClusterProfileReference is an autogenerated conversion function.
+func Convert_v1beta2_ClusterProfileReference_To_v1beta1_ClusterProfileReference(in *v1beta2.ClusterProfileReference, out *ClusterProfileReference, s conversion.Scope) error {
+	return autoConvert_v1beta2_ClusterProfileReference_To_v1beta1_ClusterProfileReference(in, out, s)
+}
+
 func autoConvert_v1beta1_ClusterQueue_To_v1beta2_ClusterQueue(in *ClusterQueue, out *v1beta2.ClusterQueue, s conversion.Scope) error {
 	out.ObjectMeta = in.ObjectMeta
 	if err := Convert_v1beta1_ClusterQueueSpec_To_v1beta2_ClusterQueueSpec(&in.Spec, &out.Spec, s); err != nil {
@@ -1737,6 +1767,7 @@ func Convert_v1beta2_MultiKueueClusterList_To_v1beta1_MultiKueueClusterList(in *
 
 func autoConvert_v1beta1_MultiKueueClusterSpec_To_v1beta2_MultiKueueClusterSpec(in *MultiKueueClusterSpec, out *v1beta2.MultiKueueClusterSpec, s conversion.Scope) error {
 	// WARNING: in.KubeConfig requires manual conversion: does not exist in peer-type
+	// WARNING: in.ClusterProfileRef requires manual conversion: does not exist in peer-type
 	return nil
 }
 
diff --git a/apis/kueue/v1beta1/zz_generated.deepcopy.go b/apis/kueue/v1beta1/zz_generated.deepcopy.go
index 6f98100a4..c22e2fa92 100644
--- a/apis/kueue/v1beta1/zz_generated.deepcopy.go
+++ b/apis/kueue/v1beta1/zz_generated.deepcopy.go
@@ -302,6 +302,21 @@ func (in *BorrowWithinCohort) DeepCopy() *BorrowWithinCohort {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterProfileReference) DeepCopyInto(out *ClusterProfileReference) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProfileReference.
+func (in *ClusterProfileReference) DeepCopy() *ClusterProfileReference {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterProfileReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterQueue) DeepCopyInto(out *ClusterQueue) {
 	*out = *in
@@ -964,7 +979,7 @@ func (in *MultiKueueCluster) DeepCopyInto(out *MultiKueueCluster) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	out.Spec = in.Spec
+	in.Spec.DeepCopyInto(&out.Spec)
 	in.Status.DeepCopyInto(&out.Status)
 }
 
@@ -1022,6 +1037,11 @@ func (in *MultiKueueClusterList) DeepCopyObject() runtime.Object {
 func (in *MultiKueueClusterSpec) DeepCopyInto(out *MultiKueueClusterSpec) {
 	*out = *in
 	out.KubeConfig = in.KubeConfig
+	if in.ClusterProfileRef != nil {
+		in, out := &in.ClusterProfileRef, &out.ClusterProfileRef
+		*out = new(ClusterProfileReference)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiKueueClusterSpec.
diff --git a/charts/kueue/templates/crd/kueue.x-k8s.io_multikueueclusters.yaml b/charts/kueue/templates/crd/kueue.x-k8s.io_multikueueclusters.yaml
index ec13903bb..2c2d95e2d 100644
--- a/charts/kueue/templates/crd/kueue.x-k8s.io_multikueueclusters.yaml
+++ b/charts/kueue/templates/crd/kueue.x-k8s.io_multikueueclusters.yaml
@@ -80,6 +80,21 @@ spec:
             spec:
               description: spec is the specification of the MultiKueueCluster.
               properties:
+                clusterProfileRef:
+                  description: |-
+                    clusterProfileRef is the reference to the ClusterProfile object used to connect to the cluster.
+
+                    This is only used to prevent data loss when converting between v1beta2 and v1beta1.
+                    It has no effect in v1beta1.
+                  properties:
+                    name:
+                      description: name of the ClusterProfile.
+                      maxLength: 256
+                      minLength: 1
+                      type: string
+                  required:
+                    - name
+                  type: object
                 kubeConfig:
                   description: kubeConfig is information on how to connect to the cluster.
                   properties:
diff --git a/client-go/applyconfiguration/kueue/v1beta1/clusterprofilereference.go b/client-go/applyconfiguration/kueue/v1beta1/clusterprofilereference.go
new file mode 100644
index 000000000..ff6229fed
--- /dev/null
+++ b/client-go/applyconfiguration/kueue/v1beta1/clusterprofilereference.go
@@ -0,0 +1,38 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1beta1
+
+// ClusterProfileReferenceApplyConfiguration represents a declarative configuration of the ClusterProfileReference type for use
+// with apply.
+type ClusterProfileReferenceApplyConfiguration struct {
+	Name *string `json:"name,omitempty"`
+}
+
+// ClusterProfileReferenceApplyConfiguration constructs a declarative configuration of the ClusterProfileReference type for use with
+// apply.
+func ClusterProfileReference() *ClusterProfileReferenceApplyConfiguration {
+	return &ClusterProfileReferenceApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *ClusterProfileReferenceApplyConfiguration) WithName(value string) *ClusterProfileReferenceApplyConfiguration {
+	b.Name = &value
+	return b
+}
diff --git a/client-go/applyconfiguration/kueue/v1beta1/multikueueclusterspec.go b/client-go/applyconfiguration/kueue/v1beta1/multikueueclusterspec.go
index d218f49d9..92814254a 100644
--- a/client-go/applyconfiguration/kueue/v1beta1/multikueueclusterspec.go
+++ b/client-go/applyconfiguration/kueue/v1beta1/multikueueclusterspec.go
@@ -20,7 +20,8 @@ package v1beta1
 // MultiKueueClusterSpecApplyConfiguration represents a declarative configuration of the MultiKueueClusterSpec type for use
 // with apply.
 type MultiKueueClusterSpecApplyConfiguration struct {
-	KubeConfig *KubeConfigApplyConfiguration `json:"kubeConfig,omitempty"`
+	KubeConfig        *KubeConfigApplyConfiguration              `json:"kubeConfig,omitempty"`
+	ClusterProfileRef *ClusterProfileReferenceApplyConfiguration `json:"clusterProfileRef,omitempty"`
 }
 
 // MultiKueueClusterSpecApplyConfiguration constructs a declarative configuration of the MultiKueueClusterSpec type for use with
@@ -36,3 +37,11 @@ func (b *MultiKueueClusterSpecApplyConfiguration) WithKubeConfig(value *KubeConf
 	b.KubeConfig = value
 	return b
 }
+
+// WithClusterProfileRef sets the ClusterProfileRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ClusterProfileRef field is set to the value of the last call.
+func (b *MultiKueueClusterSpecApplyConfiguration) WithClusterProfileRef(value *ClusterProfileReferenceApplyConfiguration) *MultiKueueClusterSpecApplyConfiguration {
+	b.ClusterProfileRef = value
+	return b
+}
diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go
index b8a8257dc..f78502e9e 100644
--- a/client-go/applyconfiguration/utils.go
+++ b/client-go/applyconfiguration/utils.go
@@ -59,6 +59,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
 		return &kueuev1beta1.AdmissionScopeApplyConfiguration{}
 	case v1beta1.SchemeGroupVersion.WithKind("BorrowWithinCohort"):
 		return &kueuev1beta1.BorrowWithinCohortApplyConfiguration{}
+	case v1beta1.SchemeGroupVersion.WithKind("ClusterProfileReference"):
+		return &kueuev1beta1.ClusterProfileReferenceApplyConfiguration{}
 	case v1beta1.SchemeGroupVersion.WithKind("ClusterQueue"):
 		return &kueuev1beta1.ClusterQueueApplyConfiguration{}
 	case v1beta1.SchemeGroupVersion.WithKind("ClusterQueuePendingWorkload"):
diff --git a/config/components/crd/bases/kueue.x-k8s.io_multikueueclusters.yaml b/config/components/crd/bases/kueue.x-k8s.io_multikueueclusters.yaml
index 8a49ccc0c..c54a75322 100644
--- a/config/components/crd/bases/kueue.x-k8s.io_multikueueclusters.yaml
+++ b/config/components/crd/bases/kueue.x-k8s.io_multikueueclusters.yaml
@@ -48,6 +48,21 @@ spec:
           spec:
             description: spec is the specification of the MultiKueueCluster.
             properties:
+              clusterProfileRef:
+                description: |-
+                  clusterProfileRef is the reference to the ClusterProfile object used to connect to the cluster.
+
+                  This is only used to prevent data loss when converting between v1beta2 and v1beta1.
+                  It has no effect in v1beta1.
+                properties:
+                  name:
+                    description: name of the ClusterProfile.
+                    maxLength: 256
+                    minLength: 1
+                    type: string
+                required:
+                - name
+                type: object
               kubeConfig:
                 description: kubeConfig is information on how to connect to the cluster.
                 properties:
diff --git a/site/content/en/docs/reference/kueue.v1beta1.md b/site/content/en/docs/reference/kueue.v1beta1.md
index 15eab11bd..dd470b788 100644
--- a/site/content/en/docs/reference/kueue.v1beta1.md
+++ b/site/content/en/docs/reference/kueue.v1beta1.md
@@ -826,6 +826,30 @@ policy can be preempted by the borrowing workload.</p>
 
 
 
+## `ClusterProfileReference`     {#kueue-x-k8s-io-v1beta1-ClusterProfileReference}
+    
+
+**Appears in:**
+
+- [MultiKueueClusterSpec](#kueue-x-k8s-io-v1beta1-MultiKueueClusterSpec)
+
+
+
+<table class="table">
+<thead><tr><th width="30%">Field</th><th>Description</th></tr></thead>
+<tbody>
+    
+  
+<tr><td><code>name</code> <B>[Required]</B><br/>
+<code>string</code>
+</td>
+<td>
+   <p>name of the ClusterProfile.</p>
+</td>
+</tr>
+</tbody>
+</table>
+
 ## `ClusterQueuePendingWorkload`     {#kueue-x-k8s-io-v1beta1-ClusterQueuePendingWorkload}
     
 
@@ -1934,6 +1958,15 @@ workloads assigned to this LocalQueue.</p>
    <p>kubeConfig is information on how to connect to the cluster.</p>
 </td>
 </tr>
+<tr><td><code>clusterProfileRef</code><br/>
+<a href="#kueue-x-k8s-io-v1beta1-ClusterProfileReference"><code>ClusterProfileReference</code></a>
+</td>
+<td>
+   <p>clusterProfileRef is the reference to the ClusterProfile object used to connect to the cluster.</p>
+<p>This is only used to prevent data loss when converting between v1beta2 and v1beta1.
+It has no effect in v1beta1.</p>
+</td>
+</tr>
 </tbody>
 </table>
 



Test patch:
diff --git a/apis/kueue/v1beta1/multikueue_conversion_test.go b/apis/kueue/v1beta1/multikueue_conversion_test.go
index 17aab5e1f..c8eed3c29 100644
--- a/apis/kueue/v1beta1/multikueue_conversion_test.go
+++ b/apis/kueue/v1beta1/multikueue_conversion_test.go
@@ -179,7 +179,11 @@ func TestMultiKueueClusterConvertFrom(t *testing.T) {
 			},
 			expected: &MultiKueueCluster{
 				ObjectMeta: defaultObjectMeta,
-				Spec:       MultiKueueClusterSpec{},
+				Spec: MultiKueueClusterSpec{
+					ClusterProfileRef: &ClusterProfileReference{
+						Name: "foobar",
+					},
+				},
 			},
 		},
 		"complete MultiKueueCluster with status": {
@@ -293,3 +297,85 @@ func TestMultiKueueClusterConversion_RoundTrip(t *testing.T) {
 		})
 	}
 }
+
+func TestMultiKueueClusterConversion_RoundTrip_Reverse(t *testing.T) {
+	testCases := map[string]struct {
+		v1beta2Obj *v1beta2.MultiKueueCluster
+	}{
+		"complete MultiKueueCluster with ClusterProfile and status": {
+			v1beta2Obj: &v1beta2.MultiKueueCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-multikueuecluster",
+				},
+				Spec: v1beta2.MultiKueueClusterSpec{
+					ClusterSource: v1beta2.ClusterSource{
+						ClusterProfileRef: &v1beta2.ClusterProfileReference{
+							Name: "foobar",
+						},
+					},
+				},
+				Status: v1beta2.MultiKueueClusterStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   "Active",
+							Status: metav1.ConditionTrue,
+							Reason: "Ready",
+						},
+					},
+				},
+			},
+		},
+		"complete MultiKueueCluster with KubeConfig and status": {
+			v1beta2Obj: &v1beta2.MultiKueueCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-multikueuecluster",
+				},
+				Spec: v1beta2.MultiKueueClusterSpec{
+					ClusterSource: v1beta2.ClusterSource{
+						KubeConfig: &v1beta2.KubeConfig{
+							Location:     "test-location",
+							LocationType: v1beta2.LocationType(SecretLocationType),
+						},
+					},
+				},
+				Status: v1beta2.MultiKueueClusterStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   "Active",
+							Status: metav1.ConditionTrue,
+							Reason: "Ready",
+						},
+					},
+				},
+			},
+		},
+		"minimal MultiKueueCluster": {
+			v1beta2Obj: &v1beta2.MultiKueueCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "minimal-mkc",
+				},
+			},
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			// Convert v1beta2 -> v1beta1
+			v1beta1Obj := &MultiKueueCluster{}
+			if err := v1beta1Obj.ConvertFrom(tc.v1beta2Obj); err != nil {
+				t.Fatalf("ConvertFrom failed: %v", err)
+			}
+
+			// Convert v1beta1 -> v1beta2 (round-trip)
+			roundTripped := &v1beta2.MultiKueueCluster{}
+			if err := v1beta1Obj.ConvertTo(roundTripped); err != nil {
+				t.Fatalf("ConvertTo failed: %v", err)
+			}
+
+			// Verify round-trip
+			if diff := cmp.Diff(tc.v1beta2Obj, roundTripped); diff != "" {
+				t.Errorf("round-trip conversion produced diff (-original +roundtripped):\n%s", diff)
+			}
+		})
+	}
+}