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

Issue:
vSphereCSIConfig API and Controller
Per new TKR and addons design/requirement, we would like to convert client side ytt configs to adopt new config APIs

Create Core Addons Config API CRD - Create vSphereCSI CRD and controller to create data values secret


Discussion / hints:

Fix patch:
diff --git a/addons/Makefile b/addons/Makefile
index 239614d5c2..07b00e9bd5 100644
--- a/addons/Makefile
+++ b/addons/Makefile
@@ -51,7 +51,7 @@ help: ## Display this help
 .PHONY: test
 test: fmt vet template-tests ## Run Tests
 	$(MAKE) kubebuilder -C ${ROOT_DIR}$(TOOLS_DIR)
-	KUBEBUILDER_ASSETS=${ROOT_DIR}$(KUBEBUILDER_BIN_DIR) go test ./... -coverprofile cover.out -v 2 -timeout 15m
+	KUBEBUILDER_ASSETS=${ROOT_DIR}$(KUBEBUILDER_BIN_DIR) go test ./... -timeout 60m -coverprofile cover.out -v 2
 	$(MAKE) test -C pinniped/post-deploy
 
 .PHONY: test-verbose
diff --git a/addons/controllers/cpi/vspherecpiconfig_const.go b/addons/controllers/cpi/vspherecpiconfig_const.go
index da0215f089..211b79fceb 100644
--- a/addons/controllers/cpi/vspherecpiconfig_const.go
+++ b/addons/controllers/cpi/vspherecpiconfig_const.go
@@ -35,6 +35,7 @@ const (
 	ProviderServiceAccountSecretNamespace = "vmware-system-cloud-provider" // nolint:gosec
 )
 
