Repository: kubernetes-sigs/cluster-api-provider-gcp
Pull request #1434: feat: align GCPManagedControlPlane with CAPI >=v1.9
Author: login: salasberryfin
Labels: name: kind/bug
description: Categorizes issue or PR as related to a bug.
name: cncf-cla: yes
description: Indicates the PR's author has signed the CNCF CLA.
name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: lgtm
description: "Looks good to me", indicates that a PR is ready to be merged.
name: release-note-action-required
description: Denotes a PR that introduces potentially breaking changes that require user action.
Created: 2025-02-25T10:09:46Z | Merged: 2025-03-05T14:25:46Z

/kind bug

**What this PR does / why we need it**:

To align `GCPManagedControlPlane` specification and status with CAPI >=v1.9 we need to introduce new control plane version fields and set existing ones for deprecation in coming releases. The list of changes, as agreed on Slack (thanks @damdo @richardcase @cpanato):
- Introduce new `Version` fields in `GCPManagedControlPlaneSpec` and `GCPManagedControlPlaneStatus` as per the updated CAPI contract for control planes.
- Mark `ControlPlaneVersion` and `CurrentVersion` as deprecated with a message saying to use `Version` instead.
- Have a validation webhook that disallows setting `Version` and `ControlPlaneVersion`.
- Update all templates and e2e tests to use `Version`.

**Which issue(s) this PR fixes**:
Fixes #1433 

**Special notes for your reviewer**:

In the future, as the deprecated field is ultimately removed, we can add a conversion from `ControlPlaneVersion` to `Version`.

**TODOs**:

