Repository: vmware-tanzu/tanzu-framework (language: go)

Issue:
Support ClusterClass based Cluster Upgrade
(This is used to request new product features)

**Describe the feature request**
Request is to support ClusterClass based cluster upgrades.

Currently, we support creating management and workload cluster using ClusterClass when `package-based-lcm` feature-flag is enabled. Along with cluster creation we should also support the cluster upgrade as well.

**Describe alternatives you've considered**
None

**Affected product area (please put an X in all that apply)**

- [ ] APIs
- [ ] Addons
- [x] CLI
- [ ] Docs
- [ ] IAM
- [ ] Installation
- [ ] Plugin
- [ ] Security
- [ ] Test and Release
- [ ] User Experience

**Additional context**



Discussion / hints:

Fix patch:
diff --git a/pkg/v1/tkg/client/upgrade_cluster.go b/pkg/v1/tkg/client/upgrade_cluster.go
index cfde8c0dcd..ce18f0bc99 100644
--- a/pkg/v1/tkg/client/upgrade_cluster.go
+++ b/pkg/v1/tkg/client/upgrade_cluster.go
@@ -117,7 +117,7 @@ type ClusterUpgradeInfo struct {
 // 5. Wait for k8s version to be updated for the cluster
 // 6. Patch MachineDeployment object to upgrade worker nodes
 // 7. Wait for k8s version to be updated for all worker nodes
-func (c *TkgClient) UpgradeCluster(options *UpgradeClusterOptions) error { // nolint:funlen,gocyclo
+func (c *TkgClient) UpgradeCluster(options *UpgradeClusterOptions) error {
 	if options == nil {
 		return errors.New("invalid upgrade cluster options nil")
 	}
@@ -177,7 +177,21 @@ func (c *TkgClient) UpgradeCluster(options *UpgradeClusterOptions) error { // no
 		}
 	}
 
-	err = c.addKubernetesReleaseLabel(regionalClusterClient, options)
+	// Check if Cluster is ClusterClass based cluster or not
+	isClusterClassBased, err := regionalClusterClient.IsClusterClassBased(options.ClusterName, options.Namespace)
+	if err != nil {
+		return errors.Wrap(err, "unable to determine cluster type")
+	}
+
+	// If cluster is ClusterClass based cluster upgrade the cluster with different path
+	if isClusterClassBased {
+		return c.DoClassyClusterUpgrade(regionalClusterClient, currentClusterClient, options)
+	}
+	return c.DoLegacyClusterUpgrade(regionalClusterClient, currentClusterClient, options)
+}
+
+func (c *TkgClient) DoLegacyClusterUpgrade(regionalClusterClient, currentClusterClient clusterclient.Client, options *UpgradeClusterOptions) error {
+	err := c.addKubernetesReleaseLabel(regionalClusterClient, options)
 	if err != nil {
 		return errors.Wrapf(err, "unable to patch the cluster object with TanzuKubernetesRelease label")
 	}
diff --git a/pkg/v1/tkg/client/upgrade_cluster_clusterclass.go b/pkg/v1/tkg/client/upgrade_cluster_clusterclass.go
new file mode 100644
index 0000000000..d0bcd7f4f7
--- /dev/null
+++ b/pkg/v1/tkg/client/upgrade_cluster_clusterclass.go
@@ -0,0 +1,41 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package client
+
+import (
+	"fmt"
+
+	"github.com/pkg/errors"
+
+	"github.com/vmware-tanzu/tanzu-framework/pkg/v1/tkg/clusterclient"
+	"github.com/vmware-tanzu/tanzu-framework/pkg/v1/tkg/log"
+)
+
+func (c *TkgClient) DoClassyClusterUpgrade(regionalClusterClient clusterclient.Client,
+	currentClusterClient clusterclient.Client, options *UpgradeClusterOptions) error {
+
+	kubernetesVersion := options.KubernetesVersion
+
+	log.Infof("Upgrading kubernetes cluster to `%v` version", kubernetesVersion)
+	patchJSONString := fmt.Sprintf(`{"spec": {"topology": {"version": "%v"}}}`, kubernetesVersion)
+
+	err := regionalClusterClient.PatchClusterObject(options.ClusterName, options.Namespace, patchJSONString)
+	if err != nil {
+		return errors.Wrap(err, "unable to patch kubernetes version to cluster")
+	}
+
+	log.Info("Waiting for kubernetes version to be updated for control plane nodes...")
+	err = regionalClusterClient.WaitK8sVersionUpdateForCPNodes(options.ClusterName, options.Namespace, kubernetesVersion, currentClusterClient)
+	if err != nil {
+		return errors.Wrap(err, "error waiting for kubernetes version update for kubeadm control plane")
+	}
+
+	log.Info("Waiting for kubernetes version to be updated for worker nodes...")
+	err = regionalClusterClient.WaitK8sVersionUpdateForWorkerNodes(options.ClusterName, options.Namespace, kubernetesVersion, currentClusterClient)
+	if err != nil {
+		return errors.Wrap(err, "error waiting for kubernetes version update for worker nodes")
+	}
+
+	return nil
+}
diff --git a/pkg/v1/tkg/clusterclient/clusterclient.go b/pkg/v1/tkg/clusterclient/clusterclient.go
index 3f1895f59c..9acb2677b6 100644
--- a/pkg/v1/tkg/clusterclient/clusterclient.go
+++ b/pkg/v1/tkg/clusterclient/clusterclient.go
@@ -321,6 +321,8 @@ type Client interface {
 	ListCLIPluginResources() ([]cliv1alpha1.CLIPlugin, error)
 	// VerifyCLIPluginCRD returns true if CRD exists else return false
 	VerifyCLIPluginCRD() (bool, error)
+	// IsClusterClassBased check whether cluster is ClusterClass based or not
+	IsClusterClassBased(clusterName, namespace string) (bool, error)
 }
 
 // PollOptions is options for polling
@@ -2341,6 +2343,18 @@ func (c *client) ListCLIPluginResources() ([]cliv1alpha1.CLIPlugin, error) {
 	return cliPlugins.Items, nil
 }
 
+// IsClusterClassBased check whether cluster is ClusterClass based or not
+func (c *client) IsClusterClassBased(clusterName, namespace string) (bool, error) {
+	clusterObj := &capi.Cluster{}
+	if err := c.GetResource(clusterObj, clusterName, namespace, nil, nil); err != nil {
+		return false, err
+	}
+	if clusterObj.Spec.Topology.Class == "" {
+		return false, nil
+	}
+	return true, nil
+}
+
 // Options provides way to customize creation of clusterClient
 type Options struct {
 	poller                    Poller
diff --git a/pkg/v1/tkg/fakes/clusterclient.go b/pkg/v1/tkg/fakes/clusterclient.go
index 070b192002..b9614eeeb8 100644
--- a/pkg/v1/tkg/fakes/clusterclient.go
+++ b/pkg/v1/tkg/fakes/clusterclient.go
@@ -553,6 +553,20 @@ type ClusterClient struct {
 		result1 bool
 		result2 error
 	}
+	IsClusterClassBasedStub        func(string, string) (bool, error)
+	isClusterClassBasedMutex       sync.RWMutex
+	isClusterClassBasedArgsForCall []struct {
+		arg1 string
+		arg2 string
+	}
+	isClusterClassBasedReturns struct {
+		result1 bool
+		result2 error
+	}
+	isClusterClassBasedReturnsOnCall map[int]struct {
+		result1 bool
+		result2 error
+	}
 	IsClusterRegisteredToTMCStub        func() (bool, error)
 	isClusterRegisteredToTMCMutex       sync.RWMutex
 	isClusterRegisteredToTMCArgsForCall []struct {
@@ -3659,6 +3673,71 @@ func (fake *ClusterClient) HasCEIPTelemetryJobReturnsOnCall(i int, result1 bool,
 	}{result1, result2}
 }
 
+func (fake *ClusterClient) IsClusterClassBased(arg1 string, arg2 string) (bool, error) {
+	fake.isClusterClassBasedMutex.Lock()
+	ret, specificReturn := fake.isClusterClassBasedReturnsOnCall[len(fake.isClusterClassBasedArgsForCall)]
+	fake.isClusterClassBasedArgsForCall = append(fake.isClusterClassBasedArgsForCall, struct {
+		arg1 string
+		arg2 string
+	}{arg1, arg2})
+	stub := fake.IsClusterClassBasedStub
+	fakeReturns := fake.isClusterClassBasedReturns
+	fake.recordInvocation("IsClusterClassBased", []interface{}{arg1, arg2})
+	fake.isClusterClassBasedMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *ClusterClient) IsClusterClassBasedCallCount() int {
+	fake.isClusterClassBasedMutex.RLock()
+	defer fake.isClusterClassBasedMutex.RUnlock()
+	return len(fake.isClusterClassBasedArgsForCall)
+}
+
+func (fake *ClusterClient) IsClusterClassBasedCalls(stub func(string, string) (bool, error)) {
+	fake.isClusterClassBasedMutex.Lock()
+	defer fake.isClusterClassBasedMutex.Unlock()
+	fake.IsClusterClassBasedStub = stub
+}
+
+func (fake *ClusterClient) IsClusterClassBasedArgsForCall(i int) (string, string) {
+	fake.isClusterClassBasedMutex.RLock()
+	defer fake.isClusterClassBasedMutex.RUnlock()
+	argsForCall := fake.isClusterClassBasedArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *ClusterClient) IsClusterClassBasedReturns(result1 bool, result2 error) {
+	fake.isClusterClassBasedMutex.Lock()
+	defer fake.isClusterClassBasedMutex.Unlock()
+	fake.IsClusterClassBasedStub = nil
+	fake.isClusterClassBasedReturns = struct {
+		result1 bool
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *ClusterClient) IsClusterClassBasedReturnsOnCall(i int, result1 bool, result2 error) {
+	fake.isClusterClassBasedMutex.Lock()
+	defer fake.isClusterClassBasedMutex.Unlock()
+	fake.IsClusterClassBasedStub = nil
+	if fake.isClusterClassBasedReturnsOnCall == nil {
+		fake.isClusterClassBasedReturnsOnCall = make(map[int]struct {
+			result1 bool
+			result2 error
+		})
+	}
+	fake.isClusterClassBasedReturnsOnCall[i] = struct {
+		result1 bool
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *ClusterClient) IsClusterRegisteredToTMC() (bool, error) {
 	fake.isClusterRegisteredToTMCMutex.Lock()
 	ret, specificReturn := fake.isClusterRegisteredToTMCReturnsOnCall[len(fake.isClusterRegisteredToTMCArgsForCall)]
@@ -6603,6 +6682,8 @@ func (fake *ClusterClient) Invocations() map[string][][]interface{} {
 	defer fake.getVCServerMutex.RUnlock()
 	fake.hasCEIPTelemetryJobMutex.RLock()
 	defer fake.hasCEIPTelemetryJobMutex.RUnlock()
+	fake.isClusterClassBasedMutex.RLock()
+	defer fake.isClusterClassBasedMutex.RUnlock()
 	fake.isClusterRegisteredToTMCMutex.RLock()
 	defer fake.isClusterRegisteredToTMCMutex.RUnlock()
 	fake.isPacificRegionalClusterMutex.RLock()
diff --git a/pkg/v1/tkg/fakes/helper/fakeobjectcreater.go b/pkg/v1/tkg/fakes/helper/fakeobjectcreater.go
index 559e125bf6..6bd1228f8f 100644
--- a/pkg/v1/tkg/fakes/helper/fakeobjectcreater.go
+++ b/pkg/v1/tkg/fakes/helper/fakeobjectcreater.go
@@ -73,6 +73,10 @@ func NewCluster(options TestAllClusterComponentOptions) *capi.Cluster {
 			Name:       "kcp-" + options.ClusterName,
 			APIVersion: controlplanev1.GroupVersion.String(),
 		},
+		Topology: &capi.Topology{
+			Class:   options.ClusterTopology.Class,
+			Version: options.ClusterTopology.Version,
+		},
 	}
 	cluster.Status = capi.ClusterStatus{
 		Phase:               options.ClusterOptions.Phase,
diff --git a/pkg/v1/tkg/fakes/helper/types.go b/pkg/v1/tkg/fakes/helper/types.go
index c482c3eb35..8d4aa90628 100644
--- a/pkg/v1/tkg/fakes/helper/types.go
+++ b/pkg/v1/tkg/fakes/helper/types.go
@@ -27,6 +27,7 @@ type TestAllClusterComponentOptions struct {
 	ListMDOptions               []TestMDOptions
 	MachineOptions              []TestMachineOptions
 	ClusterConfigurationOptions TestClusterConfiguration
+	ClusterTopology             TestClusterTopology
 	InfraComponentsOptions      TestInfraComponentsOptions
 }
 
@@ -49,6 +50,11 @@ type TestClusterConfiguration struct {
 	EtcdImageTag        string
 }
 
+type TestClusterTopology struct {
+	Class   string
+	Version string
+}
+
 // TestClusterOptions describes options for CAPI/TKC cluster
 type TestClusterOptions struct {
 	Phase                   string


Test patch:
diff --git a/pkg/v1/tkg/client/upgrade_cluster_test.go b/pkg/v1/tkg/client/upgrade_cluster_test.go
index 3ede2a693c..5a131c0d3b 100644
--- a/pkg/v1/tkg/client/upgrade_cluster_test.go
+++ b/pkg/v1/tkg/client/upgrade_cluster_test.go
@@ -103,7 +103,7 @@ owner: root:root
 path: /etc/kubernetes/manifests/kube-vip.yaml`
 )
 
-var _ = Describe("Unit tests for upgrade cluster", func() {
+var _ = Describe("Unit tests for upgrading legacy cluster", func() {
 	var (
 		err                   error
 		regionalClusterClient *fakes.ClusterClient
@@ -635,217 +635,374 @@ var _ = Describe("When upgrading cluster with fake controller runtime client", f
 			SkipAddonUpgrade:    true,
 		}
 	})
+	Describe("When upgrading a legacy cluster with fake controller runtime client", func() {
+		BeforeEach(func() {
+			newK8sVersion = "v1.18.0+vmware.1"
+			currentK8sVersion = "v1.17.3+vmware.2"
+			setupBomFile("../fakes/config/bom/tkg-bom-v1.3.1.yaml", testingDir)
+			os.Setenv("SKIP_VSPHERE_TEMPLATE_VERIFICATION", "1")
 
-	BeforeEach(func() {
-		newK8sVersion = "v1.18.0+vmware.1"
-		currentK8sVersion = "v1.17.3+vmware.2"
-		setupBomFile("../fakes/config/bom/tkg-bom-v1.3.1.yaml", testingDir)
-		os.Setenv("SKIP_VSPHERE_TEMPLATE_VERIFICATION", "1")
-
-		regionalClusterOptions = fakehelper.TestAllClusterComponentOptions{
-			ClusterName: "cluster-1",
-			Namespace:   constants.DefaultNamespace,
-			Labels: map[string]string{
-				TkgLabelClusterRolePrefix + TkgLabelClusterRoleWorkload: "",
-			},
-			ClusterOptions: fakehelper.TestClusterOptions{
-				Phase:                   "provisioned",
-				InfrastructureReady:     true,
-				ControlPlaneInitialized: true,
-				ControlPlaneReady:       true,
-			},
-			CPOptions: fakehelper.TestCPOptions{
-				SpecReplicas:    3,
-				ReadyReplicas:   3,
-				UpdatedReplicas: 3,
-				Replicas:        3,
-				K8sVersion:      "v1.18.2+vmware.1",
-				InfrastructureTemplate: fakehelper.TestObject{
-					Kind:      constants.VSphereMachineTemplate,
-					Name:      "cluster-1-control-plane",
-					Namespace: constants.DefaultNamespace,
+			regionalClusterOptions = fakehelper.TestAllClusterComponentOptions{
+				ClusterName: "cluster-1",
+				Namespace:   constants.DefaultNamespace,
+				Labels: map[string]string{
+					TkgLabelClusterRolePrefix + TkgLabelClusterRoleWorkload: "",
 				},
-			},
-			ListMDOptions: fakehelper.GetListMDOptionsFromMDOptions(fakehelper.TestMDOptions{
-				SpecReplicas:    3,
-				ReadyReplicas:   3,
-				UpdatedReplicas: 3,
-				Replicas:        3,
-				InfrastructureTemplate: fakehelper.TestObject{
-					Kind:      constants.VSphereMachineTemplate,
-					Name:      "cluster-1-md-0",
-					Namespace: constants.DefaultNamespace,
+				ClusterOptions: fakehelper.TestClusterOptions{
+					Phase:                   "provisioned",
+					InfrastructureReady:     true,
+					ControlPlaneInitialized: true,
+					ControlPlaneReady:       true,
 				},
-			}),
-			ClusterConfigurationOptions: fakehelper.TestClusterConfiguration{
-				ImageRepository:     "fake.image.repository",
-				DNSImageRepository:  "fake.image.repository",
-				DNSImageTag:         "v1.6.7_vmware.1",
-				EtcdImageRepository: "fake.image.repository",
-				EtcdImageTag:        "v3.4.3_vmware.5",
-			},
-			MachineOptions: []fakehelper.TestMachineOptions{
-				{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
-				{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
-				{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
-				{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
-				{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
-				{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
-			},
-		}
-	})
-	JustBeforeEach(func() {
-		configureTKGClient()
-		err = tkgClient.DoClusterUpgrade(regionalClusterClient, currentClusterClient, &upgradeClusterOptions)
-	})
+				CPOptions: fakehelper.TestCPOptions{
+					SpecReplicas:    3,
+					ReadyReplicas:   3,
+					UpdatedReplicas: 3,
+					Replicas:        3,
+					K8sVersion:      "v1.18.2+vmware.1",
+					InfrastructureTemplate: fakehelper.TestObject{
+						Kind:      constants.VSphereMachineTemplate,
+						Name:      "cluster-1-control-plane",
+						Namespace: constants.DefaultNamespace,
+					},
+				},
+				ListMDOptions: fakehelper.GetListMDOptionsFromMDOptions(fakehelper.TestMDOptions{
+					SpecReplicas:    3,
+					ReadyReplicas:   3,
+					UpdatedReplicas: 3,
+					Replicas:        3,
+					InfrastructureTemplate: fakehelper.TestObject{
+						Kind:      constants.VSphereMachineTemplate,
+						Name:      "cluster-1-md-0",
+						Namespace: constants.DefaultNamespace,
+					},
+				}),
+				ClusterConfigurationOptions: fakehelper.TestClusterConfiguration{
+					ImageRepository:     "fake.image.repository",
+					DNSImageRepository:  "fake.image.repository",
+					DNSImageTag:         "v1.6.7_vmware.1",
+					EtcdImageRepository: "fake.image.repository",
+					EtcdImageTag:        "v3.4.3_vmware.5",
+				},
+				MachineOptions: []fakehelper.TestMachineOptions{
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
+				},
+			}
+		})
 
-	// Context("When get current k8s version < new version of cluster only in +vmware.<version>", func() {
-	// 	BeforeEach(func() {
-	// 		upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.1"
-	// 		currentClusterK8sVersion = "v1.18.0+vmware.0"
-	// 	})
-	// 	It("should not return an error", func() {
-	// 		Expect(err).NotTo(HaveOccurred())
-	// 	})
-	// })
-	Context("When get current k8s version > new version of cluster only in +vmware.<version>", func() {
-		BeforeEach(func() {
-			upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.1"
-			currentClusterK8sVersion = "v1.18.0+vmware.2"
+		JustBeforeEach(func() {
+			configureTKGClient()
+			err = tkgClient.DoClusterUpgrade(regionalClusterClient, currentClusterClient, &upgradeClusterOptions)
 		})
-		It("should return an error", func() {
-			Expect(err).To(HaveOccurred())
-			Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.0+vmware.2 to v1.18.0+vmware.1. Kubernetes version downgrade is not allowed."))
+
+		// Context("When get current k8s version < new version of cluster only in +vmware.<version>", func() {
+		// 	BeforeEach(func() {
+		// 		upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.1"
+		// 		currentClusterK8sVersion = "v1.18.0+vmware.0"
+		// 	})
+		// 	It("should not return an error", func() {
+		// 		Expect(err).NotTo(HaveOccurred())
+		// 	})
+		// })
+		Context("When get current k8s version > new version of cluster only in +vmware.<version>", func() {
+			BeforeEach(func() {
+				upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.1"
+				currentClusterK8sVersion = "v1.18.0+vmware.2"
+			})
+			It("should return an error", func() {
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.0+vmware.2 to v1.18.0+vmware.1. Kubernetes version downgrade is not allowed."))
+			})
 		})
-	})
-	Context("When get current k8s version > new version of cluster only in +vmware.<version>", func() {
-		BeforeEach(func() {
-			upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.2"
-			currentClusterK8sVersion = "v1.18.0+vmware.11"
+		Context("When get current k8s version > new version of cluster only in +vmware.<version>", func() {
+			BeforeEach(func() {
+				upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.2"
+				currentClusterK8sVersion = "v1.18.0+vmware.11"
+			})
+			It("should return an error", func() {
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.0+vmware.11 to v1.18.0+vmware.2. Kubernetes version downgrade is not allowed."))
+			})
 		})
-		It("should return an error", func() {
-			Expect(err).To(HaveOccurred())
-			Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.0+vmware.11 to v1.18.0+vmware.2. Kubernetes version downgrade is not allowed."))
+		Context("When get current k8s version > new version of cluster only in +vmware.<version>", func() {
+			BeforeEach(func() {
+				upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.2"
+				currentClusterK8sVersion = "v2.18.0+vmware.11"
+			})
+			It("should return an error", func() {
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v2.18.0+vmware.11 to v1.18.0+vmware.2. Kubernetes version downgrade is not allowed."))
+			})
 		})
-	})
-	Context("When get current k8s version > new version of cluster only in +vmware.<version>", func() {
-		BeforeEach(func() {
-			upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.2"
-			currentClusterK8sVersion = "v2.18.0+vmware.11"
+		// Context("When get current k8s version == new version of cluster", func() {
+		// 	BeforeEach(func() {
+		// 		upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.1"
+		// 		currentClusterK8sVersion = "v1.18.0+vmware.1"
+		// 	})
+		// 	It("should not return an error", func() {
+		// 		Expect(err).NotTo(HaveOccurred())
+		// 	})
+		// })
+		// Context("When get current k8s version < new version of cluster", func() {
+		// 	BeforeEach(func() {
+		// 		currentClusterK8sVersion = "v1.18.0+vmware.0"
+		// 	})
+		// 	It("should not return an error", func() {
+		// 		Expect(err).NotTo(HaveOccurred())
+		// 	})
+		// })
+		Context("When get current k8s version > new version of cluster", func() {
+			BeforeEach(func() {
+				currentClusterK8sVersion = "v1.18.0+vmware.4"
+			})
+			It("returns an error", func() {
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.0+vmware.4 to v1.18.0+vmware.1. Kubernetes version downgrade is not allowed."))
+			})
 		})
-		It("should return an error", func() {
-			Expect(err).To(HaveOccurred())
-			Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v2.18.0+vmware.11 to v1.18.0+vmware.2. Kubernetes version downgrade is not allowed."))
+		Context("When get current k8s version > new version of cluster", func() {
+			BeforeEach(func() {
+				currentClusterK8sVersion = "v1.18.5+vmware.1"
+			})
+			It("returns an error", func() {
+				Expect(err).To(HaveOccurred())
+				Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.5+vmware.1 to v1.18.0+vmware.1. Kubernetes version downgrade is not allowed."))
+			})
 		})
-	})
-	// Context("When get current k8s version == new version of cluster", func() {
-	// 	BeforeEach(func() {
-	// 		upgradeClusterOptions.KubernetesVersion = "v1.18.0+vmware.1"
-	// 		currentClusterK8sVersion = "v1.18.0+vmware.1"
-	// 	})
-	// 	It("should not return an error", func() {
-	// 		Expect(err).NotTo(HaveOccurred())
-	// 	})
-	// })
-	// Context("When get current k8s version < new version of cluster", func() {
-	// 	BeforeEach(func() {
-	// 		currentClusterK8sVersion = "v1.18.0+vmware.0"
-	// 	})
-	// 	It("should not return an error", func() {
-	// 		Expect(err).NotTo(HaveOccurred())
-	// 	})
-	// })
-	Context("When get current k8s version > new version of cluster", func() {
-		BeforeEach(func() {
-			currentClusterK8sVersion = "v1.18.0+vmware.4"
+
+		var _ = Describe("Test PatchKubernetesVersionToKubeadmControlPlane", func() {
+			Context("Testing EtcdExtraArgs parameter configuration", func() {
+				It("when EtcdExtraArgs is defined", func() {
+					clusterUpgradeConfig := &ClusterUpgradeInfo{
+						ClusterName:      "cluster-1",
+						ClusterNamespace: constants.DefaultNamespace,
+						UpgradeComponentInfo: ComponentInfo{
+							EtcdExtraArgs:     map[string]string{"fake-arg": "fake-arg-value"},
+							KubernetesVersion: "v1.18.0+vmware.2",
+						},
+						ActualComponentInfo: ComponentInfo{
+							KubernetesVersion: "v1.18.0+vmware.1",
+						},
+					}
+
+					err = tkgClient.PatchKubernetesVersionToKubeadmControlPlane(regionalClusterClient, clusterUpgradeConfig)
+					Expect(err).To(BeNil())
+
+					updatedKCP, err := regionalClusterClient.GetKCPObjectForCluster(clusterUpgradeConfig.ClusterName, clusterUpgradeConfig.ClusterNamespace)
+					Expect(err).To(BeNil())
+					Expect(updatedKCP.ObjectMeta.Name).To(Equal("kcp-cluster-1"))
+					Expect(updatedKCP.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ExtraArgs["fake-arg"]).To(Equal("fake-arg-value"))
+				})
+
+				It("when EtcdExtraArgs is empty", func() {
+					clusterUpgradeConfig := &ClusterUpgradeInfo{
+						ClusterName:      "cluster-1",
+						ClusterNamespace: constants.DefaultNamespace,
+						UpgradeComponentInfo: ComponentInfo{
+							EtcdExtraArgs:     map[string]string{},
+							KubernetesVersion: "v1.18.0+vmware.2",
+						},
+						ActualComponentInfo: ComponentInfo{
+							KubernetesVersion: "v1.18.0+vmware.1",
+						},
+					}
+
+					err = tkgClient.PatchKubernetesVersionToKubeadmControlPlane(regionalClusterClient, clusterUpgradeConfig)
+					Expect(err).To(BeNil())
+
+					updatedKCP, err := regionalClusterClient.GetKCPObjectForCluster(clusterUpgradeConfig.ClusterName, clusterUpgradeConfig.ClusterNamespace)
+					Expect(err).To(BeNil())
+					Expect(updatedKCP.ObjectMeta.Name).To(Equal("kcp-cluster-1"))
+					Expect(len(updatedKCP.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ExtraArgs)).To(Equal(0))
+				})
+			})
 		})
-		It("returns an error", func() {
-			Expect(err).To(HaveOccurred())
-			Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.0+vmware.4 to v1.18.0+vmware.1. Kubernetes version downgrade is not allowed."))
+
+		var _ = Describe("Test helper functions", func() {
+			Context("Testing the kube-vip modifier helper function", func() {
+				It("modifies the kube-vip parameters", func() {
+					pod := corev1.Pod{}
+					err := yaml.Unmarshal([]byte(kubeVipPodString), &pod)
+					Expect(err).To(BeNil())
+
+					newPodString, err := ModifyKubeVipAndSerialize(&pod, "30", "20", "4")
+					Expect(err).To(BeNil())
+
+					Expect(newPodString).ToNot(BeNil())
+				})
+			})
+			Context("Testing the KCP modifier helper function", func() {
+				It("Updates the KCP object with increased timeouts", func() {
+					currentKCP := getDummyKCP(constants.VSphereMachineTemplate)
+					newKCP, err := tkgClient.UpdateKCPObjectWithIncreasedKubeVip(currentKCP)
+
+					Expect(err).To(BeNil())
+					Expect(len(newKCP.Spec.KubeadmConfigSpec.Files)).To(Equal(1))
+					Expect(newKCP.Spec.KubeadmConfigSpec.Files[0].Content).To(ContainSubstring("value: \"30\""))
+					Expect(newKCP.Spec.KubeadmConfigSpec.Files[0].Content).To(ContainSubstring("name: cp_enable"))
+					Expect(newKCP.Spec.KubeadmConfigSpec.Files[0].Content).To(ContainSubstring("NET_RAW"))
+				})
+			})
 		})
 	})
-	Context("When get current k8s version > new version of cluster", func() {
+
+	Describe("When upgrading clusterclass-based cluster with fake controller runtime client", func() {
+
 		BeforeEach(func() {
-			currentClusterK8sVersion = "v1.18.5+vmware.1"
-		})
-		It("returns an error", func() {
-			Expect(err).To(HaveOccurred())
-			Expect(err.Error()).To(ContainSubstring("attempted to upgrade kubernetes from v1.18.5+vmware.1 to v1.18.0+vmware.1. Kubernetes version downgrade is not allowed."))
-		})
-	})
+			upgradeClusterOptions.KubernetesVersion = "v1.18.2+vmware.1"
+			currentK8sVersion = "v1.17.3+vmware.2"
+			setupBomFile("../fakes/config/bom/tkg-bom-v1.3.1.yaml", testingDir)
+			os.Setenv("SKIP_VSPHERE_TEMPLATE_VERIFICATION", "1")
 
-	var _ = Describe("Test PatchKubernetesVersionToKubeadmControlPlane", func() {
-		Context("Testing EtcdExtraArgs parameter configuration", func() {
-			It("when EtcdExtraArgs is defined", func() {
-				clusterUpgradeConfig := &ClusterUpgradeInfo{
-					ClusterName:      "cluster-1",
-					ClusterNamespace: constants.DefaultNamespace,
-					UpgradeComponentInfo: ComponentInfo{
-						EtcdExtraArgs:     map[string]string{"fake-arg": "fake-arg-value"},
-						KubernetesVersion: "v1.18.0+vmware.2",
+			regionalClusterOptions = fakehelper.TestAllClusterComponentOptions{
+				ClusterName: "cluster-1",
+				Namespace:   constants.DefaultNamespace,
+				Labels: map[string]string{
+					TkgLabelClusterRolePrefix + TkgLabelClusterRoleWorkload: "",
+				},
+				ClusterOptions: fakehelper.TestClusterOptions{
+					Phase:                   "provisioned",
+					InfrastructureReady:     true,
+					ControlPlaneInitialized: true,
+					ControlPlaneReady:       true,
+				},
+				ClusterTopology: fakehelper.TestClusterTopology{
+					Class:   "test-fake-clusterclass",
+					Version: currentK8sVersion,
+				},
+				CPOptions: fakehelper.TestCPOptions{
+					SpecReplicas:    3,
+					ReadyReplicas:   3,
+					UpdatedReplicas: 3,
+					Replicas:        3,
+					K8sVersion:      "v1.18.2+vmware.1",
+					InfrastructureTemplate: fakehelper.TestObject{
+						Kind:      constants.VSphereMachineTemplate,
+						Name:      "cluster-1-control-plane",
+						Namespace: constants.DefaultNamespace,
 					},
-					ActualComponentInfo: ComponentInfo{
-						KubernetesVersion: "v1.18.0+vmware.1",
+				},
+				ListMDOptions: fakehelper.GetListMDOptionsFromMDOptions(fakehelper.TestMDOptions{
+					SpecReplicas:    3,
+					ReadyReplicas:   3,
+					UpdatedReplicas: 3,
+					Replicas:        3,
+					InfrastructureTemplate: fakehelper.TestObject{
+						Kind:      constants.VSphereMachineTemplate,
+						Name:      "cluster-1-md-0",
+						Namespace: constants.DefaultNamespace,
 					},
-				}
+				}),
+				ClusterConfigurationOptions: fakehelper.TestClusterConfiguration{
+					ImageRepository:     "fake.image.repository",
+					DNSImageRepository:  "fake.image.repository",
+					DNSImageTag:         "v1.6.7_vmware.1",
+					EtcdImageRepository: "fake.image.repository",
+					EtcdImageTag:        "v3.4.3_vmware.5",
+				},
+				MachineOptions: []fakehelper.TestMachineOptions{
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: true},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
+					{Phase: "running", K8sVersion: "v1.18.2+vmware.1", IsCP: false},
+				},
+			}
+		})
 
-				err = tkgClient.PatchKubernetesVersionToKubeadmControlPlane(regionalClusterClient, clusterUpgradeConfig)
-				Expect(err).To(BeNil())
+		Context("Verify the .spec.topology.version got updated and cluster upgrade is successful", func() {
+			BeforeEach(func() {
+				configureTKGClient()
+				Expect(regionalClusterClient).NotTo(BeNil())
 
-				updatedKCP, err := regionalClusterClient.GetKCPObjectForCluster(clusterUpgradeConfig.ClusterName, clusterUpgradeConfig.ClusterNamespace)
-				Expect(err).To(BeNil())
-				Expect(updatedKCP.ObjectMeta.Name).To(Equal("kcp-cluster-1"))
-				Expect(updatedKCP.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ExtraArgs["fake-arg"]).To(Equal("fake-arg-value"))
+				err = tkgClient.DoClassyClusterUpgrade(regionalClusterClient, currentClusterClient, &upgradeClusterOptions)
 			})
 
-			It("when EtcdExtraArgs is empty", func() {
-				clusterUpgradeConfig := &ClusterUpgradeInfo{
-					ClusterName:      "cluster-1",
-					ClusterNamespace: constants.DefaultNamespace,
-					UpgradeComponentInfo: ComponentInfo{
-						EtcdExtraArgs:     map[string]string{},
-						KubernetesVersion: "v1.18.0+vmware.2",
-					},
-					ActualComponentInfo: ComponentInfo{
-						KubernetesVersion: "v1.18.0+vmware.1",
-					},
-				}
-
-				err = tkgClient.PatchKubernetesVersionToKubeadmControlPlane(regionalClusterClient, clusterUpgradeConfig)
+			It("should return an error", func() {
 				Expect(err).To(BeNil())
-
-				updatedKCP, err := regionalClusterClient.GetKCPObjectForCluster(clusterUpgradeConfig.ClusterName, clusterUpgradeConfig.ClusterNamespace)
+				Expect(regionalClusterClient).NotTo(BeNil())
+				Expect(upgradeClusterOptions).NotTo(BeNil())
+				cluster := &capi.Cluster{}
+				err = regionalClusterClient.GetResource(cluster, upgradeClusterOptions.ClusterName, upgradeClusterOptions.Namespace, nil, nil)
 				Expect(err).To(BeNil())
-				Expect(updatedKCP.ObjectMeta.Name).To(Equal("kcp-cluster-1"))
-				Expect(len(updatedKCP.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ExtraArgs)).To(Equal(0))
+				Expect(cluster.Spec.Topology.Version).To(Equal(upgradeClusterOptions.KubernetesVersion))
 			})
 		})
 	})
+})
 
-	var _ = Describe("Test helper functions", func() {
-		Context("Testing the kube-vip modifier helper function", func() {
-			It("modifies the kube-vip parameters", func() {
-				pod := corev1.Pod{}
-				err := yaml.Unmarshal([]byte(kubeVipPodString), &pod)
-				Expect(err).To(BeNil())
+var _ = Describe("Unit tests for clusterclass-based upgrade", func() {
+	var (
+		err                   error
+		regionalClusterClient *fakes.ClusterClient
+		currentClusterClient  *fakes.ClusterClient
+		tkgClient             *TkgClient
+		upgradeClusterOptions UpgradeClusterOptions
+	)
 
-				newPodString, err := ModifyKubeVipAndSerialize(&pod, "30", "20", "4")
-				Expect(err).To(BeNil())
+	BeforeEach(func() {
+		regionalClusterClient = &fakes.ClusterClient{}
+		currentClusterClient = &fakes.ClusterClient{}
 
-				Expect(newPodString).ToNot(BeNil())
-			})
-		})
-		Context("Testing the KCP modifier helper function", func() {
-			It("Updates the KCP object with increased timeouts", func() {
-				currentKCP := getDummyKCP(constants.VSphereMachineTemplate)
-				newKCP, err := tkgClient.UpdateKCPObjectWithIncreasedKubeVip(currentKCP)
+		tkgClient, err = CreateTKGClient("../fakes/config/config.yaml", testingDir, "../fakes/config/bom/tkg-bom-v1.3.1.yaml", 2*time.Millisecond)
 
-				Expect(err).To(BeNil())
-				Expect(len(newKCP.Spec.KubeadmConfigSpec.Files)).To(Equal(1))
-				Expect(newKCP.Spec.KubeadmConfigSpec.Files[0].Content).To(ContainSubstring("value: \"30\""))
-				Expect(newKCP.Spec.KubeadmConfigSpec.Files[0].Content).To(ContainSubstring("name: cp_enable"))
-				Expect(newKCP.Spec.KubeadmConfigSpec.Files[0].Content).To(ContainSubstring("NET_RAW"))
-			})
+		upgradeClusterOptions = UpgradeClusterOptions{
+			ClusterName:       "fake-cluster-name",
+			Namespace:         "fake-namespace",
+			KubernetesVersion: "v1.23.5+vmware.1",
+			TkrVersion:        newTKRVersion,
+			IsRegionalCluster: false,
+			SkipAddonUpgrade:  true,
+		}
+	})
+
+	JustBeforeEach(func() {
+		err = tkgClient.DoClassyClusterUpgrade(regionalClusterClient, currentClusterClient, &upgradeClusterOptions)
+	})
+	Context("When cluster patch fails", func() {
+		BeforeEach(func() {
+			regionalClusterClient.PatchClusterObjectReturns(errors.New("fake-patch-error"))
+		})
+		It("should return an error", func() {
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring("unable to patch kubernetes version to cluster: fake-patch-error"))
+		})
+	})
+	Context("When failure happens while waiting for control-plane node upgrade", func() {
+		BeforeEach(func() {
+			regionalClusterClient.PatchClusterObjectReturns(nil)
+			regionalClusterClient.WaitK8sVersionUpdateForCPNodesReturns(errors.New("fake-error-kcp-upgrade"))
+		})
+		It("should return an error", func() {
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring("error waiting for kubernetes version update for kubeadm control plane: fake-error-kcp-upgrade"))
+		})
+	})
+	Context("When failure happens while waiting for worker node upgrade", func() {
+		BeforeEach(func() {
+			regionalClusterClient.PatchClusterObjectReturns(nil)
+			regionalClusterClient.WaitK8sVersionUpdateForCPNodesReturns(nil)
+			regionalClusterClient.WaitK8sVersionUpdateForWorkerNodesReturns(errors.New("fake-error-worker-upgrade"))
+		})
+		It("should return an error", func() {
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(ContainSubstring("error waiting for kubernetes version update for worker nodes: fake-error-worker-upgrade"))
+		})
+	})
+	Context("When cluster patch is successful and cluster get's upgraded successfully", func() {
+		BeforeEach(func() {
+			regionalClusterClient.PatchClusterObjectReturns(nil)
+			regionalClusterClient.WaitK8sVersionUpdateForCPNodesReturns(nil)
+			regionalClusterClient.WaitK8sVersionUpdateForWorkerNodesReturns(nil)
+		})
+		It("should not return an error", func() {
+			Expect(err).NotTo(HaveOccurred())
 		})
 	})
 })
diff --git a/pkg/v1/tkg/clusterclient/clusterclient_test.go b/pkg/v1/tkg/clusterclient/clusterclient_test.go
index 2f18437943..8207b8c39b 100644
--- a/pkg/v1/tkg/clusterclient/clusterclient_test.go
+++ b/pkg/v1/tkg/clusterclient/clusterclient_test.go
@@ -2616,6 +2616,62 @@ var _ = Describe("Cluster Client", func() {
 			})
 		})
 	})
+
+	Describe("Unit tests for IsClusterClassBased", func() {
+		var isClusterClassBased bool
+
+		BeforeEach(func() {
+			reInitialize()
+			kubeConfigPath := getConfigFilePath("config1.yaml")
+			clstClient, err = NewClient(kubeConfigPath, "", clusterClientOptions)
+			Expect(err).NotTo(HaveOccurred())
+		})
+		Context("When clientset Get api return error", func() {
+			JustBeforeEach(func() {
+				clientset.GetReturns(errors.New("fake-error"))
+				isClusterClassBased, err = clstClient.IsClusterClassBased("fake-clusterName", "fake-namespace")
+			})
+			It("should return an error", func() {
+				Expect(err).To(HaveOccurred())
+				Expect(isClusterClassBased).To(Equal(false))
+				Expect(err.Error()).To(ContainSubstring("fake-error"))
+			})
+		})
+		Context("When cluster is not using ClusterClass", func() {
+			JustBeforeEach(func() {
+				clientset.GetCalls(func(ctx context.Context, namespace types.NamespacedName, cluster crtclient.Object) error {
+					topology := &capi.Topology{
+						Class: "",
+					}
+					cluster.(*capi.Cluster).Spec.Topology = topology
+					return nil
+				})
+
+				isClusterClassBased, err = clstClient.IsClusterClassBased("fake-clusterName", "fake-namespace")
+			})
+			It("should not return an error and isClusterClassBased to be false", func() {
+				Expect(err).NotTo(HaveOccurred())
+				Expect(isClusterClassBased).To(Equal(false))
+			})
+		})
+		Context("When cluster is using ClusterClass", func() {
+			JustBeforeEach(func() {
+				clientset.GetCalls(func(ctx context.Context, namespace types.NamespacedName, cluster crtclient.Object) error {
+					topology := &capi.Topology{
+						Class: "fake-cluster-class",
+					}
+					cluster.(*capi.Cluster).Spec.Topology = topology
+					return nil
+				})
+				isClusterClassBased, err = clstClient.IsClusterClassBased("fake-clusterName", "fake-namespace")
+			})
+			It("should not return an error and isClusterClassBased to be true", func() {
+				Expect(err).NotTo(HaveOccurred())
+				Expect(isClusterClassBased).To(Equal(true))
+			})
+		})
+	})
+
 })
 
 func createTempDirectory() {