+// TODO: make these constants accessible to other controllers (for example csi) https://github.com/vmware-tanzu/tanzu-framework/issues/2086
 // constants used for deriving supervisor API server endpoint
 const (
 	SupervisorEndpointHostname = "supervisor.default.svc"
diff --git a/addons/controllers/cpi/vspherecpiconfig_utils.go b/addons/controllers/cpi/vspherecpiconfig_utils.go
index a4484d8d44..49f22e639c 100644
--- a/addons/controllers/cpi/vspherecpiconfig_utils.go
+++ b/addons/controllers/cpi/vspherecpiconfig_utils.go
@@ -276,6 +276,7 @@ func (r *VSphereCPIConfigReconciler) getOwnerCluster(ctx context.Context, cpiCon
 	return cluster, nil
 }
 
+// TODO: make these functions accessible to other controllers (for example csi) https://github.com/vmware-tanzu/tanzu-framework/issues/2086
 // getVSphereCluster gets the VSphereCluster CR for the cluster object
 func (r *VSphereCPIConfigReconciler) getVSphereCluster(ctx context.Context, cluster *clusterapiv1beta1.Cluster) (*capvv1beta1.VSphereCluster, error) {
 	vsphereCluster := &capvv1beta1.VSphereCluster{}
diff --git a/addons/controllers/csi/vspherecsiconfig_constants.go b/addons/controllers/csi/vspherecsiconfig_constants.go
new file mode 100644
index 0000000000..4f0b9a073e
--- /dev/null
+++ b/addons/controllers/csi/vspherecsiconfig_constants.go
@@ -0,0 +1,34 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package controllers ...
+package controllers
+
+import (
+	"time"
+)
+
+// supported modes for VSphereCSIConfig
+const (
+	VSphereCSINonParavirtualMode = "vsphereCSI"
+	VSphereCSIParavirtualMode    = "vsphereParavirtualCSI"
+)
+
+// cluster variable names required by VSphereCSIConfig to derive values
+const (
+	VSphereNetworkVarName           = "VSPHERE_NETWORK"
+	VSphereRegionVarName            = "VSPHERE_REGION"
+	VSphereZoneVarName              = "VSPHERE_ZONE"
+	VSphereVersionVarName           = "VSPHERE_VERSION"
+	VSphereTLSThumbprintVarName     = "VSPHERE_TLS_THUMBPRINT"
+	IsWindowsWorkloadClusterVarName = "IS_WINDOWS_WORKLOAD_CLUSTER"
+	ControllerRequeueDelay          = 20 * time.Second
+)
+
+const (
+	VSphereCSINamespace                 = "kube-system"
+	VSphereCSIProvisionTimeout          = "300s"
+	VSphereCSIAttachTimeout             = "300s"
+	VSphereCSIResizerTimeout            = "300s"
+	VSphereCSIDefaultDeploymentReplicas = 3
+)
diff --git a/addons/controllers/csi/vspherecsiconfig_controller.go b/addons/controllers/csi/vspherecsiconfig_controller.go
new file mode 100644
index 0000000000..9b1589afe1
--- /dev/null
+++ b/addons/controllers/csi/vspherecsiconfig_controller.go
@@ -0,0 +1,200 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	yaml "gopkg.in/yaml.v3"
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	capvvmwarev1beta1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
+	clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	clusterapiutil "sigs.k8s.io/cluster-api/util"
+	clusterapipatchutil "sigs.k8s.io/cluster-api/util/patch"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/constants"
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/util"
+	csiv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/csi/v1alpha1"
+)
+
+// VSphereCSIConfigReconciler reconciles a VSphereCSIConfig object
+type VSphereCSIConfigReconciler struct {
+	client.Client
+	Log    logr.Logger
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups=csi.tanzu.vmware.com,resources=vspherecsiconfigs,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=csi.tanzu.vmware.com,resources=vspherecsiconfigs/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=csi.tanzu.vmware.com,resources=vspherecsiconfigs/finalizers,verbs=update
+//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch
+//+kubebuilder:rbac:groups=vmware.infrastructure.cluster.x-k8s.io,resources=providerserviceaccounts,verbs=get;create;list;watch;update;patch
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+func (r *VSphereCSIConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	r.Log = r.Log.WithValues("VSphereCSIConfig", req.NamespacedName)
+	ctx = logr.NewContext(ctx, r.Log)
+	logger := log.FromContext(ctx)
+
+	vcsiConfig := &csiv1alpha1.VSphereCSIConfig{}
+	if err := r.Get(ctx, req.NamespacedName, vcsiConfig); err != nil {
+		if apierrors.IsNotFound(err) {
+			logger.Info("VSphereCSIConfig resource not found")
+			return ctrl.Result{}, nil
+		}
+
+		logger.Error(err, "Unable to fetch VSphereCSIConfig resource")
+		return ctrl.Result{}, err
+	}
+
+	cluster, err := r.getOwnerCluster(ctx, vcsiConfig)
+	if cluster == nil {
+		return ctrl.Result{RequeueAfter: 20 * time.Second}, err // retry until corresponding cluster is found
+	}
+
+	return r.reconcileVSphereCSIConfig(ctx, vcsiConfig, cluster)
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *VSphereCSIConfigReconciler) SetupWithManager(_ context.Context, mgr ctrl.Manager,
+	options controller.Options) error {
+
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&csiv1alpha1.VSphereCSIConfig{}).
+		WithOptions(options).
+		Complete(r)
+}
+
+func (r *VSphereCSIConfigReconciler) reconcileVSphereCSIConfig(ctx context.Context,
+	csiCfg *csiv1alpha1.VSphereCSIConfig,
+	cluster *clusterapiv1beta1.Cluster) (result ctrl.Result, retErr error) {
+
+	logger := log.FromContext(ctx)
+
+	patchHelper, err := clusterapipatchutil.NewHelper(csiCfg, r.Client)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
+	defer func() {
+		if retErr != nil {
+			// don't modify VSphereCSIConfig if there is an error
+			return
+		}
+
+		if err := patchHelper.Patch(ctx, csiCfg); err != nil {
+			logger.Error(err, "Error patching VSphereCSIConfig")
+			retErr = err
+		}
+	}()
+
+	if !csiCfg.GetDeletionTimestamp().IsZero() {
+		return ctrl.Result{}, nil // deleted
+	}
+
+	if result, err = r.reconcileVSphereCSIConfigNormal(ctx, csiCfg, cluster); err != nil {
+		logger.Error(err, "Error reconciling VSphereCSIConfig")
+		return result, err
+	}
+
+	return result, nil
+}
+
+func (r *VSphereCSIConfigReconciler) reconcileVSphereCSIConfigNormal(ctx context.Context,
+	csiCfg *csiv1alpha1.VSphereCSIConfig,
+	cluster *clusterapiv1beta1.Cluster) (ctrl.Result, error) {
+
+	logger := log.FromContext(ctx)
+
+	ownerRef := metav1.OwnerReference{
+		APIVersion: clusterapiv1beta1.GroupVersion.String(),
+		Kind:       cluster.Kind,
+		Name:       cluster.Name,
+		UID:        cluster.UID,
+	}
+
+	if !clusterapiutil.HasOwnerRef(csiCfg.OwnerReferences, ownerRef) {
+		// csiCfg object is patched in defer func in 'reconcileVSphereCSIConfig'
+		csiCfg.OwnerReferences = clusterapiutil.EnsureOwnerRef(csiCfg.OwnerReferences, ownerRef)
+	}
+
+	addonName := ""
+	switch csiCfg.Spec.VSphereCSI.Mode {
+	case VSphereCSINonParavirtualMode:
+		addonName = constants.CSIAddonName
+	case VSphereCSIParavirtualMode:
+		addonName = constants.PVCSIAddonName
+	default:
+		err := errors.Errorf("Invalid CSI mode '%s', must either be '%s' or '%s'",
+			csiCfg.Spec.VSphereCSI.Mode, VSphereCSIParavirtualMode, VSphereCSINonParavirtualMode)
+		logger.Error(err, "")
+		return ctrl.Result{}, err
+	}
+
+	secret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      util.GenerateDataValueSecretName(cluster.Name, addonName),
+			Namespace: csiCfg.Namespace},
+		Type: v1.SecretTypeOpaque,
+	}
+
+	secret.SetOwnerReferences([]metav1.OwnerReference{ownerRef})
+
+	mutateFn := func() error {
+		secret.Data = make(map[string][]byte)
+		dvs, err := r.mapVSphereCSIConfigToDataValues(ctx, csiCfg, cluster)
+		if err != nil {
+			logger.Error(err, "Error while mapping VSphereCSIConfig to data values")
+			return err
+		}
+		yamlBytes, err := yaml.Marshal(dvs)
+		if err != nil {
+			logger.Error(err, "Error marshaling CSI config data values to yaml")
+			return err
+		}
+		secret.Data[constants.TKGDataValueFileName] = yamlBytes
+		return nil
+	}
+
+	opResult, err := controllerutil.CreateOrPatch(ctx, r.Client, secret, mutateFn)
+
+	if err != nil {
+		logger.Error(err, "Error creating or patching VSphereCSIConfig data values secret")
+		return ctrl.Result{}, err
+	}
+
+	// deploy the provider service account for paravirtual mode
+	if csiCfg.Spec.VSphereCSI.Mode == VSphereCSIParavirtualMode {
+		serviceAccount := &capvvmwarev1beta1.ProviderServiceAccount{}
+		_, err := controllerutil.CreateOrUpdate(ctx, r.Client, serviceAccount, func() error {
+			serviceAccount = r.mapCSIConfigToProviderServiceAccount(cluster)
+			return controllerutil.SetControllerReference(cluster, serviceAccount, r.Scheme)
+		})
+		if err != nil {
+			logger.Error(err, "Error creating or updating ProviderServiceAccount for VSphere CPI")
+			return ctrl.Result{}, err
+		}
+	}
+
+	logger.Info(fmt.Sprintf("'%s' the secret '%s'", opResult,
+		fmt.Sprintf("%s/%s", secret.Namespace, secret.Name)))
+
+	csiCfg.Status.SecretRef = &secret.Name
+
+	return ctrl.Result{}, nil
+}
diff --git a/addons/controllers/csi/vspherecsiconfig_datavalues.go b/addons/controllers/csi/vspherecsiconfig_datavalues.go
new file mode 100644
index 0000000000..66641612c9
--- /dev/null
+++ b/addons/controllers/csi/vspherecsiconfig_datavalues.go
@@ -0,0 +1,42 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+// DataValues is the data values type generated by the VSphereCSIConfig CR
+type DataValues struct {
+	VSphereCSI   *DataValuesVSphereCSI   `yaml:"vsphereCSI,omitempty"`
+	VSpherePVCSI *DataValuesVSpherePVCSI `yaml:"vspherePVCSI,omitempty"`
+}
+
+type DataValuesVSpherePVCSI struct {
+	ClusterName                      string `yaml:"cluster_name"`
+	ClusterUID                       string `yaml:"cluster_uid"`
+	Namespace                        string `yaml:"namespace"`
+	SupervisorMasterEndpointHostname string `yaml:"supervisor_master_endpoint_hostname"`
+	SupervisorMasterPort             int32  `yaml:"supervisor_master_port"`
+}
+
+type DataValuesVSphereCSI struct {
+	TLSThumbprint         string `yaml:"tlsThumbprint"`
+	Namespace             string `yaml:"namespace"`
+	ClusterName           string `yaml:"clusterName"`
+	Server                string `yaml:"server"`
+	Datacenter            string `yaml:"datacenter"`
+	PublicNetwork         string `yaml:"publicNetwork"`
+	Username              string `yaml:"username"`
+	Password              string `yaml:"password"`
+	Region                string `yaml:"region"`
+	Zone                  string `yaml:"zone"`
+	InsecureFlag          bool   `yaml:"insecureFlag"`
+	UseTopologyCategories bool   `yaml:"useTopologyCategories"`
+	ProvisionTimeout      string `yaml:"provisionTimeout"`
+	AttachTimeout         string `yaml:"attachTimeout"`
+	ResizerTimeout        string `yaml:"resizerTimeout"`
+	VSphereVersion        string `yaml:"vSphereVersion"`
+	HTTPProxy             string `yaml:"http_proxy"`
+	HTTPSProxy            string `yaml:"https_proxy"`
+	NoProxy               string `yaml:"no_proxy"`
+	DeploymentReplicas    int32  `yaml:"deployment_replicas"`
+	WindowsSupport        bool   `yaml:"windows_support"`
+}
diff --git a/addons/controllers/csi/vspherecsiconfig_utils.go b/addons/controllers/csi/vspherecsiconfig_utils.go
new file mode 100644
index 0000000000..261eb2b624
--- /dev/null
+++ b/addons/controllers/csi/vspherecsiconfig_utils.go
@@ -0,0 +1,272 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/pkg/errors"
+	v1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	capvv1beta1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
+	capvvmwarev1beta1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
+	clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	cuitl "github.com/vmware-tanzu/tanzu-framework/addons/controllers/utils"
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/constants"
+	pkgtypes "github.com/vmware-tanzu/tanzu-framework/addons/pkg/types"
+	csiv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/csi/v1alpha1"
+)
+
+// mapVSphereCSIConfigToDataValues maps VSphereCSIConfig CR to data values
+func (r *VSphereCSIConfigReconciler) mapVSphereCSIConfigToDataValues(ctx context.Context,
+	vcsiConfig *csiv1alpha1.VSphereCSIConfig,
+	cluster *clusterapiv1beta1.Cluster) (*DataValues, error) {
+
+	switch vcsiConfig.Spec.VSphereCSI.Mode {
+	case VSphereCSINonParavirtualMode:
+		return r.mapVSphereCSIConfigToDataValuesNonParavirtual(ctx, vcsiConfig, cluster)
+	case VSphereCSIParavirtualMode:
+		return r.mapVSphereCSIConfigToDataValuesParavirtual(ctx, cluster)
+	default:
+		break
+	}
+	//TODO: implement validation webhook to prevent this https://github.com/vmware-tanzu/tanzu-framework/issues/2087
+	return nil, errors.Errorf("Invalid CSI mode '%s', must either be '%s' or '%s'",
+		vcsiConfig.Spec.VSphereCSI.Mode, VSphereCSIParavirtualMode, VSphereCSINonParavirtualMode)
+}
+
+func (r *VSphereCSIConfigReconciler) mapVSphereCSIConfigToDataValuesParavirtual(_ context.Context,
+	cluster *clusterapiv1beta1.Cluster) (*DataValues, error) {
+
+	dvs := &DataValues{}
+	dvs.VSpherePVCSI = &DataValuesVSpherePVCSI{}
+	dvs.VSpherePVCSI.ClusterName = cluster.Name
+	dvs.VSpherePVCSI.ClusterUID = string(cluster.UID)
+	// default values from https://github.com/vmware-tanzu/community-edition/blob/main/addons/packages/vsphere-pv-csi/2.4.1/bundle/config/values.yaml
+	dvs.VSpherePVCSI.Namespace = "vmware-system-csi"
+	dvs.VSpherePVCSI.SupervisorMasterEndpointHostname = "supervisor.default.svc"
+	dvs.VSpherePVCSI.SupervisorMasterPort = 6443
+
+	return dvs, nil
+}
+
+func (r *VSphereCSIConfigReconciler) mapVSphereCSIConfigToDataValuesNonParavirtual(ctx context.Context,
+	vcsiConfig *csiv1alpha1.VSphereCSIConfig,
+	cluster *clusterapiv1beta1.Cluster) (*DataValues, error) {
+
+	dvs := &DataValues{}
+
+	// populate derived values
+	dvs.VSphereCSI = &DataValuesVSphereCSI{}
+	dvs.VSphereCSI.ClusterName = cluster.Name
+
+	vsphereCluster, err := cuitl.GetVSphereCluster(ctx, r.Client, cluster)
+	if err != nil {
+		return nil, err
+	}
+	dvs.VSphereCSI.Server = vsphereCluster.Spec.Server
+	dvs.VSphereCSI.TLSThumbprint = vsphereCluster.Spec.Thumbprint
+
+	cpMachineTemplate := &capvv1beta1.VSphereMachineTemplate{}
+	if err := r.Client.Get(ctx, types.NamespacedName{
+		Namespace: cluster.Namespace,
+		Name:      cuitl.ControlPlaneName(cluster.Name),
+	}, cpMachineTemplate); err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, errors.Errorf("VSphereMachineTemplate %s/%s not found", cluster.Namespace, cuitl.ControlPlaneName(cluster.Name))
+		}
+		return nil, errors.Errorf("VSphereMachineTemplate %s/%s could not be fetched, error %v", cluster.Namespace, cuitl.ControlPlaneName(cluster.Name), err)
+	}
+
+	dvs.VSphereCSI.Datacenter = cpMachineTemplate.Spec.Template.Spec.Datacenter
+	dvs.VSphereCSI.PublicNetwork = cuitl.TryParseClusterVariableString(ctx, cluster, VSphereNetworkVarName)
+
+	// derive vSphere username and password from the <cluster name> secret
+	clusterSecret, err := cuitl.GetSecret(ctx, r.Client, cluster.Namespace, cluster.Name)
+	if err != nil {
+		return nil, err
+	}
+	dvs.VSphereCSI.Username, dvs.VSphereCSI.Password, err = cuitl.GetUsernameAndPasswordFromSecret(clusterSecret)
+	if err != nil {
+		return nil, err
+	}
+
+	dvs.VSphereCSI.Region = cuitl.TryParseClusterVariableString(ctx, cluster, VSphereRegionVarName)
+	dvs.VSphereCSI.Zone = cuitl.TryParseClusterVariableString(ctx, cluster, VSphereZoneVarName)
+	dvs.VSphereCSI.VSphereVersion = cuitl.TryParseClusterVariableString(ctx, cluster, VSphereVersionVarName)
+	dvs.VSphereCSI.WindowsSupport = cuitl.TryParseClusterVariableBool(ctx, cluster, IsWindowsWorkloadClusterVarName)
+
+	if cluster.Annotations != nil {
+		dvs.VSphereCSI.HTTPProxy = cluster.Annotations[pkgtypes.HTTPProxyConfigAnnotation]
+		dvs.VSphereCSI.HTTPSProxy = cluster.Annotations[pkgtypes.HTTPSProxyConfigAnnotation]
+		dvs.VSphereCSI.NoProxy = cluster.Annotations[pkgtypes.NoProxyConfigAnnotation]
+	}
+
+	// populated from default value in https://github.com/vmware-tanzu/community-edition/blob/main/addons/packages/vsphere-csi/2.4.1/bundle/config/values.yaml
+	dvs.VSphereCSI.Namespace = VSphereCSINamespace
+	dvs.VSphereCSI.UseTopologyCategories = false
+	dvs.VSphereCSI.ProvisionTimeout = VSphereCSIProvisionTimeout
+	dvs.VSphereCSI.AttachTimeout = VSphereCSIAttachTimeout
+	dvs.VSphereCSI.ResizerTimeout = VSphereCSIResizerTimeout
+	dvs.VSphereCSI.DeploymentReplicas = VSphereCSIDefaultDeploymentReplicas
+	dvs.VSphereCSI.InsecureFlag = true
+
+	// TODO: implement defaulting webhook for 'vspherecsiconfig' https://github.com/vmware-tanzu/tanzu-framework/issues/2088
+	// override derived values IF set in csi configuration by user
+	if vcsiConfig.Spec.VSphereCSI.NonParavirtualConfig != nil {
+		overrideDerivedValues(dvs.VSphereCSI, vcsiConfig.Spec.VSphereCSI.NonParavirtualConfig)
+	}
+
+	return dvs, nil
+}
+
+// getOwnerCluster verifies that the VSphereCSIConfig has a cluster as its owner reference,
+// and returns the cluster. It tries to read the cluster name from the VSphereCSIConfig's owner reference objects.
+// If not there, we assume the owner cluster and VSphereCSIConfig always has the same name.
+func (r *VSphereCSIConfigReconciler) getOwnerCluster(ctx context.Context,
+	vcsiConfig *csiv1alpha1.VSphereCSIConfig) (*clusterapiv1beta1.Cluster, error) {
+
+	logger := log.FromContext(ctx)
+	cluster := &clusterapiv1beta1.Cluster{}
+	clusterName := vcsiConfig.Name // usually the corresponding 'cluster' shares the same name
+
+	// retrieve the owner cluster for the VSphereCSIConfig object
+	for _, ownerRef := range vcsiConfig.GetOwnerReferences() {
+		if strings.EqualFold(ownerRef.Kind, constants.ClusterKind) {
+			clusterName = ownerRef.Name
+			break
+		}
+	}
+	if err := r.Client.Get(ctx, types.NamespacedName{Namespace: vcsiConfig.Namespace, Name: clusterName}, cluster); err != nil {
+		if apierrors.IsNotFound(err) {
+			logger.Info(fmt.Sprintf("Cluster resource '%s/%s' not found", vcsiConfig.Namespace, clusterName))
+			return nil, nil
+		}
+		logger.Error(err, fmt.Sprintf("Unable to fetch cluster '%s/%s'", vcsiConfig.Namespace, clusterName))
+		return nil, err
+	}
+
+	return cluster, nil
+}
+
+// mapCSIConfigToProviderServiceAccount maps CSIConfig and cluster to the corresponding service account
+func (r *VSphereCSIConfigReconciler) mapCSIConfigToProviderServiceAccount(cluster *clusterapiv1beta1.Cluster) *capvvmwarev1beta1.ProviderServiceAccount {
+	serviceAccount := &capvvmwarev1beta1.ProviderServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      fmt.Sprintf("%s-%s", cluster.Name, "pvcsi"),
+			Namespace: cluster.Namespace,
+		},
+		Spec: capvvmwarev1beta1.ProviderServiceAccountSpec{
+			Ref: &v1.ObjectReference{Name: cluster.Name, Namespace: cluster.Namespace},
+			Rules: []rbacv1.PolicyRule{
+				{
+					APIGroups: []string{"vmoperator.vmware.com"},
+					Resources: []string{"virtualmachines"},
+					Verbs:     []string{"get", "list", "watch", "update", "patch"},
+				},
+				{
+					APIGroups: []string{"cns.vmware.com"},
+					Resources: []string{"cnsvolumemetadatas", "cnsfileaccessconfigs"},
+					Verbs:     []string{"get", "list", "watch", "update", "create", "delete"},
+				},
+				{
+					APIGroups: []string{"cns.vmware.com"},
+					Resources: []string{"cnscsisvfeaturestates"},
+					Verbs:     []string{"get", "list", "watch"},
+				},
+				{
+					APIGroups: []string{""},
+					Resources: []string{"persistentvolumeclaims"},
+					Verbs:     []string{"get", "list", "watch", "update", "create", "delete"},
+				},
+				{
+					APIGroups: []string{""},
+					Resources: []string{"persistentvolumeclaims/status"},
+					Verbs:     []string{"get", "update", "patch"},
+				},
+				{
+					APIGroups: []string{""},
+					Resources: []string{"events"},
+					Verbs:     []string{"list"},
+				},
+			},
+			TargetNamespace:  "vmware-system-csi",
+			TargetSecretName: "pvcsi-provider-creds",
+		},
+	}
+	return serviceAccount
+}
+
+func overrideDerivedValues(dvscsi *DataValuesVSphereCSI, config *csiv1alpha1.NonParavirtualConfig) { // nolint:gocyclo
+	if config.Namespace != "" {
+		dvscsi.Namespace = config.Namespace
+	}
+	if config.ClusterName != "" {
+		dvscsi.ClusterName = config.ClusterName
+	}
+	if config.Server != "" {
+		dvscsi.Server = config.Server
+	}
+	if config.Datacenter != "" {
+		dvscsi.Datacenter = config.Datacenter
+	}
+	if config.PublicNetwork != "" {
+		dvscsi.PublicNetwork = config.PublicNetwork
+	}
+	if config.Username != "" {
+		dvscsi.Username = config.Username
+	}
+	if config.Password != "" {
+		dvscsi.Password = config.Password
+	}
+	if config.Region != "" {
+		dvscsi.Region = config.Region
+	}
+	if config.Zone != "" {
+		dvscsi.Zone = config.Zone
+	}
+	if config.VSphereVersion != "" {
+		dvscsi.VSphereVersion = config.VSphereVersion
+	}
+	if config.WindowsSupport != nil {
+		dvscsi.WindowsSupport = *config.WindowsSupport
+	}
+	if config.TLSThumbprint != "" {
+		dvscsi.TLSThumbprint = config.TLSThumbprint
+	}
+	if config.HTTPProxy != "" {
+		dvscsi.HTTPProxy = config.HTTPProxy
+	}
+	if config.HTTPSProxy != "" {
+		dvscsi.HTTPSProxy = config.HTTPSProxy
+	}
+	if config.NoProxy != "" {
+		dvscsi.NoProxy = config.NoProxy
+	}
+	if config.UseTopologyCategories != nil {
+		dvscsi.UseTopologyCategories = *config.UseTopologyCategories
+	}
+	if config.ProvisionTimeout != "" {
+		dvscsi.ProvisionTimeout = config.ProvisionTimeout
+	}
+	if config.AttachTimeout != "" {
+		dvscsi.AttachTimeout = config.AttachTimeout
+	}
+	if config.ResizerTimeout != "" {
+		dvscsi.ResizerTimeout = config.ResizerTimeout
+	}
+	if config.DeploymentReplicas != nil {
+		dvscsi.DeploymentReplicas = *config.DeploymentReplicas
+	}
+	if config.InsecureFlag != nil {
+		dvscsi.InsecureFlag = *config.InsecureFlag
+	}
+}
diff --git a/addons/controllers/utils/utils.go b/addons/controllers/utils/utils.go
new file mode 100644
index 0000000000..041a6fb5f1
--- /dev/null
+++ b/addons/controllers/utils/utils.go
@@ -0,0 +1,95 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package utils contains utility functions shared by controllers
+package utils
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/pkg/errors"
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
+	capvv1beta1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
+	clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/util"
+)
+
+// GetVSphereCluster gets the VSphereCluster CR for the cluster object
+func GetVSphereCluster(ctx context.Context, clt client.Client, cluster *clusterapiv1beta1.Cluster) (*capvv1beta1.VSphereCluster, error) {
+	vsphereCluster := &capvv1beta1.VSphereCluster{}
+	if err := clt.Get(ctx, types.NamespacedName{
+		Namespace: cluster.Namespace,
+		Name:      cluster.Name,
+	}, vsphereCluster); err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, errors.Errorf("VSphereCluster %s/%s not found", cluster.Namespace, cluster.Name)
+		}
+		return nil, errors.Errorf("VSphereCluster %s/%s could not be fetched, error %v", cluster.Namespace, cluster.Name, err)
+	}
+	return vsphereCluster, nil
+}
+
+// ControlPlaneName returns the control plane name for a cluster name
+func ControlPlaneName(clusterName string) string {
+	return fmt.Sprintf("%s-control-plane", clusterName)
+}
+
+// GetSecret gets the secret object given its name and namespace
+func GetSecret(ctx context.Context, clt client.Client, namespace, name string) (*v1.Secret, error) {
+	secret := &v1.Secret{}
+	if err := clt.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, secret); err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, errors.Errorf("Secret %s/%s not found", namespace, name)
+		}
+		return nil, errors.Errorf("Secret %s/%s could not be fetched, error %v", namespace, name, err)
+	}
+	return secret, nil
+}
+
+// GetUsernameAndPasswordFromSecret extracts a username and password from secret
+func GetUsernameAndPasswordFromSecret(s *v1.Secret) (string, string, error) {
+	username, exists := s.Data["username"]
+	if !exists {
+		return "", "", errors.Errorf("Secret %s/%s doesn't have string data with username", s.Namespace, s.Name)
+	}
+	password, exists := s.Data["password"]
+	if !exists {
+		return "", "", errors.Errorf("Secret %s/%s doesn't have string data with password", s.Namespace, s.Name)
+	}
+	return string(username), string(password), nil
+}
+
+// GetCCMName returns the name of cloud control manager for a cluster
+func GetCCMName(cluster *clusterapiv1beta1.Cluster) string {
+	return fmt.Sprintf("%s-%s", cluster.Name, "ccm")
+}
+
+// TryParseClusterVariableBool tries to parse a boolean cluster variable,
+// info any error that occurs
+func TryParseClusterVariableBool(ctx context.Context, cluster *clusterapiv1beta1.Cluster,
+	variableName string) bool {
+
+	res, err := util.ParseClusterVariableBool(cluster, variableName)
+	if err != nil {
+		log.FromContext(ctx).Error(err, "cannot parse cluster variable", "key", variableName)
+	}
+	return res
+}
+
+// TryParseClusterVariableString tries to parse a string cluster variable,
+// info any error that occurs
+func TryParseClusterVariableString(ctx context.Context, cluster *clusterapiv1beta1.Cluster,
+	variableName string) string {
+
+	res, err := util.ParseClusterVariableString(cluster, variableName)
+	if err != nil {
+		log.FromContext(ctx).Error(err, "cannot parse cluster variable", "key", variableName)
+	}
+	return res
+}
diff --git a/addons/main.go b/addons/main.go
index 3b40bfeaa2..752598cfd5 100644
--- a/addons/main.go
+++ b/addons/main.go
@@ -32,12 +32,14 @@ import (
 	antreacontroller "github.com/vmware-tanzu/tanzu-framework/addons/controllers/antrea"
 	calicocontroller "github.com/vmware-tanzu/tanzu-framework/addons/controllers/calico"
 	cpicontroller "github.com/vmware-tanzu/tanzu-framework/addons/controllers/cpi"
+	csicontroller "github.com/vmware-tanzu/tanzu-framework/addons/controllers/csi"
 	kappcontroller "github.com/vmware-tanzu/tanzu-framework/addons/controllers/kapp-controller"
 	addonconfig "github.com/vmware-tanzu/tanzu-framework/addons/pkg/config"
 	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/constants"
 	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/crdwait"
 	cniv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/cni/v1alpha1"
 	cpiv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/cpi/v1alpha1"
+	csiv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/csi/v1alpha1"
 	runtanzuv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha1"
 	runtanzuv1alpha3 "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha3"
 	"github.com/vmware-tanzu/tanzu-framework/pkg/v1/buildinfo"
@@ -62,6 +64,7 @@ func init() {
 	_ = runtanzuv1alpha3.AddToScheme(scheme)
 	_ = cniv1alpha1.AddToScheme(scheme)
 	_ = cpiv1alpha1.AddToScheme(scheme)
+	_ = csiv1alpha1.AddToScheme(scheme)
 
 	// +kubebuilder:scaffold:scheme
 }
@@ -251,6 +254,15 @@ func enableClusterBootstrapAndConfigControllers(ctx context.Context, mgr ctrl.Ma
 		os.Exit(1)
 	}
 
+	if err := (&csicontroller.VSphereCSIConfigReconciler{
+		Client: mgr.GetClient(),
+		Log:    ctrl.Log.WithName("VSphereCSIConfig"),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1}); err != nil {
+		setupLog.Error(err, "unable to create CSIConfigController", "controller", "vspherecsi")
+		os.Exit(1)
+	}
+
 	bootstrapReconciler := controllers.NewClusterBootstrapReconciler(
 		mgr.GetClient(),
 		ctrl.Log.WithName("ClusterBootstrapController"),
diff --git a/addons/pkg/constants/constants.go b/addons/pkg/constants/constants.go
index 179da11a21..48b9a4dfec 100644
--- a/addons/pkg/constants/constants.go
+++ b/addons/pkg/constants/constants.go
@@ -20,6 +20,12 @@ const (
 	// CPIAddonName is name of the cloud-provider-vsphere addon
 	CPIAddonName = "vsphere-cpi"
 
+	// PVCSIAddonName is name of the vsphere-pv-csi addon
+	PVCSIAddonName = "vsphere-pv-csi"
+
+	// CSIAddonName is name of the vsphere-csi addon
+	CSIAddonName = "vsphere-csi"
+
 	// TKGBomNamespace is the TKG add on BOM namespace.
 	TKGBomNamespace = "tkr-system"
 
diff --git a/apis/csi/v1alpha1/groupversion_info.go b/apis/csi/v1alpha1/groupversion_info.go
new file mode 100644
index 0000000000..dd5f689ac6
--- /dev/null
+++ b/apis/csi/v1alpha1/groupversion_info.go
@@ -0,0 +1,23 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package v1alpha1 contains API Schema definitions for the csi v1alpha1 API group
+//+kubebuilder:object:generate=true
+//+groupName=csi.tanzu.vmware.com
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+	// GroupVersion is group version used to register these objects
+	GroupVersion = schema.GroupVersion{Group: "csi.tanzu.vmware.com", Version: "v1alpha1"}
+
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+	// AddToScheme adds the types in this group-version to the given scheme.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/apis/csi/v1alpha1/vspherecsiconfig_types.go b/apis/csi/v1alpha1/vspherecsiconfig_types.go
new file mode 100644
index 0000000000..c71bc52cb2
--- /dev/null
+++ b/apis/csi/v1alpha1/vspherecsiconfig_types.go
@@ -0,0 +1,123 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
+// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
+
+// VSphereCSIConfigSpec defines the desired state of VSphereCSIConfig
+type VSphereCSIConfigSpec struct {
+	VSphereCSI VSphereCSI `json:"vsphereCSI"`
+}
+
+// VSphereCSIConfigStatus defines the observed state of VSphereCSIConfig
+type VSphereCSIConfigStatus struct {
+	// Name of the secret created by csi controller
+	//+ kubebuilder:validation:Optional
+	SecretRef *string `json:"secretRef,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+
+// VSphereCSIConfig is the Schema for the vspherecsiconfigs API
+type VSphereCSIConfig struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   VSphereCSIConfigSpec   `json:"spec,omitempty"`
+	Status VSphereCSIConfigStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// VSphereCSIConfigList contains a list of VSphereCSIConfig
+type VSphereCSIConfigList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []VSphereCSIConfig `json:"items"`
+}
+
+type VSphereCSI struct {
+	// The vSphere mode. Either `vsphereCSI` or `vsphereParavirtualCSI`.
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:Enum=vsphereCSI;vsphereParavirtualCSI
+	Mode string `json:"mode"`
+
+	*NonParavirtualConfig `json:"config,omitempty"`
+}
+
+type NonParavirtualConfig struct {
+	// +kubebuilder:validation:Optional
+	TLSThumbprint string `json:"tlsThumbprint,omitempty"`
+
+	// The namespace csi components are to be deployed in
+	// +kubebuilder:validation:Optional
+	Namespace string `json:"namespace"`
+
+	// +kubebuilder:validation:Optional
+	ClusterName string `json:"clusterName"`
+
+	// +kubebuilder:validation:Optional
+	Server string `json:"server"`
+
+	// +kubebuilder:validation:Optional
+	Datacenter string `json:"datacenter"`
+
+	// +kubebuilder:validation:Optional
+	PublicNetwork string `json:"publicNetwork"`
+
+	// +kubebuilder:validation:Optional
+	Username string `json:"username"`
+
+	// +kubebuilder:validation:Optional
+	Password string `json:"password"`
+
+	// +kubebuilder:validation:Optional
+	Region string `json:"region,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	Zone string `json:"zone,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	InsecureFlag *bool `json:"insecureFlag,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	UseTopologyCategories *bool `json:"useTopologyCategories,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	ProvisionTimeout string `json:"provisionTimeout,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	AttachTimeout string `json:"attachTimeout,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	ResizerTimeout string `json:"resizerTimeout,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	VSphereVersion string `json:"vSphereVersion,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	HTTPProxy string `json:"httpProxy,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	HTTPSProxy string `json:"httpsProxy,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	NoProxy string `json:"noProxy,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	DeploymentReplicas *int32 `json:"deploymentReplicas,omitempty"`
+
+	// +kubebuilder:validation:Optional
+	WindowsSupport *bool `json:"windowsSupport,omitempty"`
+}
+
+func init() {
+	SchemeBuilder.Register(&VSphereCSIConfig{}, &VSphereCSIConfigList{})
+}
diff --git a/apis/csi/v1alpha1/zz_generated.deepcopy.go b/apis/csi/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 0000000000..0f7a736117
--- /dev/null
+++ b/apis/csi/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,163 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NonParavirtualConfig) DeepCopyInto(out *NonParavirtualConfig) {
+	*out = *in
+	if in.InsecureFlag != nil {
+		in, out := &in.InsecureFlag, &out.InsecureFlag
+		*out = new(bool)
+		**out = **in
+	}
+	if in.UseTopologyCategories != nil {
+		in, out := &in.UseTopologyCategories, &out.UseTopologyCategories
+		*out = new(bool)
+		**out = **in
+	}
+	if in.DeploymentReplicas != nil {
+		in, out := &in.DeploymentReplicas, &out.DeploymentReplicas
+		*out = new(int32)
+		**out = **in
+	}
+	if in.WindowsSupport != nil {
+		in, out := &in.WindowsSupport, &out.WindowsSupport
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonParavirtualConfig.
+func (in *NonParavirtualConfig) DeepCopy() *NonParavirtualConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(NonParavirtualConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VSphereCSI) DeepCopyInto(out *VSphereCSI) {
+	*out = *in
+	if in.NonParavirtualConfig != nil {
+		in, out := &in.NonParavirtualConfig, &out.NonParavirtualConfig
+		*out = new(NonParavirtualConfig)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereCSI.
+func (in *VSphereCSI) DeepCopy() *VSphereCSI {
+	if in == nil {
+		return nil
+	}
+	out := new(VSphereCSI)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VSphereCSIConfig) DeepCopyInto(out *VSphereCSIConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereCSIConfig.
+func (in *VSphereCSIConfig) DeepCopy() *VSphereCSIConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(VSphereCSIConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VSphereCSIConfig) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VSphereCSIConfigList) DeepCopyInto(out *VSphereCSIConfigList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]VSphereCSIConfig, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereCSIConfigList.
+func (in *VSphereCSIConfigList) DeepCopy() *VSphereCSIConfigList {
+	if in == nil {
+		return nil
+	}
+	out := new(VSphereCSIConfigList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VSphereCSIConfigList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VSphereCSIConfigSpec) DeepCopyInto(out *VSphereCSIConfigSpec) {
+	*out = *in
+	in.VSphereCSI.DeepCopyInto(&out.VSphereCSI)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereCSIConfigSpec.
+func (in *VSphereCSIConfigSpec) DeepCopy() *VSphereCSIConfigSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(VSphereCSIConfigSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VSphereCSIConfigStatus) DeepCopyInto(out *VSphereCSIConfigStatus) {
+	*out = *in
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereCSIConfigStatus.
+func (in *VSphereCSIConfigStatus) DeepCopy() *VSphereCSIConfigStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(VSphereCSIConfigStatus)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/config/crd/bases/csi.tanzu.vmware.com_vspherecsiconfigs.yaml b/config/crd/bases/csi.tanzu.vmware.com_vspherecsiconfigs.yaml
new file mode 100644
index 0000000000..e685c9cd65
--- /dev/null
+++ b/config/crd/bases/csi.tanzu.vmware.com_vspherecsiconfigs.yaml
@@ -0,0 +1,117 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.8.0
+  creationTimestamp: null
+  name: vspherecsiconfigs.csi.tanzu.vmware.com
+spec:
+  group: csi.tanzu.vmware.com
+  names:
+    kind: VSphereCSIConfig
+    listKind: VSphereCSIConfigList
+    plural: vspherecsiconfigs
+    singular: vspherecsiconfig
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: VSphereCSIConfig is the Schema for the vspherecsiconfigs API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: VSphereCSIConfigSpec defines the desired state of VSphereCSIConfig
+            properties:
+              vsphereCSI:
+                properties:
+                  config:
+                    properties:
+                      attachTimeout:
+                        type: string
+                      clusterName:
+                        type: string
+                      datacenter:
+                        type: string
+                      deploymentReplicas:
+                        format: int32
+                        type: integer
+                      httpProxy:
+                        type: string
+                      httpsProxy:
+                        type: string
+                      insecureFlag:
+                        type: boolean
+                      namespace:
+                        description: The namespace csi components are to be deployed
+                          in
+                        type: string
+                      noProxy:
+                        type: string
+                      password:
+                        type: string
+                      provisionTimeout:
+                        type: string
+                      publicNetwork:
+                        type: string
+                      region:
+                        type: string
+                      resizerTimeout:
+                        type: string
+                      server:
+                        type: string
+                      tlsThumbprint:
+                        type: string
+                      useTopologyCategories:
+                        type: boolean
+                      username:
+                        type: string
+                      vSphereVersion:
+                        type: string
+                      windowsSupport:
+                        type: boolean
+                      zone:
+                        type: string
+                    type: object
+                  mode:
+                    description: The vSphere mode. Either `vsphereCSI` or `vsphereParavirtualCSI`.
+                    enum:
+                    - vsphereCSI
+                    - vsphereParavirtualCSI
+                    type: string
+                required:
+                - mode
+                type: object
+            required:
+            - vsphereCSI
+            type: object
+          status:
+            description: VSphereCSIConfigStatus defines the observed state of VSphereCSIConfig
+            properties:
+              secretRef:
+                description: Name of the secret created by csi controller
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 39d9e78b45..373cabd4c6 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -11,6 +11,7 @@ resources:
 - bases/cni.tanzu.vmware.com_antreaconfigs.yaml
 - bases/run.tanzu.vmware.com_kappcontrollerconfigs.yaml
 - bases/cpi.tanzu.vmware.com_cpiconfigs.yaml
+- bases/csi.tanzu.vmware.com_vspherecsiconfigs.yaml
 # +kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
diff --git a/config/rbac/vspherecsiconfig_editor_role.yaml b/config/rbac/vspherecsiconfig_editor_role.yaml
new file mode 100644
index 0000000000..cfd61e9102
--- /dev/null
+++ b/config/rbac/vspherecsiconfig_editor_role.yaml
@@ -0,0 +1,24 @@
+# permissions for end users to edit vspherecsiconfigs.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: vspherecsiconfig-editor-role
+rules:
+- apiGroups:
+  - csi.tanzu.vmware.com
+  resources:
+  - vspherecsiconfigs
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - csi.tanzu.vmware.com
+  resources:
+  - vspherecsiconfigs/status
+  verbs:
+  - get
diff --git a/config/rbac/vspherecsiconfig_viewer_role.yaml b/config/rbac/vspherecsiconfig_viewer_role.yaml
new file mode 100644
index 0000000000..0f5d8b178a
--- /dev/null
+++ b/config/rbac/vspherecsiconfig_viewer_role.yaml
@@ -0,0 +1,20 @@
+# permissions for end users to view csiconfigs.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: vspherecsiconfig-viewer-role
+rules:
+- apiGroups:
+  - csi.tanzu.vmware.com
+  resources:
+  - vspherecsiconfigs
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - csi.tanzu.vmware.com
+  resources:
+  - vspherecsiconfigs/status
+  verbs:
+  - get
diff --git a/packages/management/addons-manager-v2/bundle/config/overlays/feature-gate-clusterbootstrap.yaml b/packages/management/addons-manager-v2/bundle/config/overlays/feature-gate-clusterbootstrap.yaml
index cf1f46634e..5d8b02ec49 100644
--- a/packages/management/addons-manager-v2/bundle/config/overlays/feature-gate-clusterbootstrap.yaml
+++ b/packages/management/addons-manager-v2/bundle/config/overlays/feature-gate-clusterbootstrap.yaml
@@ -9,12 +9,13 @@
 #@ clusterbootstraptemplatescrd = overlay.subset({"kind": "CustomResourceDefinition", "metadata": {"name": "clusterbootstraptemplates.run.tanzu.vmware.com"}})
 #@ kappcontrollerconfigscrd = overlay.subset({"kind": "CustomResourceDefinition", "metadata": {"name": "kappcontrollerconfigs.run.tanzu.vmware.com"}})
 #@ vspherecpiconfigscrd = overlay.subset({"kind": "CustomResourceDefinition", "metadata": {"name": "vspherecpiconfigs.cpi.tanzu.vmware.com"}})
+#@ vspherecsiconfigscrd = overlay.subset({"kind": "CustomResourceDefinition", "metadata": {"name": "vspherecsiconfigs.csi.tanzu.vmware.com"}})
 
 #@ if data.values.tanzuAddonsManager.featureGates.clusterBootstrapController:
 --- #@ template.replace(webhook_manifests())
 #@ end
 
-#@overlay/match by=overlay.or_op(antreaconfigscrd, calicoconfigscrd, clusterbootstrapscrd, clusterbootstraptemplatescrd, kappcontrollerconfigscrd, vspherecpiconfigscrd), expects=6
+#@overlay/match by=overlay.or_op(antreaconfigscrd, calicoconfigscrd, clusterbootstrapscrd, clusterbootstraptemplatescrd, kappcontrollerconfigscrd, vspherecpiconfigscrd, vspherecsiconfigscrd), expects=7
 #@ if/end not data.values.tanzuAddonsManager.featureGates.clusterBootstrapController:
 #@overlay/remove
 
diff --git a/packages/management/addons-manager-v2/bundle/config/upstream/addons-manager.yaml b/packages/management/addons-manager-v2/bundle/config/upstream/addons-manager.yaml
index deef09144b..ca773173af 100644
--- a/packages/management/addons-manager-v2/bundle/config/upstream/addons-manager.yaml
+++ b/packages/management/addons-manager-v2/bundle/config/upstream/addons-manager.yaml
@@ -82,6 +82,38 @@ rules:
   - watch
   - update
   - patch
+#! permissions are required for addons-manager to reconcile vspherecsiconfigs and CSI related service accounts
+- apiGroups:
+  - csi.tanzu.vmware.com
+  resources:
+  - vspherecsiconfigs
+  verbs:
+  - get
+  - list
+  - watch
+  - create
+  - update
+  - patch
+  - delete
+- apiGroups:
+  - csi.tanzu.vmware.com
+  resources:
+  - vspherecsiconfigs/status
+  verbs:
+  - get
+  - update
+  - patch
+- apiGroups:
+  - vmware.infrastructure.cluster.x-k8s.io
+  resources:
+  - providerserviceaccounts
+  verbs:
+  - get
+  - create
+  - list
+  - watch
+  - update
+  - patch
 #! permissions are required for addons-manager to create/update resources in the ProviderRef which are dynamic.
 - apiGroups: ['*']
   resources: ['*']


Test patch:
diff --git a/addons/controllers/suite_test.go b/addons/controllers/suite_test.go
index 25edf1b48d..1b20bc98ec 100644
--- a/addons/controllers/suite_test.go
+++ b/addons/controllers/suite_test.go
@@ -40,6 +40,7 @@ import (
 	antrea "github.com/vmware-tanzu/tanzu-framework/addons/controllers/antrea"
 	calico "github.com/vmware-tanzu/tanzu-framework/addons/controllers/calico"
 	cpi "github.com/vmware-tanzu/tanzu-framework/addons/controllers/cpi"
+	csi "github.com/vmware-tanzu/tanzu-framework/addons/controllers/csi"
 	kappcontroller "github.com/vmware-tanzu/tanzu-framework/addons/controllers/kapp-controller"
 	addonconfig "github.com/vmware-tanzu/tanzu-framework/addons/pkg/config"
 	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/constants"
@@ -47,6 +48,7 @@ import (
 	testutil "github.com/vmware-tanzu/tanzu-framework/addons/testutil"
 	cniv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/cni/v1alpha1"
 	cpiv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/cpi/v1alpha1"
+	csiv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/csi/v1alpha1"
 	runtanzuv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha1"
 	runtanzuv1alpha3 "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha3"
 	"github.com/vmware-tanzu/tanzu-framework/pkg/v1/webhooks"
@@ -157,6 +159,9 @@ var _ = BeforeSuite(func(done Done) {
 	err = cpiv1alpha1.AddToScheme(scheme)
 	Expect(err).NotTo(HaveOccurred())
 
+	err = csiv1alpha1.AddToScheme(scheme)
+	Expect(err).NotTo(HaveOccurred())
+
 	err = capvv1beta1.AddToScheme(scheme)
 	Expect(err).NotTo(HaveOccurred())
 
@@ -240,6 +245,12 @@ var _ = BeforeSuite(func(done Done) {
 		Scheme: mgr.GetScheme(),
 	}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 
+	Expect((&csi.VSphereCSIConfigReconciler{
+		Client: mgr.GetClient(),
+		Log:    ctrl.Log.WithName("controllers").WithName("VSphereCSIConfig"),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+
 	Expect((&antrea.AntreaConfigReconciler{
 		Client: mgr.GetClient(),
 		Log:    ctrl.Log.WithName("controllers").WithName("AntreaConfig"),
diff --git a/addons/controllers/testdata/test-vsphere-csi-non-paravirtual-minimal.yaml b/addons/controllers/testdata/test-vsphere-csi-non-paravirtual-minimal.yaml
new file mode 100644
index 0000000000..ee4ad09fc6
--- /dev/null
+++ b/addons/controllers/testdata/test-vsphere-csi-non-paravirtual-minimal.yaml
@@ -0,0 +1,74 @@
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: test-cluster-csi
+  namespace: default
+  annotations:
+    tkg.tanzu.vmware.com/tkg-http-proxy: "foo.com"
+    tkg.tanzu.vmware.com/tkg-https-proxy: "bar.com"
+    tkg.tanzu.vmware.com/tkg-no-proxy: "foobar.com"
+    tkg.tanzu.vmware.com/tkg-ip-family: "ipv6"
+spec:
+  infrastructureRef:
+    kind: VSphereCluster
+  clusterNetwork:
+    pods:
+      cidrBlocks: [ "192.168.0.0/16","fd00:100:96::/48" ]
+  topology:
+    class: test-clusterclass-tcbt
+    version: v1.22.3
+    variables:
+      - name: VSPHERE_NETWORK
+        value: "3.7.9.0/16"
+      - name: VSPHERE_REGION
+        value: mombasa
+      - name: VSPHERE_ZONE
+        value: kisauni
+      - name: VSPHERE_VERSION
+        value: "8.3.7-release"
+      - name: IS_WINDOWS_WORKLOAD_CLUSTER
+        value: true
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: test-cluster-csi
+  namespace: default
+data:
+  password: QWRtaW4hMjM= # Admin!23
+  username: YWRtaW5pc3RyYXRvckB2c3BoZXJlLmxvY2Fs # administrator@vsphere.local
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: VSphereCluster
+metadata:
+  name: test-cluster-csi
+  namespace: default
+spec:
+  thumbprint: thumbprint-yadayada
+  server: vsphere-server.local
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: VSphereMachineTemplate
+metadata:
+  name: test-cluster-csi-control-plane
+  namespace: default
+spec:
+  template:
+    spec:
+      datacenter: dc0
+      template: /dc0/vm/photon-3-kube-v1.22.3+vmware.1-tkg.2
+      network:
+        devices:
+        - networkName: test-network
+          dhcp6: true
+          dhcp4: false
+---
+apiVersion: csi.tanzu.vmware.com/v1alpha1
+kind: VSphereCSIConfig
+metadata:
+  name: test-cluster-csi
+  namespace: default
+spec:
+  vsphereCSI:
+    mode: vsphereCSI
+
diff --git a/addons/controllers/testdata/test-vsphere-csi-non-paravirtual.yaml b/addons/controllers/testdata/test-vsphere-csi-non-paravirtual.yaml
new file mode 100644
index 0000000000..9970982e46
--- /dev/null
+++ b/addons/controllers/testdata/test-vsphere-csi-non-paravirtual.yaml
@@ -0,0 +1,85 @@
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: test-cluster-csi
+  namespace: default
+  annotations:
+    tkg.tanzu.vmware.com/tkg-http-proxy: "foo.com"
+    tkg.tanzu.vmware.com/tkg-https-proxy: "bar.com"
+    tkg.tanzu.vmware.com/tkg-no-proxy: "foobar.com"
+    tkg.tanzu.vmware.com/tkg-ip-family: "ipv6"
+spec:
+  infrastructureRef:
+    kind: VSphereCluster
+  clusterNetwork:
+    pods:
+      cidrBlocks: [ "192.168.0.0/16","fd00:100:96::/48" ]
+  topology:
+    class: test-clusterclass-tcbt
+    version: v1.22.3
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: test-cluster-csi
+  namespace: default
+data:
+  password: QWRtaW4hMjM= # Admin!23
+  username: YWRtaW5pc3RyYXRvckB2c3BoZXJlLmxvY2Fs # administrator@vsphere.local
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: VSphereCluster
+metadata:
+  name: test-cluster-csi
+  namespace: default
+spec:
+  thumbprint: test-thumbprint
+  server: vsphere-server.local
+---
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+kind: VSphereMachineTemplate
+metadata:
+  name: test-cluster-csi-control-plane
+  namespace: default
+spec:
+  template:
+    spec:
+      datacenter: dc0
+      template: /dc0/vm/photon-3-kube-v1.22.3+vmware.1-tkg.2
+      network:
+        devices:
+        - networkName: test-network
+          dhcp6: true
+          dhcp4: false
+---
+apiVersion: csi.tanzu.vmware.com/v1alpha1
+kind: VSphereCSIConfig
+metadata:
+  name: test-cluster-csi
+  namespace: default
+spec:
+  vsphereCSI:
+    mode: vsphereCSI
+    config:
+      tlsThumbprint: "yadayada"
+      namespace: default
+      clusterName: test-clustername
+      server: svr-0
+      datacenter: dc0
+      publicNetwork: "8.2.0.0/16"
+      username: "administrator@vsphere.local"
+      password: test-passwd
+      region: test-region
+      zone: test-zone
+      insecureFlag: false
+      useTopologyCategories: true
+      provisionTimeout: 33s
+      attachTimeout: 77s
+      resizerTimeout: 99s
+      vSphereVersion: 8.1.2-release
+      httpProxy: "1.1.1.1"
+      httpsProxy: "2.2.2.2"
+      noProxy: "3.3.3.3"
+      deploymentReplicas: 3
+      windowsSupport: true
+
diff --git a/addons/controllers/testdata/test-vsphere-csi-paravirtual.yaml b/addons/controllers/testdata/test-vsphere-csi-paravirtual.yaml
new file mode 100644
index 0000000000..84ae67fa50
--- /dev/null
+++ b/addons/controllers/testdata/test-vsphere-csi-paravirtual.yaml
@@ -0,0 +1,23 @@
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: test-cluster-pv-csi
+  namespace: default
+spec:
+  infrastructureRef:
+    kind: VSphereCluster
+  clusterNetwork:
+    pods:
+      cidrBlocks: [ "192.168.0.0/16","fd00:100:96::/48" ]
+  topology:
+    class: test-clusterclass-tcbt
+    version: v1.22.3
+---
+apiVersion: csi.tanzu.vmware.com/v1alpha1
+kind: VSphereCSIConfig
+metadata:
+  name: test-cluster-pv-csi
+  namespace: default
+spec:
+  vsphereCSI:
+    mode: vsphereParavirtualCSI
diff --git a/addons/controllers/vspherecsiconfig_controller_test.go b/addons/controllers/vspherecsiconfig_controller_test.go
new file mode 100644
index 0000000000..55a09f58e2
--- /dev/null
+++ b/addons/controllers/vspherecsiconfig_controller_test.go
@@ -0,0 +1,317 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+import (
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+	capvvmwarev1beta1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
+	clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/constants"
+	"github.com/vmware-tanzu/tanzu-framework/addons/testutil"
+	csiv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/csi/v1alpha1"
+)
+
+var _ = Describe("VSphereCSIConfig Reconciler", func() {
+	const (
+		clusterNamespace = "default"
+	)
+
+	var (
+		key                     client.ObjectKey
+		clusterName             string
+		clusterResourceFilePath string
+	)
+
+	JustBeforeEach(func() {
+		By("Creating cluster and VSphereCSIConfig resources")
+		key = client.ObjectKey{
+			Namespace: clusterNamespace,
+			Name:      clusterName,
+		}
+		f, err := os.Open(clusterResourceFilePath)
+		Expect(err).ToNot(HaveOccurred())
+		defer f.Close()
+		err = testutil.CreateResources(f, cfg, dynamicClient)
+		Expect(err).ToNot(HaveOccurred())
+	})
+
+	AfterEach(func() {
+		By("Deleting cluster and VSphereCSIConfig resources")
+		for _, filePath := range []string{clusterResourceFilePath} {
+			f, err := os.Open(filePath)
+			Expect(err).ToNot(HaveOccurred())
+			err = testutil.DeleteResources(f, cfg, dynamicClient, true)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(f.Close()).ToNot(HaveOccurred())
+		}
+	})
+
+	Context("reconcile VSphereCSIConfig manifests in non-paravirtual mode", func() {
+
+		const (
+			testClusterCsiName = "test-cluster-csi"
+		)
+
+		Context("using fully populated VSphereCSIConfig", func() {
+			BeforeEach(func() {
+				clusterName = testClusterCsiName
+				clusterResourceFilePath = "testdata/test-vsphere-csi-non-paravirtual.yaml"
+			})
+
+			It("Should reconcile VSphereCSIConfig and create data values secret for VSphereCSIConfig on management cluster", func() {
+				cluster := &clusterapiv1beta1.Cluster{}
+				Eventually(func() bool {
+					if err := k8sClient.Get(ctx, key, cluster); err != nil {
+						return false
+					}
+					return true
+				}, waitTimeout, pollingInterval).Should(BeTrue())
+
+				// the csi config object should be deployed
+				config := &csiv1alpha1.VSphereCSIConfig{}
+				Eventually(func() bool {
+					if err := k8sClient.Get(ctx, key, config); err != nil {
+						return false
+					}
+					Expect(config.Spec.VSphereCSI.Mode).Should(Equal("vsphereCSI"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig).NotTo(BeZero())
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.TLSThumbprint).Should(Equal("yadayada"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.Namespace).Should(Equal("default"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.ClusterName).Should(Equal("test-clustername"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.Server).Should(Equal("svr-0"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.Datacenter).Should(Equal("dc0"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.PublicNetwork).Should(Equal("8.2.0.0/16"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.Username).Should(Equal("administrator@vsphere.local"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.Password).Should(Equal("test-passwd"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.Region).Should(Equal("test-region"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.Zone).Should(Equal("test-zone"))
+					Expect(*config.Spec.VSphereCSI.NonParavirtualConfig.InsecureFlag).Should(Equal(false))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.UseTopologyCategories).NotTo(BeZero())
+					Expect(*config.Spec.VSphereCSI.NonParavirtualConfig.UseTopologyCategories).Should(Equal(true))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.ProvisionTimeout).Should(Equal("33s"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.AttachTimeout).Should(Equal("77s"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.ResizerTimeout).Should(Equal("99s"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.VSphereVersion).Should(Equal("8.1.2-release"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.HTTPProxy).Should(Equal("1.1.1.1"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.HTTPSProxy).Should(Equal("2.2.2.2"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.NoProxy).Should(Equal("3.3.3.3"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.DeploymentReplicas).NotTo(BeZero())
+					Expect(*config.Spec.VSphereCSI.NonParavirtualConfig.DeploymentReplicas).Should(Equal(int32(3)))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig.WindowsSupport).NotTo(BeZero())
+					Expect(*config.Spec.VSphereCSI.NonParavirtualConfig.WindowsSupport).Should(Equal(true))
+
+					if len(config.OwnerReferences) == 0 {
+						return false
+					}
+					Expect(len(config.OwnerReferences)).Should(Equal(1))
+					Expect(config.OwnerReferences[0].Name).Should(Equal(clusterName))
+
+					return true
+				}, waitTimeout, pollingInterval).Should(BeTrue())
+
+				// the data values secret should be generated
+				secret := &v1.Secret{}
+				Eventually(func() bool {
+					secretKey := client.ObjectKey{
+						Namespace: clusterNamespace,
+						Name:      fmt.Sprintf("%s-%s-data-values", clusterName, constants.CSIAddonName),
+					}
+					if err := k8sClient.Get(ctx, secretKey, secret); err != nil {
+						return false
+					}
+					secretData := string(secret.Data["values.yaml"])
+					Expect(len(secretData)).Should(Not(BeZero()))
+					fmt.Println(secretData) // debug dump
+					Expect(strings.Contains(secretData, "vsphereCSI:")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "tlsThumbprint: yadayada")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "namespace: default")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "server: svr-0")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "datacenter: dc0")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "publicNetwork: 8.2.0.0/16")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "username: administrator@vsphere.local")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "password: test-passwd")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "region: test-region")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "zone: test-zone")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "insecureFlag: false")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "useTopologyCategories: true")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "provisionTimeout: 33s")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "attachTimeout: 77s")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "resizerTimeout: 99s")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "vSphereVersion: 8.1.2-release")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "http_proxy: 1.1.1.1")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "https_proxy: 2.2.2.2")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "no_proxy: 3.3.3.3")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "deployment_replicas: 3")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "windows_support: true")).Should(BeTrue())
+
+					return true
+				}, waitTimeout, pollingInterval).Should(BeTrue())
+
+				// eventually the secret ref to the data values should be updated
+				Eventually(func() bool {
+					if err := k8sClient.Get(ctx, key, config); err != nil {
+						return false
+					}
+					Expect(config.Status.SecretRef).To(Equal(fmt.Sprintf("%s-%s-data-values", clusterName, constants.CSIAddonName)))
+					return true
+				})
+			})
+
+		})
+
+		Context("using minimally populated VSphereCSIConfig", func() {
+			BeforeEach(func() {
+				clusterName = testClusterCsiName
+				clusterResourceFilePath = "testdata/test-vsphere-csi-non-paravirtual-minimal.yaml"
+			})
+
+			It("Should reconcile VSphereCSIConfig and create data values secret for VSphereCSIConfig", func() {
+				cluster := &clusterapiv1beta1.Cluster{}
+				Eventually(func() bool {
+					if err := k8sClient.Get(ctx, key, cluster); err != nil {
+						return false
+					}
+					return true
+				}, waitTimeout, pollingInterval).Should(BeTrue())
+
+				// the csi config object should be deployed
+				config := &csiv1alpha1.VSphereCSIConfig{}
+				Eventually(func() bool {
+					if err := k8sClient.Get(ctx, key, config); err != nil {
+						return false
+					}
+					Expect(config.Spec.VSphereCSI.Mode).Should(Equal("vsphereCSI"))
+					Expect(config.Spec.VSphereCSI.NonParavirtualConfig).To(BeZero())
+
+					if len(config.OwnerReferences) == 0 {
+						return false
+					}
+					Expect(len(config.OwnerReferences)).Should(Equal(1))
+					Expect(config.OwnerReferences[0].Name).Should(Equal(clusterName))
+
+					return true
+				}, waitTimeout, pollingInterval).Should(BeTrue())
+
+				// the data values secret should be generated
+				secret := &v1.Secret{}
+				Eventually(func() bool {
+					secretKey := client.ObjectKey{
+						Namespace: clusterNamespace,
+						Name:      fmt.Sprintf("%s-%s-data-values", clusterName, constants.CSIAddonName),
+					}
+					if err := k8sClient.Get(ctx, secretKey, secret); err != nil {
+						return false
+					}
+					secretData := string(secret.Data["values.yaml"])
+					Expect(len(secretData)).Should(Not(BeZero()))
+					fmt.Println(secretData) // debug dump
+					Expect(strings.Contains(secretData, "vsphereCSI:")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "tlsThumbprint: thumbprint-yadayada")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "namespace: kube-system")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "server: vsphere-server.local")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "datacenter: dc0")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "publicNetwork: 3.7.9.0/16")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "username: administrator@vsphere.local")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "password: Admin!23")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "region: mombasa")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "zone: kisauni")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "insecureFlag: true")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "useTopologyCategories: true")).Should(BeFalse())
+					Expect(strings.Contains(secretData, "provisionTimeout: 300s")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "attachTimeout: 300s")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "resizerTimeout: 300s")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "vSphereVersion: 8.3.7-release")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "http_proxy: foo.com")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "https_proxy: bar.com")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "no_proxy: foobar.com")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "deployment_replicas: 3")).Should(BeTrue())
+					Expect(strings.Contains(secretData, "windows_support: true")).Should(BeTrue())
+
+					return true
+				}, waitTimeout, pollingInterval).Should(BeTrue())
+
+				// eventually the secret ref to the data values should be updated
+				Eventually(func() bool {
+					if err := k8sClient.Get(ctx, key, config); err != nil {
+						return false
+					}
+					Expect(config.Status.SecretRef).To(Equal(fmt.Sprintf("%s-%s-data-values", clusterName, constants.CSIAddonName)))
+					return true
+				})
+			})
+
+		})
+
+	})
+
+	Context("reconcile VSphereCSIConfig manifests in paravirtual mode", func() {
+		BeforeEach(func() {
+			clusterName = "test-cluster-pv-csi"
+			clusterResourceFilePath = "testdata/test-vsphere-csi-paravirtual.yaml"
+		})
+		It("Should reconcile VSphereCSIConfig and create data values secret for VSphereCSIConfig on management cluster", func() {
+			// the data values secret should be generated
+			secret := &v1.Secret{}
+			Eventually(func() bool {
+				secretKey := client.ObjectKey{
+					Namespace: clusterNamespace,
+					Name:      fmt.Sprintf("%s-%s-data-values", clusterName, constants.PVCSIAddonName),
+				}
+				if err := k8sClient.Get(ctx, secretKey, secret); err != nil {
+					return false
+				}
+				secretData := string(secret.Data["values.yaml"])
+				fmt.Println(secretData) // debug dump
+				Expect(len(secretData)).Should(Not(BeZero()))
+				Expect(strings.Contains(secretData, "vspherePVCSI:")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "cluster_name: test-cluster-pv-csi")).Should(BeTrue())
+				match, _ := regexp.MatchString("cluster_uid: [a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", secretData)
+				Expect(match).Should(BeTrue())
+				Expect(strings.Contains(secretData, "namespace: vmware-system-csi")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "supervisor_master_endpoint_hostname: supervisor.default.svc")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "supervisor_master_port: 6443")).Should(BeTrue())
+
+				return true
+			}, waitTimeout, pollingInterval).Should(BeTrue())
+
+			// eventually the secret ref to the data values should be updated
+			config := &csiv1alpha1.VSphereCSIConfig{}
+			Eventually(func() bool {
+				if err := k8sClient.Get(ctx, key, config); err != nil {
+					return false
+				}
+				Expect(config.Status.SecretRef).To(Equal(fmt.Sprintf("%s-%s-data-values", clusterName, constants.PVCSIAddonName)))
+				return true
+			})
+		})
+
+		It("Should reconcile ProviderServiceAccount", func() {
+			serviceAccount := &capvvmwarev1beta1.ProviderServiceAccount{}
+			Eventually(func() bool {
+				serviceAccountKey := client.ObjectKey{
+					Namespace: clusterNamespace,
+					Name:      fmt.Sprintf("%s-%s", clusterName, "pvcsi"),
+				}
+				if err := k8sClient.Get(ctx, serviceAccountKey, serviceAccount); err != nil {
+					return false
+				}
+				Expect(serviceAccount.Spec.Ref.Name).To(Equal(key.Name))
+				Expect(serviceAccount.Spec.Ref.Namespace).To(Equal(key.Namespace))
+				Expect(serviceAccount.Spec.Rules).To(HaveLen(6))
+				Expect(serviceAccount.Spec.TargetNamespace).To(Equal("vmware-system-csi"))
+				Expect(serviceAccount.Spec.TargetSecretName).To(Equal("pvcsi-provider-creds"))
+				return true
+			})
+		})
+	})
+})