- [x] squashed commits
- [ ] includes documentation
- [ ] adds unit tests

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
action required
!! ACTION REQUIRED BEFORE UPGRADING !!
If you are using the GCPManagedControlPlane to provision GKE clusters and you do not have a spec.Version specified in such resource (or you are using spec.ControlPlaneVersion), you will need to either:
a) explicitly set such `spec.Version` field before upgrading CAPG (if you are already using spec.ControlPlaneVersion, please, use spec.Version instead)
or b) disable the MachineSetPreflightChecks in your cluster either:
b1) by setting this core CAPI feature gate to `false`
b2) or by disabling it via the relevant annotation on all the machineSets belonging to said cluster (follow this guide on how to do this: https://cluster-api.sigs.k8s.io/tasks/experimental-features/machineset-preflight-checks).
This is necessary as core CAPI 1.9 introduces a feature gate change, setting MachineSetPreflightChecks=true, which in turn relies on the presence of spec.Version and status.Version on the GCPManagedControlPlane object.
These fields will be deprecated in a future release.
```


Linked issue #1433: Align managed control plane to latest CAPI changes
/kind bug
/area gke

# Description

To align `GCPManagedControlPlane` specification and status with CAPI >=v1.9 we need to introduce new control plane version fields and set existing ones for deprecation in coming releases. The list of changes, as agreed on Slack (thanks @damdo @richardcase @cpanato):
- Introduce new `Version` fields in `GCPManagedControlPlaneSpec` and `GCPManagedControlPlaneStatus` as per the updated CAPI contract for control planes.
- Mark `ControlPlaneVersion` and `CurrentVersion` as deprecated with a message saying to use `Version` instead.
- Have a validation webhook that disallows setting `Version` and `ControlPlaneVersion`.
- Update all templates and e2e tests to use `Version`.

In the future, as the deprecated field is ultimately removed, we can add a conversion from `ControlPlaneVersion` to `Version`.
Linked issue comments: user: login: salasberryfin
created_at: 2025-02-25T10:00:52Z
body: /assign
author_association: CONTRIBUTOR

Diff:
diff --git a/.golangci.yml b/.golangci.yml
index 4bb218a39..faa3cd58c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -196,6 +196,11 @@ issues:
     - linters:
         - gocritic
       text: "appendAssign: append result not assigned to the same slice"
+    # Specific exclude rules for deprecated fields that are still part of the codebase.
+    # These should be removed as the referenced deprecated item is removed from the project.
+    - linters:
+        - staticcheck
+      text: "SA1019: (s.GCPManagedControlPlane.Status.CurrentVersion|s.scope.GCPManagedControlPlane.Status.CurrentVersion|spec.ControlPlaneVersion|s.GCPManagedControlPlane.Spec.ControlPlaneVersion|s.scope.GCPManagedControlPlane.Spec.ControlPlaneVersion) is deprecated: This field will soon be removed and you are expected to use Version instead."
 
 run:
   timeout: 10m
diff --git a/cloud/scope/managedcontrolplane.go b/cloud/scope/managedcontrolplane.go
index a9ac54217..1587a6aef 100644
--- a/cloud/scope/managedcontrolplane.go
+++ b/cloud/scope/managedcontrolplane.go
@@ -237,3 +237,14 @@ func (s *ManagedControlPlaneScope) SetEndpoint(host string) {
 func (s *ManagedControlPlaneScope) IsAutopilotCluster() bool {
 	return s.GCPManagedControlPlane.Spec.EnableAutopilot
 }
+
+// GetControlPlaneVersion returns the control plane version from the specification.
+func (s *ManagedControlPlaneScope) GetControlPlaneVersion() *string {
+	if s.GCPManagedControlPlane.Spec.Version != nil {
+		return s.GCPManagedControlPlane.Spec.Version
+	}
+	if s.GCPManagedControlPlane.Spec.ControlPlaneVersion != nil {
+		return s.GCPManagedControlPlane.Spec.ControlPlaneVersion
+	}
+	return nil
+}
diff --git a/cloud/services/container/clusters/reconcile.go b/cloud/services/container/clusters/reconcile.go
index 64ceec390..e3e729c32 100644
--- a/cloud/services/container/clusters/reconcile.go
+++ b/cloud/services/container/clusters/reconcile.go
@@ -96,7 +96,9 @@ func (s *Service) Reconcile(ctx context.Context) (ctrl.Result, error) {
 	}
 
 	log.V(2).Info("gke cluster found", "status", cluster.GetStatus())
-	s.scope.GCPManagedControlPlane.Status.CurrentVersion = convertToSdkMasterVersion(cluster.GetCurrentMasterVersion())
+	controlPlaneVersion := convertToSdkMasterVersion(cluster.GetCurrentMasterVersion())
+	s.scope.GCPManagedControlPlane.Status.CurrentVersion = controlPlaneVersion
+	s.scope.GCPManagedControlPlane.Status.Version = &controlPlaneVersion
 
 	switch cluster.GetStatus() {
 	case containerpb.Cluster_PROVISIONING:
@@ -271,8 +273,8 @@ func (s *Service) createCluster(ctx context.Context, log *logr.Logger) error {
 			},
 		},
 	}
-	if s.scope.GCPManagedControlPlane.Spec.ControlPlaneVersion != nil {
-		cluster.InitialClusterVersion = convertToSdkMasterVersion(*s.scope.GCPManagedControlPlane.Spec.ControlPlaneVersion)
+	if initialClusterVersionFromSpec := s.scope.GetControlPlaneVersion(); initialClusterVersionFromSpec != nil {
+		cluster.InitialClusterVersion = convertToSdkMasterVersion(*initialClusterVersionFromSpec)
 	}
 	if s.scope.GCPManagedControlPlane.Spec.ClusterNetwork != nil {
 		cn := s.scope.GCPManagedControlPlane.Spec.ClusterNetwork
@@ -434,8 +436,8 @@ func (s *Service) checkDiffAndPrepareUpdate(existingCluster *containerpb.Cluster
 		}
 	}
 	// Master version
-	if s.scope.GCPManagedControlPlane.Spec.ControlPlaneVersion != nil {
-		desiredMasterVersion := convertToSdkMasterVersion(*s.scope.GCPManagedControlPlane.Spec.ControlPlaneVersion)
+	if desiredMasterVersionFromSpec := s.scope.GetControlPlaneVersion(); desiredMasterVersionFromSpec != nil {
+		desiredMasterVersion := convertToSdkMasterVersion(*desiredMasterVersionFromSpec)
 		existingClusterMasterVersion := convertToSdkMasterVersion(existingCluster.GetCurrentMasterVersion())
 		if desiredMasterVersion != existingClusterMasterVersion {
 			needUpdate = true
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml
index c1de7a59d..2eb9c294d 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml
@@ -31,6 +31,10 @@ spec:
       jsonPath: .status.currentVersion
       name: CurrentVersion
       type: string
+    - description: The Kubernetes version of the GKE control plane
+      jsonPath: .status.version
+      name: Version
+      type: string
     - description: API Endpoint
       jsonPath: .spec.endpoint
       name: Endpoint
@@ -133,6 +137,8 @@ spec:
                   ControlPlaneVersion represents the control plane version of the GKE cluster.
                   If not specified, the default version currently supported by GKE will be
                   used.
+
+                  Deprecated: This field will soon be removed and you are expected to use Version instead.
                 type: string
               description:
                 description: Description describe the cluster.
@@ -217,6 +223,12 @@ spec:
                 - regular
                 - stable
                 type: string
+              version:
+                description: |-
+                  Version represents the control plane version of the GKE cluster.
+                  If not specified, the default version currently supported by GKE will be
+                  used.
+                type: string
             required:
             - location
             - project
@@ -272,8 +284,10 @@ spec:
                   type: object
                 type: array
               currentVersion:
-                description: CurrentVersion shows the current version of the GKE control
-                  plane.
+                description: |-
+                  CurrentVersion shows the current version of the GKE control plane.
+
+                  Deprecated: This field will soon be removed and you are expected to use Version instead.
                 type: string
               initialized:
                 description: |-
@@ -286,6 +300,9 @@ spec:
                   Ready denotes that the GCPManagedControlPlane API Server is ready to
                   receive requests.
                 type: boolean
+              version:
+                description: Version represents the version of the GKE control plane.
+                type: string
             required:
             - ready
             type: object
diff --git a/exp/api/v1beta1/gcpmanagedcontrolplane_types.go b/exp/api/v1beta1/gcpmanagedcontrolplane_types.go
index d4c65b4c5..53ecffcdd 100644
--- a/exp/api/v1beta1/gcpmanagedcontrolplane_types.go
+++ b/exp/api/v1beta1/gcpmanagedcontrolplane_types.go
@@ -146,8 +146,16 @@ type GCPManagedControlPlaneSpec struct {
 	// ControlPlaneVersion represents the control plane version of the GKE cluster.
 	// If not specified, the default version currently supported by GKE will be
 	// used.
+	//
+	// Deprecated: This field will soon be removed and you are expected to use Version instead.
+	//
 	// +optional
 	ControlPlaneVersion *string `json:"controlPlaneVersion,omitempty"`
+	// Version represents the control plane version of the GKE cluster.
+	// If not specified, the default version currently supported by GKE will be
+	// used.
+	// +optional
+	Version *string `json:"version,omitempty"`
 	// Endpoint represents the endpoint used to communicate with the control plane.
 	// +optional
 	Endpoint clusterv1.APIEndpoint `json:"endpoint"`
@@ -183,8 +191,15 @@ type GCPManagedControlPlaneStatus struct {
 	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
 
 	// CurrentVersion shows the current version of the GKE control plane.
+	//
+	// Deprecated: This field will soon be removed and you are expected to use Version instead.
+	//
 	// +optional
 	CurrentVersion string `json:"currentVersion,omitempty"`
+
+	// Version represents the version of the GKE control plane.
+	// +optional
+	Version *string `json:"version,omitempty"`
 }
 
 // +kubebuilder:object:root=true
@@ -194,6 +209,7 @@ type GCPManagedControlPlaneStatus struct {
 // +kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".metadata.labels.cluster\\.x-k8s\\.io/cluster-name",description="Cluster to which this GCPManagedControlPlane belongs"
 // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="Control plane is ready"
 // +kubebuilder:printcolumn:name="CurrentVersion",type="string",JSONPath=".status.currentVersion",description="The current Kubernetes version"
+// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version",description="The Kubernetes version of the GKE control plane"
 // +kubebuilder:printcolumn:name="Endpoint",type="string",JSONPath=".spec.endpoint",description="API Endpoint",priority=1
 
 // GCPManagedControlPlane is the Schema for the gcpmanagedcontrolplanes API.
diff --git a/exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go b/exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
index 7835b4143..d9a1ff326 100644
--- a/exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
+++ b/exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
@@ -76,6 +76,7 @@ var _ webhook.Validator = &GCPManagedControlPlane{}
 func (r *GCPManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
 	gcpmanagedcontrolplanelog.Info("validate create", "name", r.Name)
 	var allErrs field.ErrorList
+	var allWarns admission.Warnings
 
 	if len(r.Spec.ClusterName) > maxClusterNameLength {
 		allErrs = append(allErrs,
@@ -98,11 +99,20 @@ func (r *GCPManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
 			r.Spec.LoggingService, "can't be set when autopilot is enabled"))
 	}
 
+	if r.Spec.ControlPlaneVersion != nil {
+		if r.Spec.Version != nil {
+			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ControlPlaneVersion"),
+				r.Spec.LoggingService, "spec.ControlPlaneVersion and spec.Version cannot be set at the same time: please use spec.Version"))
+		} else {
+			allWarns = append(allWarns, "spec.ControlPlaneVersion is deprecated and will soon be removed: please use spec.Version")
+		}
+	}
+
 	if len(allErrs) == 0 {
-		return nil, nil
+		return allWarns, nil
 	}
 
-	return nil, apierrors.NewInvalid(GroupVersion.WithKind("GCPManagedControlPlane").GroupKind(), r.Name, allErrs)
+	return allWarns, apierrors.NewInvalid(GroupVersion.WithKind("GCPManagedControlPlane").GroupKind(), r.Name, allErrs)
 }
 
 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
@@ -149,6 +159,11 @@ func (r *GCPManagedControlPlane) ValidateUpdate(oldRaw runtime.Object) (admissio
 			r.Spec.LoggingService, "can't be set when autopilot is enabled"))
 	}
 
+	if old.Spec.Version != nil && r.Spec.ControlPlaneVersion != nil {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ControlPlaneVersion"),
+			r.Spec.LoggingService, "spec.ControlPlaneVersion and spec.Version cannot be set at the same time: please use spec.Version"))
+	}
+
 	if r.Spec.LoggingService != nil {
 		err := r.Spec.LoggingService.Validate()
 		if err != nil {
diff --git a/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go b/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go
index 1d5ea1208..7a89871ce 100644
--- a/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go
+++ b/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go
@@ -72,21 +72,21 @@ func TestGCPManagedControlPlaneDefaultingWebhook(t *testing.T) {
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_27_1",
-				ControlPlaneVersion: &vV1_27_1,
+				ClusterName: "cluster1_27_1",
+				Version:     &vV1_27_1,
 			},
-			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", ControlPlaneVersion: &vV1_27_1},
+			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", Version: &vV1_27_1},
 		},
 		{
 			name:         "with autopilot enabled",
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_autopilot",
-				ControlPlaneVersion: &vV1_27_1,
-				EnableAutopilot:     true,
+				ClusterName:     "cluster1_autopilot",
+				Version:         &vV1_27_1,
+				EnableAutopilot: true,
 			},
-			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_autopilot", ControlPlaneVersion: &vV1_27_1, EnableAutopilot: true},
+			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_autopilot", Version: &vV1_27_1, EnableAutopilot: true},
 		},
 	}
 
@@ -120,11 +120,13 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) {
 	tests := []struct {
 		name        string
 		expectError bool
+		expectWarn  bool
 		spec        GCPManagedControlPlaneSpec
 	}{
 		{
 			name:        "cluster name too long should cause an error",
 			expectError: true,
+			expectWarn:  false,
 			spec: GCPManagedControlPlaneSpec{
 				ClusterName: strings.Repeat("A", maxClusterNameLength+1),
 			},
@@ -132,6 +134,7 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) {
 		{
 			name:        "autopilot enabled without release channel should cause an error",
 			expectError: true,
+			expectWarn:  false,
 			spec: GCPManagedControlPlaneSpec{
 				ClusterName:     "",
 				EnableAutopilot: true,
@@ -141,12 +144,32 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) {
 		{
 			name:        "autopilot enabled with release channel",
 			expectError: false,
+			expectWarn:  false,
 			spec: GCPManagedControlPlaneSpec{
 				ClusterName:     "",
 				EnableAutopilot: true,
 				ReleaseChannel:  &releaseChannel,
 			},
 		},
+		{
+			name:        "using deprecated ControlPlaneVersion should cause a warning",
+			expectError: false,
+			expectWarn:  true,
+			spec: GCPManagedControlPlaneSpec{
+				ClusterName:         "",
+				ControlPlaneVersion: &vV1_27_1,
+			},
+		},
+		{
+			name:        "using both ControlPlaneVersion and Version should cause a warning and an error",
+			expectError: true,
+			expectWarn:  false,
+			spec: GCPManagedControlPlaneSpec{
+				ClusterName:         "",
+				ControlPlaneVersion: &vV1_27_1,
+				Version:             &vV1_27_1,
+			},
+		},
 	}
 
 	for _, tc := range tests {
@@ -163,8 +186,11 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) {
 			} else {
 				g.Expect(err).ToNot(HaveOccurred())
 			}
-			// Nothing emits warnings yet
-			g.Expect(warn).To(BeEmpty())
+			if tc.expectWarn {
+				g.Expect(warn).ToNot(BeEmpty())
+			} else {
+				g.Expect(warn).To(BeEmpty())
+			}
 		})
 	}
 }
diff --git a/exp/api/v1beta1/zz_generated.deepcopy.go b/exp/api/v1beta1/zz_generated.deepcopy.go
index f14563ca4..73a813a13 100644
--- a/exp/api/v1beta1/zz_generated.deepcopy.go
+++ b/exp/api/v1beta1/zz_generated.deepcopy.go
@@ -307,6 +307,11 @@ func (in *GCPManagedControlPlaneSpec) DeepCopyInto(out *GCPManagedControlPlaneSp
 		*out = new(string)
 		**out = **in
 	}
+	if in.Version != nil {
+		in, out := &in.Version, &out.Version
+		*out = new(string)
+		**out = **in
+	}
 	out.Endpoint = in.Endpoint
 	if in.MasterAuthorizedNetworksConfig != nil {
 		in, out := &in.MasterAuthorizedNetworksConfig, &out.MasterAuthorizedNetworksConfig
@@ -345,6 +350,11 @@ func (in *GCPManagedControlPlaneStatus) DeepCopyInto(out *GCPManagedControlPlane
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.Version != nil {
+		in, out := &in.Version, &out.Version
+		*out = new(string)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneStatus.
diff --git a/test/e2e/data/infrastructure-gcp/cluster-template-ci-gke.yaml b/test/e2e/data/infrastructure-gcp/cluster-template-ci-gke.yaml
index 228acdb52..72c9d4108 100644
--- a/test/e2e/data/infrastructure-gcp/cluster-template-ci-gke.yaml
+++ b/test/e2e/data/infrastructure-gcp/cluster-template-ci-gke.yaml
@@ -33,6 +33,7 @@ metadata:
 spec:
   project: "${GCP_PROJECT}"
   location: "${GCP_REGION}"
+  version: "${KUBERNETES_VERSION}"
   releaseChannel: "regular"
 ---
 apiVersion: cluster.x-k8s.io/v1beta1


Reviews:
user: login: damdo
body: Hey @salasberryfin 
Thanks a lot for working on implementing this!

Mostly LGTM, left a couple of things we need to change though.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-02-25T10:39:54Z
user: login: richardcase
body: Great work @salasberryfin 🎉 

Just a couple of minor comments.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T15:55:49Z
user: login: cpanato
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T12:43:08Z
user: login: alexander-demicev
body: /lgtm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:16:17Z
user: login: cpanato
body: thanks!

/approve

/hold for @richardcase @damdo 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-04T09:23:26Z
user: login: salasberryfin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:41:28Z
user: login: damdo
body: @salasberryfin Mostly LGTM, I left one blocking question though. LMK thanks!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T09:44:15Z
user: login: cpanato
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T09:53:18Z
user: login: richardcase
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T15:44:00Z
user: login: salasberryfin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T08:37:13Z
user: login: salasberryfin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:03:37Z
user: login: damdo
body: /lgtm

Thanks @salasberryfin for working on this

/unhold
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T14:25:09Z

Review comments:
diff_hunk: @@ -147,7 +147,14 @@ type GCPManagedControlPlaneSpec struct {
 	// If not specified, the default version currently supported by GKE will be
 	// used.
 	// +optional
+	//
+	// Deprecated: This field will soon be removed and you are expected to use Version instead.
path: exp/api/v1beta1/gcpmanagedcontrolplane_types.go
user: login: damdo
body: I think the optional stylistically (and maybe even from an implementation standpoint) goes next to the field, whereas the Deprecated can also go above in the godoc comment.
Better be safe, example here:
https://github.com/kubernetes-sigs/cluster-api/blob/7a71f4b0ed71cb92fa751e48057df6bb672319bb/api/v1beta1/cluster_types.go#L533-L539
created_at: 2025-02-25T10:36:07Z
author_association: MEMBER
diff_hunk: @@ -184,7 +191,13 @@ type GCPManagedControlPlaneStatus struct {
 
 	// CurrentVersion shows the current version of the GKE control plane.
 	// +optional
+	//
+	// Deprecated: This field will soon be removed and you are expected to use Version instead.
path: exp/api/v1beta1/gcpmanagedcontrolplane_types.go
user: login: damdo
body: Same here
created_at: 2025-02-25T10:36:22Z
author_association: MEMBER
diff_hunk: @@ -184,7 +191,13 @@ type GCPManagedControlPlaneStatus struct {
 
 	// CurrentVersion shows the current version of the GKE control plane.
 	// +optional
+	//
+	// Deprecated: This field will soon be removed and you are expected to use Version instead.
 	CurrentVersion string `json:"currentVersion,omitempty"`
+
+	// Version represents the version of the GKE control plane.
+	// +optional
+	Version *string `json:"version,omitempty"`
 }
path: exp/api/v1beta1/gcpmanagedcontrolplane_types.go
user: login: damdo
body: We want to populate this `status.Version` with whatever we currently populate the `status.CurrentVersion` with.
Then When in the future we remove the `status.CurrentVersion`, we will only have the `status.Version` to set.

One instance I saw of this is here: https://github.com/kubernetes-sigs/cluster-api-provider-gcp/blob/d80bfabadd3f611f5282d3ee7c2e963f3ffc4621/cloud/services/container/clusters/reconcile.go#L99

But there may be others.

created_at: 2025-02-25T10:38:57Z
author_association: MEMBER
diff_hunk: @@ -506,3 +506,14 @@ func compareMasterAuthorizedNetworksConfig(a, b *containerpb.MasterAuthorizedNet
 	}
 	return true
 }
+
+// get the desired master version
+func getControlPlaneVersionFromSpec(spec *infrav1exp.GCPManagedControlPlaneSpec) *string {
path: cloud/services/container/clusters/reconcile.go
user: login: richardcase
body: This could live as a function on the scope to make it a little neater.
created_at: 2025-02-27T15:51:52Z
author_association: MEMBER
diff_hunk: @@ -98,6 +98,11 @@ func (r *GCPManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
 			r.Spec.LoggingService, "can't be set when autopilot is enabled"))
 	}
 
+	if r.Spec.ControlPlaneVersion != nil && r.Spec.Version != nil {
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
user: login: richardcase
body: It might also be worth adding an admission warning if `r.Spec.ControlPlaneVersion != nil` to say that its deprecated and its advised to use spec.Version instead.
created_at: 2025-02-27T15:55:07Z
author_association: MEMBER
diff_hunk: @@ -72,21 +72,21 @@ func TestGCPManagedControlPlaneDefaultingWebhook(t *testing.T) {
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_27_1",
-				ControlPlaneVersion: &vV1_27_1,
+				ClusterName: "cluster1_27_1",
+				Version:     &vV1_27_1,
 			},
-			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", ControlPlaneVersion: &vV1_27_1},
+			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", Version: &vV1_27_1},
 		},
 		{
 			name:         "with autopilot enabled",
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_autopilot",
-				ControlPlaneVersion: &vV1_27_1,
-				EnableAutopilot:     true,
+				ClusterName:     "cluster1_autopilot",
+				Version:         &vV1_27_1,
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go
user: login: cpanato
body: can be this updated?
created_at: 2025-03-03T12:43:08Z
author_association: MEMBER
diff_hunk: @@ -72,21 +72,21 @@ func TestGCPManagedControlPlaneDefaultingWebhook(t *testing.T) {
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_27_1",
-				ControlPlaneVersion: &vV1_27_1,
+				ClusterName: "cluster1_27_1",
+				Version:     &vV1_27_1,
 			},
-			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", ControlPlaneVersion: &vV1_27_1},
+			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", Version: &vV1_27_1},
 		},
 		{
 			name:         "with autopilot enabled",
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_autopilot",
-				ControlPlaneVersion: &vV1_27_1,
-				EnableAutopilot:     true,
+				ClusterName:     "cluster1_autopilot",
+				Version:         &vV1_27_1,
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go
user: login: salasberryfin
body: I'll update this in a separate PR, if that's okay.
created_at: 2025-03-04T09:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,6 +98,15 @@ func (r *GCPManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
 			r.Spec.LoggingService, "can't be set when autopilot is enabled"))
 	}
 
+	if r.Spec.ControlPlaneVersion != nil {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ControlPlaneVersion"),
+			r.Spec.LoggingService, "spec.ControlPlaneVersion is deprecated and will soon be removed: please use spec.Version"))
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
user: login: damdo
body: @salasberryfin Does this result in blocking any `GCPManagedControlPlane` CREATE if the `spec.ControlPlaneVersion` is set?
If so, I think this would be an implicit API breaking change, we should probably warn yes, but not error, at least until we remove the field from the API, when we bump it to v1beta2.

cc. @richardcase 
created_at: 2025-03-04T09:42:28Z
author_association: MEMBER
diff_hunk: @@ -72,21 +72,21 @@ func TestGCPManagedControlPlaneDefaultingWebhook(t *testing.T) {
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_27_1",
-				ControlPlaneVersion: &vV1_27_1,
+				ClusterName: "cluster1_27_1",
+				Version:     &vV1_27_1,
 			},
-			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", ControlPlaneVersion: &vV1_27_1},
+			expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", Version: &vV1_27_1},
 		},
 		{
 			name:         "with autopilot enabled",
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			spec: GCPManagedControlPlaneSpec{
-				ClusterName:         "cluster1_autopilot",
-				ControlPlaneVersion: &vV1_27_1,
-				EnableAutopilot:     true,
+				ClusterName:     "cluster1_autopilot",
+				Version:         &vV1_27_1,
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go
user: login: cpanato
body: yes
created_at: 2025-03-04T09:53:18Z
author_association: MEMBER
diff_hunk: @@ -479,6 +481,17 @@ func (s *Service) checkDiffAndPrepareUpdate(existingCluster *containerpb.Cluster
 	return needUpdate, &updateClusterRequest
 }
 
+// get the desired master version
+func (s *Service) getControlPlaneVersionFromSpec() *string {
path: cloud/services/container/clusters/reconcile.go
user: login: richardcase
body: We should probably add this to the scope instead. The reason being it can be re-used anywhere that a version is needed. 
created_at: 2025-03-04T15:41:31Z
author_association: MEMBER
diff_hunk: @@ -98,6 +98,15 @@ func (r *GCPManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
 			r.Spec.LoggingService, "can't be set when autopilot is enabled"))
 	}
 
+	if r.Spec.ControlPlaneVersion != nil {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ControlPlaneVersion"),
+			r.Spec.LoggingService, "spec.ControlPlaneVersion is deprecated and will soon be removed: please use spec.Version"))
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
user: login: richardcase
body: I think this needs to be changed to an admission warning instead.
created_at: 2025-03-04T15:43:03Z
author_association: MEMBER
diff_hunk: @@ -98,6 +98,15 @@ func (r *GCPManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
 			r.Spec.LoggingService, "can't be set when autopilot is enabled"))
 	}
 
+	if r.Spec.ControlPlaneVersion != nil {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ControlPlaneVersion"),
+			r.Spec.LoggingService, "spec.ControlPlaneVersion is deprecated and will soon be removed: please use spec.Version"))
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
user: login: richardcase
body: Returning an error would be a breaking change.
created_at: 2025-03-04T15:43:43Z
author_association: MEMBER
diff_hunk: @@ -98,6 +98,15 @@ func (r *GCPManagedControlPlane) ValidateCreate() (admission.Warnings, error) {
 			r.Spec.LoggingService, "can't be set when autopilot is enabled"))
 	}
 
+	if r.Spec.ControlPlaneVersion != nil {
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ControlPlaneVersion"),
+			r.Spec.LoggingService, "spec.ControlPlaneVersion is deprecated and will soon be removed: please use spec.Version"))
path: exp/api/v1beta1/gcpmanagedcontrolplane_webhook.go
user: login: salasberryfin
body: Thanks for catching this!
created_at: 2025-03-05T08:37:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -479,6 +481,17 @@ func (s *Service) checkDiffAndPrepareUpdate(existingCluster *containerpb.Cluster
 	return needUpdate, &updateClusterRequest
 }
 
+// get the desired master version
+func (s *Service) getControlPlaneVersionFromSpec() *string {
path: cloud/services/container/clusters/reconcile.go
user: login: salasberryfin
body: Sorry I did not get this in the original review. Should be fixed now!
created_at: 2025-03-05T09:03:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-02-25T10:10:09Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *kubernetes-sigs-cluster-api-gcp* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | af6413dd1251e5dfd85bd56812818564b8b3cf70 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kubernetes-sigs-cluster-api-gcp/deploys/67c815d20809c5000860a17f |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1434--kubernetes-sigs-cluster-api-gcp.netlify.app](https://deploy-preview-1434--kubernetes-sigs-cluster-api-gcp.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE0MzQtLWt1YmVybmV0ZXMtc2lncy1jbHVzdGVyLWFwaS1nY3AubmV0bGlmeS5hcHAifQ.t0Mo20rORpy8jBs6-6wAgDmjpm4OQYYund6G1gIm8QA)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [kubernetes-sigs-cluster-api-gcp Preview](https://deploy-preview-1434--kubernetes-sigs-cluster-api-gcp.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/kubernetes-sigs-cluster-api-gcp/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: damdo
created_at: 2025-02-25T10:42:35Z
body: @salasberryfin Also we will likely need to add an `action required` in the release-note for this PR,
similar to the one I added for CAPA here: https://github.com/kubernetes-sigs/cluster-api-provider-aws/pull/5209#issue-2637715307
(you should be able to mostly copy-paste it).

cc. @richardcase 
author_association: MEMBER
user: login: damdo
created_at: 2025-02-26T15:06:32Z
body: @salasberryfin also we might need to silence the linter for [errors like](https://github.com/kubernetes-sigs/cluster-api-provider-gcp/actions/runs/13518827906/job/37773293001?pr=1434) : `cloud/services/container/clusters/reconcile.go:515:5: SA1019: spec.ControlPlaneVersion is deprecated: This field will soon be removed and you are expected to use Version instead. (staticcheck)`, as we still want to set/get those fields internally even if they are deprecated, until we remove them.
author_association: MEMBER
user: login: salasberryfin
created_at: 2025-02-28T10:46:28Z
body: Thanks for the reviews @damdo @richardcase. Sorry I couldn't address your comments before, I'll be doing so today!
author_association: CONTRIBUTOR
user: login: damdo
created_at: 2025-02-28T11:07:13Z
body: No worries, thanks @salasberryfin 
author_association: MEMBER
user: login: salasberryfin
created_at: 2025-03-03T10:54:22Z
body: I think the latest commit addresses all of the comments @damdo @richardcase.
author_association: CONTRIBUTOR
user: login: cpanato
created_at: 2025-03-03T10:57:43Z
body: @salasberryfin there are a few lints to tackle, thanks for working on this
author_association: MEMBER
user: login: salasberryfin
created_at: 2025-03-03T11:01:40Z
body: Thanks @cpanato. The exclude rule I added covered some of the cases where the linter found errors but missed some of them. Should be fixed now.
author_association: CONTRIBUTOR
user: login: salasberryfin
created_at: 2025-03-03T15:04:58Z
body: Also added the `action required` to the release note. Thanks @damdo!
author_association: CONTRIBUTOR
user: login: k8s-ci-robot
created_at: 2025-03-04T09:23:32Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api-provider-gcp/pull/1434#pullrequestreview-2656592216" title="LGTM">alexander-demicev</a>*, *<a href="https://github.com/kubernetes-sigs/cluster-api-provider-gcp/pull/1434#pullrequestreview-2656616776" title="Approved">cpanato</a>*, *<a href="https://github.com/kubernetes-sigs/cluster-api-provider-gcp/pull/1434#" title="Author self-approved">salasberryfin</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api-provider-gcp).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/cluster-api-provider-gcp/blob/main/OWNERS)~~ [cpanato]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: salasberryfin
created_at: 2025-03-05T09:03:55Z
body: Thanks for the reviews. The webhook now returns a warning if only `spec.controlPlaneVersion` is used and an error if both `spec.controlPlaneVersion` and `spec.Version` are set. New tests have been added to verify the validation webhook on these scenarios.
author_association: CONTRIBUTOR
user: login: damdo
created_at: 2025-03-05T10:04:50Z
body: Hm looks like the E2Es failed https://prow.k8s.io/view/gs/kubernetes-ci-logs/pr-logs/pull/kubernetes-sigs_cluster-api-provider-gcp/1434/pull-cluster-api-provider-gcp-e2e-test/1897214121795915776

Let's retest for good measure in case it is a flake, otherwise we might need to look into it.

/retest 


author_association: MEMBER
user: login: salasberryfin
created_at: 2025-03-05T10:44:50Z
body: Thanks @damdo, looks like it was a flake.
author_association: CONTRIBUTOR
user: login: richardcase
created_at: 2025-03-05T16:06:06Z
body: Great work @salasberryfin 🥇 
author_association: MEMBER