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

Issue:
KappConfig 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 kappConfig CRD and controller to create data values secret


Discussion / hints:

Fix patch:
diff --git a/PROJECT b/PROJECT
index ad80aea439..c3ecba3bd3 100644
--- a/PROJECT
+++ b/PROJECT
@@ -23,14 +23,16 @@ resources:
 - group: cni
   kind: CalicoConfig
   version: v1alpha1
+- group: cni
+  kind: AntreaConfig
+  version: v1alpha1
 - group: run
   kind: TanzuClusterBootstrap
   version: v1alpha3
 - group: run
   kind: TanzuClusterBootstrapTemplate
   version: v1alpha3
-- group: addons
-- group: cni
-  kind: AntreaConfig
-  version: v1alpha1
+- group: run
+  kind: KappControllerConfig
+  version: v1alpha3
 version: "2"
diff --git a/addons/controllers/antrea/antreaconfig_controller.go b/addons/controllers/antrea/antreaconfig_controller.go
index 91a009405a..e908995003 100644
--- a/addons/controllers/antrea/antreaconfig_controller.go
+++ b/addons/controllers/antrea/antreaconfig_controller.go
@@ -162,9 +162,7 @@ func (r *AntreaConfigReconciler) ReconcileAntreaConfigNormal(
 
 	// update status.secretRef
 	dataValueSecretName := util.GenerateDataValueSecretName(cluster.Name, constants.AntreaAddonName)
-	if antreaConfig.Status.SecretRef != dataValueSecretName {
-		antreaConfig.Status.SecretRef = dataValueSecretName
-	}
+	antreaConfig.Status.SecretRef = dataValueSecretName
 
 	return nil
 }
@@ -194,14 +192,13 @@ func (r *AntreaConfigReconciler) ReconcileAntreaConfigDataValue(
 			return err
 		}
 
-		yamlBytes, err := yaml.Marshal(antreaConfigYaml)
+		dataValueYamlBytes, err := yaml.Marshal(antreaConfigYaml)
 		if err != nil {
 			log.Error(err, "Error marshaling AntreaConfig to Yaml")
 			return err
 		}
 
-		dataValueBytes := append([]byte(constants.TKGDataValueFormatString), yamlBytes...)
-		antreaDataValuesSecret.Data[constants.TKGDataValueFileName] = dataValueBytes
+		antreaDataValuesSecret.Data[constants.TKGDataValueFileName] = dataValueYamlBytes
 
 		return nil
 	}
diff --git a/addons/controllers/calico/calicoconfig_controller.go b/addons/controllers/calico/calicoconfig_controller.go
index f6e02d36be..dcf62709b9 100644
--- a/addons/controllers/calico/calicoconfig_controller.go
+++ b/addons/controllers/calico/calicoconfig_controller.go
@@ -159,9 +159,8 @@ func (r *CalicoConfigReconciler) ReconcileCalicoConfigNormal(
 	}
 
 	// add the name of the data values secret in the CalicoConfig Status field
-	if calicoConfig.Status.SecretRef != secretNamespacedName.Name {
-		calicoConfig.Status.SecretRef = secretNamespacedName.Name
-	}
+	calicoConfig.Status.SecretRef = secretNamespacedName.Name
+
 	return nil
 }
 
@@ -193,13 +192,12 @@ func (r *CalicoConfigReconciler) ReconcileCalicoDataValuesSecret(
 			return err
 		}
 
-		yamlBytes, err := yaml.Marshal(calicoConfigYaml)
+		dataValueYamlBytes, err := yaml.Marshal(calicoConfigYaml)
 		if err != nil {
 			r.Log.Error(err, "Error marshaling CalicoConfig to Yaml")
 			return err
 		}
-		dataValueBytes := append([]byte(constants.TKGDataValueFormatString), yamlBytes...)
-		calicoDataValuesSecret.Data[constants.TKGDataValueFileName] = dataValueBytes
+		calicoDataValuesSecret.Data[constants.TKGDataValueFileName] = dataValueYamlBytes
 		return nil
 	}
 
diff --git a/addons/controllers/kapp-controller/kappcontrollerconfig_controller.go b/addons/controllers/kapp-controller/kappcontrollerconfig_controller.go
new file mode 100644
index 0000000000..bc14b0a30a
--- /dev/null
+++ b/addons/controllers/kapp-controller/kappcontrollerconfig_controller.go
@@ -0,0 +1,202 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package controllers implements k8s controller functionality for kapp-controller config CRD.
+package controllers
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/go-logr/logr"
+	"gopkg.in/yaml.v2"
+	corev1 "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"
+	"k8s.io/utils/pointer"
+	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"
+
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/constants"
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/util"
+	runv1alpha3 "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha3"
+)
+
+const KappControllerAddonName = "kapp-controller"
+
+// KappControllerConfigReconciler reconciles a KappControllerConfig object
+type KappControllerConfigReconciler struct {
+	client.Client
+	Log    logr.Logger
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups=run.tanzu.vmware.com,resources=kappcontrollerconfigs,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=run.tanzu.vmware.com,resources=kappcontrollerconfigs/status,verbs=get;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 *KappControllerConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log := r.Log.WithValues("kappControllerConfig", req.NamespacedName)
+
+	// get kapp-controller config object
+	kappControllerConfig := &runv1alpha3.KappControllerConfig{}
+	if err := r.Client.Get(ctx, req.NamespacedName, kappControllerConfig); err != nil {
+		log.Error(err, "Unable to fetch kappControllerConfig")
+		return ctrl.Result{}, nil
+	}
+	// Deepcopy to prevent client-go cache conflict
+	kappControllerConfig = kappControllerConfig.DeepCopy()
+
+	// get the parent cluster name from owner reference
+	// if the owner reference doesn't exist, use the same name as config CR
+	clusterNamespacedName := req.NamespacedName
+	cluster := &clusterapiv1beta1.Cluster{}
+	for _, owner := range kappControllerConfig.OwnerReferences {
+		if owner.Kind == cluster.Kind {
+			clusterNamespacedName.Name = owner.Name
+			break
+		}
+	}
+
+	// verify that the cluster related to config is present
+	if err := r.Client.Get(ctx, clusterNamespacedName, cluster); err != nil {
+		if apierrors.IsNotFound(err) {
+			log.Info(fmt.Sprintf("Cluster %s/%s not found", clusterNamespacedName.Namespace, clusterNamespacedName.Name))
+			return ctrl.Result{}, nil
+		}
+
+		log.Error(err, fmt.Sprintf("unable to fetch cluster %s/%s", clusterNamespacedName.Namespace, clusterNamespacedName.Name))
+		return ctrl.Result{}, err
+	}
+
+	if retResult, err := r.ReconcileKappControllerConfig(ctx, kappControllerConfig, cluster, log); err != nil {
+		log.Error(err, "unable to reconcile kappControllerConfig")
+		return retResult, err
+	}
+
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *KappControllerConfigReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&runv1alpha3.KappControllerConfig{}).
+		WithOptions(options).
+		Complete(r)
+}
+
+// ReconcileKappControllerConfig reconciles KappControllerConfig CR
+func (r *KappControllerConfigReconciler) ReconcileKappControllerConfig(
+	ctx context.Context,
+	kappControllerConfig *runv1alpha3.KappControllerConfig,
+	cluster *clusterapiv1beta1.Cluster,
+	log logr.Logger) (_ ctrl.Result, retErr error) {
+
+	patchHelper, err := clusterapipatchutil.NewHelper(kappControllerConfig, r.Client)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+	// Patch KappControllerConfig before returning the function
+	defer func() {
+		log.Info("Patching kappControllerConfig")
+
+		if err := patchHelper.Patch(ctx, kappControllerConfig); err != nil {
+			log.Error(err, "Error patching kappControllerConfig")
+			retErr = err
+		}
+	}()
+
+	if err := r.ReconcileKappControllerConfigNormal(ctx, kappControllerConfig, cluster, log); err != nil {
+		log.Error(err, "Error reconciling kappControllerConfig")
+		return ctrl.Result{}, err
+	}
+
+	log.Info("Successfully reconciled kappControllerConfig")
+
+	return ctrl.Result{}, nil
+}
+
+// ReconcileKappControllerConfigNormal reconciles KappControllerConfig CR
+func (r *KappControllerConfigReconciler) ReconcileKappControllerConfigNormal(
+	ctx context.Context,
+	kappControllerConfig *runv1alpha3.KappControllerConfig,
+	cluster *clusterapiv1beta1.Cluster,
+	log logr.Logger) (retErr error) {
+
+	// add owner reference to kappControllerConfig
+	ownerReference := metav1.OwnerReference{
+		APIVersion:         clusterapiv1beta1.GroupVersion.String(),
+		Kind:               cluster.Kind,
+		Name:               cluster.Name,
+		UID:                cluster.UID,
+		BlockOwnerDeletion: pointer.BoolPtr(false),
+	}
+
+	if !clusterapiutil.HasOwnerRef(kappControllerConfig.OwnerReferences, ownerReference) {
+		log.Info("Adding owner reference to kappControllerConfig")
+		kappControllerConfig.OwnerReferences = clusterapiutil.EnsureOwnerRef(kappControllerConfig.OwnerReferences, ownerReference)
+	}
+
+	if err := r.ReconcileKappControllerConfigDataValue(ctx, kappControllerConfig, cluster, log); err != nil {
+		log.Error(err, "Error creating kappControllerConfig data value secret")
+		return err
+	}
+
+	// update status.secretRef
+	dataValueSecretName := util.GenerateDataValueSecretName(kappControllerConfig.Name, KappControllerAddonName)
+	kappControllerConfig.Status.SecretRef = dataValueSecretName
+
+	return nil
+}
+
+// ReconcileKappControllerConfigDataValue reconciles KappControllerConfig data values secret
+func (r *KappControllerConfigReconciler) ReconcileKappControllerConfigDataValue(
+	ctx context.Context,
+	kappControllerConfig *runv1alpha3.KappControllerConfig,
+	cluster *clusterapiv1beta1.Cluster,
+	log logr.Logger) (retErr error) {
+
+	dataValuesSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      util.GenerateDataValueSecretName(kappControllerConfig.Name, KappControllerAddonName),
+			Namespace: cluster.Namespace,
+		},
+	}
+
+	dataValuesSecretMutateFn := func() error {
+		dataValuesSecret.Type = corev1.SecretTypeOpaque
+		dataValuesSecret.Data = map[string][]byte{}
+
+		// marshall the yaml contents
+		kappConfig, err := mapKappControllerConfigSpec(cluster, kappControllerConfig)
+		if err != nil {
+			return err
+		}
+
+		dataValueYamlBytes, err := yaml.Marshal(kappConfig)
+		if err != nil {
+			return err
+		}
+
+		dataValuesSecret.Data[constants.TKGDataValueFileName] = dataValueYamlBytes
+
+		return nil
+	}
+
+	result, err := controllerutil.CreateOrPatch(ctx, r.Client, dataValuesSecret, dataValuesSecretMutateFn)
+	if err != nil {
+		log.Error(err, "Error creating or patching kappControllerConfig data values secret")
+		return err
+	}
+
+	log.Info(fmt.Sprintf("Resource %s data values secret %s", KappControllerAddonName, result))
+
+	return nil
+}
diff --git a/addons/controllers/kapp-controller/kappcontrollerconfig_types.go b/addons/controllers/kapp-controller/kappcontrollerconfig_types.go
new file mode 100644
index 0000000000..1e3ee6be9a
--- /dev/null
+++ b/addons/controllers/kapp-controller/kappcontrollerconfig_types.go
@@ -0,0 +1,101 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+import (
+	"strings"
+
+	"github.com/pkg/errors"
+	netutils "k8s.io/utils/net"
+	clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
+
+	runv1alpha3 "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha3"
+)
+
+// kappControllerConfigSpec defines the desired state of KappControllerConfig
+type kappControllerConfigSpec struct {
+	Namespace      string         `yaml:"namespace,omitempty"`
+	KappController kappController `yaml:"kappController,omitempty"`
+}
+
+type kappController struct {
+	CreateNamespace bool           `yaml:"createNamespace,omitempty"`
+	GlobalNamespace string         `yaml:"globalNamespace,omitempty"`
+	Deployment      kappDeployment `yaml:"deployment,omitempty"`
+	Config          kappConfig     `yaml:"config,omitempty"`
+}
+
+type kappDeployment struct {
+	CoreDNSIP          string              `yaml:"coreDNSIP,omitempty"`
+	HostNetwork        bool                `yaml:"hostNetwork,omitempty"`
+	PriorityClassName  string              `yaml:"priorityClassName,omitempty"`
+	Concurrency        int                 `yaml:"concurrency,omitempty"`
+	Tolerations        []map[string]string `yaml:"tolerations,omitempty"`
+	APIPort            int                 `yaml:"apiPort,omitempty"`
+	MetricsBindAddress string              `yaml:"metricsBindAddress,omitempty"`
+}
+
+type kappConfig struct {
+	CaCerts                string `yaml:"caCerts,omitempty"`
+	HTTPProxy              string `yaml:"httpProxy,omitempty"`
+	HTTPSProxy             string `yaml:"httpsProxy,omitempty"`
+	NoProxy                string `yaml:"noProxy,omitempty"`
+	DangerousSkipTLSVerify string `yaml:"dangerousSkipTLSVerify,omitempty"`
+}
+
+func getCoreDNSIP(cluster *clusterapiv1beta1.Cluster) (string, error) {
+	var serviceCIDR string
+	if cluster.Spec.ClusterNetwork != nil && cluster.Spec.ClusterNetwork.Services != nil && len(cluster.Spec.ClusterNetwork.Services.CIDRBlocks) > 0 {
+		serviceCIDR = cluster.Spec.ClusterNetwork.Services.CIDRBlocks[0]
+	} else {
+		return "", errors.New("Unable to get cluster serviceCIDR")
+	}
+
+	svcSubnets, err := netutils.ParseCIDRs(strings.Split(serviceCIDR, ","))
+	if err != nil {
+		return "", err
+	}
+	dnsIP, err := netutils.GetIndexedIP(svcSubnets[0], 10)
+	if err != nil {
+		return "", err
+	}
+
+	return dnsIP.String(), nil
+}
+
+func mapKappControllerConfigSpec(cluster *clusterapiv1beta1.Cluster, config *runv1alpha3.KappControllerConfig) (*kappControllerConfigSpec, error) {
+	configSpec := &kappControllerConfigSpec{}
+
+	configSpec.Namespace = config.Spec.Namespace
+
+	// KappController
+	configSpec.KappController.CreateNamespace = config.Spec.KappController.CreateNamespace
+	configSpec.KappController.GlobalNamespace = config.Spec.KappController.GlobalNamespace
+
+	// Deployment
+	configSpec.KappController.Deployment.HostNetwork = config.Spec.KappController.Deployment.HostNetwork
+	// set the coreDNS IP if hostNetwork is enabled
+	if configSpec.KappController.Deployment.HostNetwork {
+		coreDNSIP, err := getCoreDNSIP(cluster)
+		if err != nil {
+			return nil, errors.Wrap(err, "Unable to get cluster CoreDNS IP")
+		}
+		configSpec.KappController.Deployment.CoreDNSIP = coreDNSIP
+	}
+	configSpec.KappController.Deployment.PriorityClassName = config.Spec.KappController.Deployment.PriorityClassName
+	configSpec.KappController.Deployment.Concurrency = config.Spec.KappController.Deployment.Concurrency
+	configSpec.KappController.Deployment.Tolerations = config.Spec.KappController.Deployment.Tolerations
+	configSpec.KappController.Deployment.APIPort = config.Spec.KappController.Deployment.APIPort
+	configSpec.KappController.Deployment.MetricsBindAddress = config.Spec.KappController.Deployment.MetricsBindAddress
+
+	// Config
+	// TODO: consolidate proxy settings as derived fields https://github.com/vmware-tanzu/tanzu-framework/issues/1586
+	configSpec.KappController.Config.CaCerts = config.Spec.KappController.Config.CaCerts
+	configSpec.KappController.Config.HTTPProxy = config.Spec.KappController.Config.HTTPProxy
+	configSpec.KappController.Config.HTTPSProxy = config.Spec.KappController.Config.HTTPSProxy
+	configSpec.KappController.Config.NoProxy = config.Spec.KappController.Config.NoProxy
+	configSpec.KappController.Config.DangerousSkipTLSVerify = config.Spec.KappController.Config.DangerousSkipTLSVerify
+
+	return configSpec, nil
+}
diff --git a/apis/run/v1alpha3/kappcontrollerconfig_types.go b/apis/run/v1alpha3/kappcontrollerconfig_types.go
new file mode 100644
index 0000000000..74787487c7
--- /dev/null
+++ b/apis/run/v1alpha3/kappcontrollerconfig_types.go
@@ -0,0 +1,125 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package v1alpha3
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
+
+// KappControllerConfigSpec defines the desired state of KappControllerConfig
+type KappControllerConfigSpec struct {
+	// The namespace in which kapp-controller is deployed
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:=kube-system
+	Namespace string `json:"namespace,omitempty"`
+
+	KappController KappController `json:"kappController,omitempty"`
+}
+
+type KappController struct {
+	// Whether to create namespace specified for kapp-controller
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:=true
+	CreateNamespace bool `json:"createNamespace,omitempty"`
+
+	// The namespace value used for global packaging resources. Any Package and PackageMetadata CRs within that namespace will be included in all other namespaces on the cluster, without duplicating them
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:=tanzu-package-repo-global
+	GlobalNamespace string `json:"globalNamespace,omitempty"`
+
+	Deployment KappDeployment `json:"deployment,omitempty"`
+
+	Config KappConfig `json:"config,omitempty"`
+}
+
+type KappDeployment struct {
+	// Whether to enable host networking for kapp-controller deployment
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:=true
+	HostNetwork bool `json:"hostNetwork,omitempty"`
+
+	// The priority value that various system components use to find the priority of the kapp-controller pod
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:=system-cluster-critical
+	PriorityClassName string `json:"priorityClassName,omitempty"`
+
+	// Concurrency of kapp-controller deployment
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:=4
+	Concurrency int `json:"concurrency,omitempty"`
+
+	// kapp-controller deployment tolerations
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:={{key: CriticalAddonsOnly, operator: Exists}, {effect: NoSchedule, key: node-role.kubernetes.io/master}, {effect: NoSchedule, key: node.kubernetes.io/not-ready}, {effect: NoSchedule, key: node.cloudprovider.kubernetes.io/uninitialized, value: "true"}}
+	Tolerations []map[string]string `json:"tolerations,omitempty"`
+
+	// Bind port for kapp-controller API
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:=10350
+	APIPort int `json:"apiPort,omitempty"`
+
+	// Address for metrics server
+	//+kubebuilder:validation:Optional
+	//+kubebuilder:default:="0"
+	MetricsBindAddress string `json:"metricsBindAddress,omitempty"`
+}
+
+type KappConfig struct {
+	// A cert chain of trusted CA certs. These will be added to the system-wide cert pool of trusted CA's
+	//+kubebuilder:validation:Optional
+	CaCerts string `json:"caCerts,omitempty"`
+
+	// The url/ip of a proxy for kapp controller to use when making network requests
+	//+kubebuilder:validation:Optional
+	HTTPProxy string `json:"httpProxy,omitempty"`
+
+	// The url/ip of a TLS capable proxy for kapp-controller to use when making network requests
+	//+kubebuilder:validation:Optional
+	HTTPSProxy string `json:"httpsProxy,omitempty"`
+
+	// A comma delimited list of domain names which kapp-controller should bypass the proxy for when making requests
+	//+kubebuilder:validation:Optional
+	NoProxy string `json:"noProxy,omitempty"`
+
+	// A comma delimited list of hostnames for which kapp-controller should skip TLS verification
+	//+kubebuilder:validation:Optional
+	DangerousSkipTLSVerify string `json:"dangerousSkipTLSVerify,omitempty"`
+}
+
+// KappControllerConfigStatus defines the observed state of KappControllerConfig
+type KappControllerConfigStatus struct {
+	// Name of the data value secret created by controller
+	//+kubebuilder:validation:Optional
+	SecretRef string `json:"secretRef,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+//+kubebuilder:printcolumn:name="Namespace",type="string",JSONPath=".spec.namespace",description="The namespace in which kapp-controller is deployed"
+//+kubebuilder:printcolumn:name="GlobalNamespace",type="string",JSONPath=".spec.kappController.globalNamespace",description="The namespace value used for global packaging resources. Any Package and PackageMetadata CRs within that namespace will be included in all other namespaces on the cluster, without duplicating them"
+//+kubebuilder:printcolumn:name="SecretName",type="string",JSONPath=".status.secretName",description="Name of the kapp-controller data values secret"
+
+// KappControllerConfig is the Schema for the kappcontrollerconfigs API
+type KappControllerConfig struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   KappControllerConfigSpec   `json:"spec"`
+	Status KappControllerConfigStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// KappControllerConfigList contains a list of KappControllerConfig
+type KappControllerConfigList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []KappControllerConfig `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&KappControllerConfig{}, &KappControllerConfigList{})
+}
diff --git a/apis/run/v1alpha3/zz_generated.deepcopy.go b/apis/run/v1alpha3/zz_generated.deepcopy.go
index 2a9ce5c43d..2cce4e65ec 100644
--- a/apis/run/v1alpha3/zz_generated.deepcopy.go
+++ b/apis/run/v1alpha3/zz_generated.deepcopy.go
@@ -29,6 +29,156 @@ func (in *ContainerImageInfo) DeepCopy() *ContainerImageInfo {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KappConfig) DeepCopyInto(out *KappConfig) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KappConfig.
+func (in *KappConfig) DeepCopy() *KappConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(KappConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KappController) DeepCopyInto(out *KappController) {
+	*out = *in
+	in.Deployment.DeepCopyInto(&out.Deployment)
+	out.Config = in.Config
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KappController.
+func (in *KappController) DeepCopy() *KappController {
+	if in == nil {
+		return nil
+	}
+	out := new(KappController)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KappControllerConfig) DeepCopyInto(out *KappControllerConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KappControllerConfig.
+func (in *KappControllerConfig) DeepCopy() *KappControllerConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(KappControllerConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *KappControllerConfig) 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 *KappControllerConfigList) DeepCopyInto(out *KappControllerConfigList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]KappControllerConfig, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KappControllerConfigList.
+func (in *KappControllerConfigList) DeepCopy() *KappControllerConfigList {
+	if in == nil {
+		return nil
+	}
+	out := new(KappControllerConfigList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *KappControllerConfigList) 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 *KappControllerConfigSpec) DeepCopyInto(out *KappControllerConfigSpec) {
+	*out = *in
+	in.KappController.DeepCopyInto(&out.KappController)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KappControllerConfigSpec.
+func (in *KappControllerConfigSpec) DeepCopy() *KappControllerConfigSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(KappControllerConfigSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KappControllerConfigStatus) DeepCopyInto(out *KappControllerConfigStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KappControllerConfigStatus.
+func (in *KappControllerConfigStatus) DeepCopy() *KappControllerConfigStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(KappControllerConfigStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KappDeployment) DeepCopyInto(out *KappDeployment) {
+	*out = *in
+	if in.Tolerations != nil {
+		in, out := &in.Tolerations, &out.Tolerations
+		*out = make([]map[string]string, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = make(map[string]string, len(*in))
+				for key, val := range *in {
+					(*out)[key] = val
+				}
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KappDeployment.
+func (in *KappDeployment) DeepCopy() *KappDeployment {
+	if in == nil {
+		return nil
+	}
+	out := new(KappDeployment)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *KubernetesSpec) DeepCopyInto(out *KubernetesSpec) {
 	*out = *in
diff --git a/config/crd/bases/run.tanzu.vmware.com_kappcontrollerconfigs.yaml b/config/crd/bases/run.tanzu.vmware.com_kappcontrollerconfigs.yaml
new file mode 100644
index 0000000000..c50bc8f41f
--- /dev/null
+++ b/config/crd/bases/run.tanzu.vmware.com_kappcontrollerconfigs.yaml
@@ -0,0 +1,160 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: kappcontrollerconfigs.run.tanzu.vmware.com
+spec:
+  group: run.tanzu.vmware.com
+  names:
+    kind: KappControllerConfig
+    listKind: KappControllerConfigList
+    plural: kappcontrollerconfigs
+    singular: kappcontrollerconfig
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The namespace in which kapp-controller is deployed
+      jsonPath: .spec.namespace
+      name: Namespace
+      type: string
+    - description: The namespace value used for global packaging resources. Any Package
+        and PackageMetadata CRs within that namespace will be included in all other
+        namespaces on the cluster, without duplicating them
+      jsonPath: .spec.kappController.globalNamespace
+      name: GlobalNamespace
+      type: string
+    - description: Name of the kapp-controller data values secret
+      jsonPath: .status.secretName
+      name: SecretName
+      type: string
+    name: v1alpha3
+    schema:
+      openAPIV3Schema:
+        description: KappControllerConfig is the Schema for the kappcontrollerconfigs
+          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: KappControllerConfigSpec defines the desired state of KappControllerConfig
+            properties:
+              kappController:
+                properties:
+                  config:
+                    properties:
+                      caCerts:
+                        description: A cert chain of trusted CA certs. These will
+                          be added to the system-wide cert pool of trusted CA's
+                        type: string
+                      dangerousSkipTLSVerify:
+                        description: A comma delimited list of hostnames for which
+                          kapp-controller should skip TLS verification
+                        type: string
+                      httpProxy:
+                        description: The url/ip of a proxy for kapp controller to
+                          use when making network requests
+                        type: string
+                      httpsProxy:
+                        description: The url/ip of a TLS capable proxy for kapp-controller
+                          to use when making network requests
+                        type: string
+                      noProxy:
+                        description: A comma delimited list of domain names which
+                          kapp-controller should bypass the proxy for when making
+                          requests
+                        type: string
+                    type: object
+                  createNamespace:
+                    default: true
+                    description: Whether to create namespace specified for kapp-controller
+                    type: boolean
+                  deployment:
+                    properties:
+                      apiPort:
+                        default: 10350
+                        description: Bind port for kapp-controller API
+                        type: integer
+                      concurrency:
+                        default: 4
+                        description: Concurrency of kapp-controller deployment
+                        type: integer
+                      hostNetwork:
+                        default: true
+                        description: Whether to enable host networking for kapp-controller
+                          deployment
+                        type: boolean
+                      metricsBindAddress:
+                        default: "0"
+                        description: Address for metrics server
+                        type: string
+                      priorityClassName:
+                        default: system-cluster-critical
+                        description: The priority value that various system components
+                          use to find the priority of the kapp-controller pod
+                        type: string
+                      tolerations:
+                        default:
+                        - key: CriticalAddonsOnly
+                          operator: Exists
+                        - effect: NoSchedule
+                          key: node-role.kubernetes.io/master
+                        - effect: NoSchedule
+                          key: node.kubernetes.io/not-ready
+                        - effect: NoSchedule
+                          key: node.cloudprovider.kubernetes.io/uninitialized
+                          value: "true"
+                        description: kapp-controller deployment tolerations
+                        items:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        type: array
+                    type: object
+                  globalNamespace:
+                    default: tanzu-package-repo-global
+                    description: The namespace value used for global packaging resources.
+                      Any Package and PackageMetadata CRs within that namespace will
+                      be included in all other namespaces on the cluster, without
+                      duplicating them
+                    type: string
+                type: object
+              namespace:
+                default: kube-system
+                description: The namespace in which kapp-controller is deployed
+                type: string
+            type: object
+          status:
+            description: KappControllerConfigStatus defines the observed state of
+              KappControllerConfig
+            properties:
+              secretRef:
+                description: Name of the data value secret created by controller
+                type: string
+            type: object
+        required:
+        - spec
+        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 efd1334d26..5ab919117e 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -9,6 +9,7 @@ resources:
 - bases/config.tanzu.vmware.com_featuregates.yaml
 - bases/cni.tanzu.vmware.com_calicoconfigs.yaml
 - bases/cni.tanzu.vmware.com_antreaconfigs.yaml
+- bases/run.tanzu.vmware.com_kappcontrollerconfigs.yaml
 # +kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
@@ -18,6 +19,7 @@ patchesStrategicMerge:
 #- patches/webhook_in_capabilities.yaml
 #- patches/webhook_in_calicoconfigs.yaml
 #- patches/webhook_in_antreaconfigs.yaml
+#- patches/webhook_in_kappcontrollerconfigs.yaml
 # +kubebuilder:scaffold:crdkustomizewebhookpatch
 
 # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
@@ -26,6 +28,7 @@ patchesStrategicMerge:
 #- patches/cainjection_in_capabilities.yaml
 #- patches/cainjection_in_calicoconfigs.yaml
 #- patches/cainjection_in_antreaconfigs.yaml
+#- patches/cainjection_in_kappcontrollerconfigs.yaml
 # +kubebuilder:scaffold:crdkustomizecainjectionpatch
 
 # the following config is for teaching kustomize how to do kustomization for CRDs.
diff --git a/config/rbac/kappcontrollerconfig_editor_role.yaml b/config/rbac/kappcontrollerconfig_editor_role.yaml
new file mode 100644
index 0000000000..16e3e22f41
--- /dev/null
+++ b/config/rbac/kappcontrollerconfig_editor_role.yaml
@@ -0,0 +1,24 @@
+# permissions for end users to edit kappcontrollerconfigs.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: kappcontrollerconfig-editor-role
+rules:
+- apiGroups:
+  - run.tanzu.vmware.com
+  resources:
+  - kappcontrollerconfigs
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - run.tanzu.vmware.com
+  resources:
+  - kappcontrollerconfigs/status
+  verbs:
+  - get
diff --git a/config/rbac/kappcontrollerconfig_viewer_role.yaml b/config/rbac/kappcontrollerconfig_viewer_role.yaml
new file mode 100644
index 0000000000..3046fb66e1
--- /dev/null
+++ b/config/rbac/kappcontrollerconfig_viewer_role.yaml
@@ -0,0 +1,20 @@
+# permissions for end users to view kappcontrollerconfigs.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: kappcontrollerconfig-viewer-role
+rules:
+- apiGroups:
+  - run.tanzu.vmware.com
+  resources:
+  - kappcontrollerconfigs
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - run.tanzu.vmware.com
+  resources:
+  - kappcontrollerconfigs/status
+  verbs:
+  - get
diff --git a/config/samples/run_v1alpha3_kappcontrollerconfig.yaml b/config/samples/run_v1alpha3_kappcontrollerconfig.yaml
new file mode 100644
index 0000000000..3832687b6a
--- /dev/null
+++ b/config/samples/run_v1alpha3_kappcontrollerconfig.yaml
@@ -0,0 +1,25 @@
+apiVersion: run.tanzu.vmware.com/v1alpha3
+kind: KappControllerConfig
+metadata:
+  name: kappcontrollerconfig-sample
+spec:
+  namespace: test-ns
+  kappController:
+    createNamespace: true
+    globalNamespace: tanzu-package-repo-global
+    deployment:
+      concurrency: 4
+      hostNetwork: true
+      priorityClassName: system-cluster-critical
+      apiPort: 10100
+      metricsBindAddress: "0"
+      tolerations:
+        - key: CriticalAddonsOnly
+          operator: Exists
+        - effect: NoSchedule
+          key: node-role.kubernetes.io/master
+        - effect: NoSchedule
+          key: node.kubernetes.io/not-ready
+        - effect: NoSchedule
+          key: node.cloudprovider.kubernetes.io/uninitialized
+          value: "true"


Test patch:
diff --git a/addons/controllers/addon_controller_test.go b/addons/controllers/addon_controller_test.go
index d413a04f9f..b1453d193b 100644
--- a/addons/controllers/addon_controller_test.go
+++ b/addons/controllers/addon_controller_test.go
@@ -6,7 +6,6 @@ package controllers
 import (
 	"os"
 	"strings"
-	"time"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -23,19 +22,6 @@ import (
 	pkgiv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/packaging/v1alpha1"
 )
 
-const (
-	waitTimeout             = time.Second * 90
-	pollingInterval         = time.Second * 2
-	appSyncPeriod           = 5 * time.Minute
-	appWaitTimeout          = 30 * time.Second
-	addonNamespace          = "tkg-system"
-	addonServiceAccount     = "tkg-addons-app-sa"
-	addonClusterRole        = "tkg-addons-app-cluster-role"
-	addonClusterRoleBinding = "tkg-addons-app-cluster-role-binding"
-	addonImagePullPolicy    = "IfNotPresent"
-	corePackageRepoName     = "core"
-)
-
 var _ = Describe("Addon Reconciler", func() {
 	var (
 		clusterName             string
diff --git a/addons/controllers/kappcontrollerconfig_controller_test.go b/addons/controllers/kappcontrollerconfig_controller_test.go
new file mode 100644
index 0000000000..6510e913ba
--- /dev/null
+++ b/addons/controllers/kappcontrollerconfig_controller_test.go
@@ -0,0 +1,136 @@
+// Copyright 2022 VMware, Inc. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package controllers
+
+import (
+	"os"
+	"strings"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+	clusterapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	kappcontroller "github.com/vmware-tanzu/tanzu-framework/addons/controllers/kapp-controller"
+	"github.com/vmware-tanzu/tanzu-framework/addons/pkg/util"
+	"github.com/vmware-tanzu/tanzu-framework/addons/testutil"
+	runv1alpha3 "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha3"
+)
+
+var _ = Describe("KappControllerConfig Reconciler", func() {
+	var (
+		kappConfigCRName        string
+		clusterResourceFilePath string
+	)
+
+	JustBeforeEach(func() {
+		By("Creating a cluster and a KappControllerConfig")
+		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 KappControllerConfig")
+		f, err := os.Open(clusterResourceFilePath)
+		Expect(err).ToNot(HaveOccurred())
+		defer f.Close()
+		Expect(testutil.DeleteResources(f, cfg, dynamicClient, true)).To(Succeed())
+	})
+
+	Context("reconcile KappControllerConfig for management cluster", func() {
+
+		BeforeEach(func() {
+			kappConfigCRName = "test-kapp-controller-1"
+			clusterResourceFilePath = "testdata/test-kapp-controller-1.yaml"
+		})
+
+		It("Should reconcile KappControllerConfig and create data value secret on management cluster", func() {
+
+			key := client.ObjectKey{
+				Namespace: "default",
+				Name:      kappConfigCRName,
+			}
+
+			cluster := &clusterapiv1beta1.Cluster{}
+			Eventually(func() bool {
+				err := k8sClient.Get(ctx, key, cluster)
+				return err == nil
+			}, waitTimeout, pollingInterval).Should(BeTrue())
+
+			Eventually(func() bool {
+				config := &runv1alpha3.KappControllerConfig{}
+				err := k8sClient.Get(ctx, key, config)
+				if err != nil {
+					return false
+				}
+				// check owner reference
+				if len(config.OwnerReferences) == 0 {
+					return false
+				}
+				Expect(len(config.OwnerReferences)).Should(Equal(1))
+				Expect(config.OwnerReferences[0].Name).Should(Equal(kappConfigCRName))
+
+				// check spec values
+				Expect(config.Spec.Namespace).Should(Equal("test-ns"))
+				Expect(config.Spec.KappController.CreateNamespace).Should(Equal(true))
+				Expect(config.Spec.KappController.GlobalNamespace).Should(Equal("tanzu-package-repo-global"))
+				Expect(config.Spec.KappController.Deployment.Concurrency).Should(Equal(4))
+				Expect(config.Spec.KappController.Deployment.HostNetwork).Should(Equal(true))
+				Expect(config.Spec.KappController.Deployment.PriorityClassName).Should(Equal("system-cluster-critical"))
+				Expect(config.Spec.KappController.Deployment.APIPort).Should(Equal(10100))
+				Expect(config.Spec.KappController.Deployment.MetricsBindAddress).Should(Equal("0"))
+				Expect(config.Spec.KappController.Deployment.Tolerations).ShouldNot(BeNil())
+
+				return true
+			}, waitTimeout, pollingInterval).Should(BeTrue())
+
+			Eventually(func() bool {
+				secretKey := client.ObjectKey{
+					Namespace: "default",
+					Name:      util.GenerateDataValueSecretName(kappConfigCRName, kappcontroller.KappControllerAddonName),
+				}
+				secret := &v1.Secret{}
+				err := k8sClient.Get(ctx, secretKey, secret)
+				if err != nil {
+					return false
+				}
+				Expect(secret.Type).Should(Equal(v1.SecretTypeOpaque))
+
+				// check data value secret contents
+				secretData := string(secret.Data["values.yaml"])
+				Expect(strings.Contains(secretData, "createNamespace: true")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "globalNamespace: tanzu-package-repo-global")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "concurrency: 4")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "hostNetwork: true")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "coreDNSIP: 100.64.0.10")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "- key: CriticalAddonsOnly")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "key: node-role.kubernetes.io/master")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "key: node.kubernetes.io/not-ready")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "key: node.cloudprovider.kubernetes.io/uninitialized")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "apiPort: 10100")).Should(BeTrue())
+				Expect(strings.Contains(secretData, "metricsBindAddress: \"0\"")).Should(BeTrue())
+
+				return true
+			}, waitTimeout, pollingInterval).Should(BeTrue())
+
+			Eventually(func() bool {
+				// Check status.secretRef after reconciliation
+				config := &runv1alpha3.KappControllerConfig{}
+				err := k8sClient.Get(ctx, key, config)
+				if err != nil {
+					return false
+				}
+				Expect(config.Status.SecretRef).Should(Equal(util.GenerateDataValueSecretName(kappConfigCRName, kappcontroller.KappControllerAddonName)))
+				return true
+			}, waitTimeout, pollingInterval).Should(BeTrue())
+
+		})
+
+	})
+
+})
diff --git a/addons/controllers/suite_test.go b/addons/controllers/suite_test.go
index b180c841e6..d9b11489ba 100644
--- a/addons/controllers/suite_test.go
+++ b/addons/controllers/suite_test.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
+	"time"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -33,6 +34,7 @@ import (
 	pkgiv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/packaging/v1alpha1"
 	antrea "github.com/vmware-tanzu/tanzu-framework/addons/controllers/antrea"
 	calico "github.com/vmware-tanzu/tanzu-framework/addons/controllers/calico"
+	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"
@@ -45,6 +47,19 @@ import (
 // These tests use Ginkgo (BDD-style Go testing framework). Refer to
 // http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
 
+const (
+	waitTimeout             = time.Second * 90
+	pollingInterval         = time.Second * 2
+	appSyncPeriod           = 5 * time.Minute
+	appWaitTimeout          = 30 * time.Second
+	addonNamespace          = "tkg-system"
+	addonServiceAccount     = "tkg-addons-app-sa"
+	addonClusterRole        = "tkg-addons-app-cluster-role"
+	addonClusterRoleBinding = "tkg-addons-app-cluster-role-binding"
+	addonImagePullPolicy    = "IfNotPresent"
+	corePackageRepoName     = "core"
+)
+
 var (
 	cfg           *rest.Config
 	k8sClient     client.Client
@@ -94,6 +109,9 @@ var _ = BeforeSuite(func(done Done) {
 	err = runtanzuv1alpha1.AddToScheme(scheme)
 	Expect(err).NotTo(HaveOccurred())
 
+	err = runtanzuv1alpha3.AddToScheme(scheme)
+	Expect(err).NotTo(HaveOccurred())
+
 	err = clientgoscheme.AddToScheme(scheme)
 	Expect(err).NotTo(HaveOccurred())
 
@@ -181,6 +199,12 @@ var _ = BeforeSuite(func(done Done) {
 		Scheme: mgr.GetScheme(),
 	}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 
+	Expect((&kappcontroller.KappControllerConfigReconciler{
+		Client: mgr.GetClient(),
+		Log:    setupLog,
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+
 	tanzuBootstrapReconciler := NewtanzuClusterBootstrapReconciler(mgr.GetClient(),
 		ctrl.Log.WithName("controllers").WithName("TanzuClusterBootstrap"),
 		mgr.GetScheme(),
diff --git a/addons/controllers/testdata/test-kapp-controller-1.yaml b/addons/controllers/testdata/test-kapp-controller-1.yaml
new file mode 100644
index 0000000000..c5661c3dff
--- /dev/null
+++ b/addons/controllers/testdata/test-kapp-controller-1.yaml
@@ -0,0 +1,41 @@
+---
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: test-kapp-controller-1
+  namespace: default
+spec:
+  clusterNetwork:
+    pods:
+      cidrBlocks:
+        - 100.96.0.0/11
+    services:
+      cidrBlocks:
+        - 100.64.0.0/13
+---
+apiVersion: run.tanzu.vmware.com/v1alpha3
+kind: KappControllerConfig
+metadata:
+  name: test-kapp-controller-1
+  namespace: default
+spec:
+  namespace: test-ns
+  kappController:
+    createNamespace: true
+    globalNamespace: tanzu-package-repo-global
+    deployment:
+      concurrency: 4
+      hostNetwork: true
+      priorityClassName: system-cluster-critical
+      apiPort: 10100
+      metricsBindAddress: "0"
+      tolerations:
+        - key: CriticalAddonsOnly
+          operator: Exists
+        - effect: NoSchedule
+          key: node-role.kubernetes.io/master
+        - effect: NoSchedule
+          key: node.kubernetes.io/not-ready
+        - effect: NoSchedule
+          key: node.cloudprovider.kubernetes.io/uninitialized
+          value: "true"