Repository: clusternet/clusternet
Pull request #58 (2021-08-10 07:25:00)

add localizer for overrides
<!--  Thanks for sending a pull request!

-->

#### What type of PR is this?
kind/feature

#### What this PR does / why we need it:

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #

#### Special notes for your reviewer:



Patch:
diff --git a/manifests/crds/apps.clusternet.io_bases.yaml b/manifests/crds/apps.clusternet.io_bases.yaml
index dcc6fd9..919d180 100644
--- a/manifests/crds/apps.clusternet.io_bases.yaml
+++ b/manifests/crds/apps.clusternet.io_bases.yaml
@@ -83,7 +83,7 @@ spec:
                       description: Name of the target resource. Either Name or FeedSelector should be set.
                       type: string
                     namespace:
-                      description: Namespace of the target resource. Default to use the same Namespace of Subscription.
+                      description: Namespace of the target resource.
                       type: string
                   required:
                   - apiVersion
diff --git a/manifests/crds/apps.clusternet.io_globalizations.yaml b/manifests/crds/apps.clusternet.io_globalizations.yaml
index a435a60..256c9fa 100644
--- a/manifests/crds/apps.clusternet.io_globalizations.yaml
+++ b/manifests/crds/apps.clusternet.io_globalizations.yaml
@@ -16,8 +16,8 @@ spec:
     listKind: GlobalizationList
     plural: globalizations
     shortNames:
-    - cloc
-    - clocal
+    - glob
+    - global
     singular: globalization
   scope: Cluster
   versions:
@@ -41,6 +41,36 @@ spec:
           spec:
             description: GlobalizationSpec defines the desired state of Globalization
             properties:
+              clusterAffinity:
+                description: ClusterAffinity is a label query over managed clusters by labels. If no labels are specified, all clusters will be selected.
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
+                    items:
+                      description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies to.
+                          type: string
+                        operator:
+                          description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
+                          type: string
+                        values:
+                          description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
+                          items:
+                            type: string
+                          type: array
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
+                    type: object
+                type: object
               feed:
                 description: Feed holds references to the objects the Globalization applies to.
                 properties:
@@ -84,34 +114,37 @@ spec:
                     description: Name of the target resource. Either Name or FeedSelector should be set.
                     type: string
                   namespace:
-                    description: Namespace of the target resource. Default to use the same Namespace of Subscription.
+                    description: Namespace of the target resource.
                     type: string
                 required:
                 - apiVersion
                 - kind
                 type: object
-              overrideType:
-                description: OverrideType specifies the override type for this Globalization.
+              overridePolicy:
+                default: ApplyLater
+                description: OverridePolicy specifies the override policy for this Globalization.
                 type: string
               overrides:
                 description: Overrides holds all the OverrideRule.
                 items:
                   description: OverrideRule holds information that describes a override rule.
                   properties:
-                    ruleName:
-                      description: RuleName indicate the OverrideRule name.
-                      type: string
-                    value:
+                    overrideValue:
                       additionalProperties:
                         type: string
                       description: OverrideValue represents override values.
                       type: object
+                    ruleName:
+                      description: RuleName indicate the OverrideRule name.
+                      type: string
                   type: object
                 type: array
               priority:
-                default: 1000
+                default: 500
                 description: Priority is an integer defining the relative importance of this Globalization compared to others. Lower numbers are considered lower priority.
                 format: int32
+                maximum: 1000
+                minimum: 0
                 type: integer
             type: object
         required:
diff --git a/manifests/crds/apps.clusternet.io_localizations.yaml b/manifests/crds/apps.clusternet.io_localizations.yaml
index 359e0d1..e6b7f99 100644
--- a/manifests/crds/apps.clusternet.io_localizations.yaml
+++ b/manifests/crds/apps.clusternet.io_localizations.yaml
@@ -84,34 +84,37 @@ spec:
                     description: Name of the target resource. Either Name or FeedSelector should be set.
                     type: string
                   namespace:
-                    description: Namespace of the target resource. Default to use the same Namespace of Subscription.
+                    description: Namespace of the target resource.
                     type: string
                 required:
                 - apiVersion
                 - kind
                 type: object
-              overrideType:
-                description: OverrideType specifies the override type for this Localization.
+              overridePolicy:
+                default: ApplyLater
+                description: OverridePolicy specifies the override policy for this Localization.
                 type: string
               overrides:
                 description: Overrides holds all the OverrideRule.
                 items:
                   description: OverrideRule holds information that describes a override rule.
                   properties:
-                    ruleName:
-                      description: RuleName indicate the OverrideRule name.
-                      type: string
-                    value:
+                    overrideValue:
                       additionalProperties:
                         type: string
                       description: OverrideValue represents override values.
                       type: object
+                    ruleName:
+                      description: RuleName indicate the OverrideRule name.
+                      type: string
                   type: object
                 type: array
               priority:
-                default: 1000
+                default: 500
                 description: Priority is an integer defining the relative importance of this Localization compared to others. Lower numbers are considered lower priority.
                 format: int32
+                maximum: 1000
+                minimum: 0
                 type: integer
             type: object
         required:
diff --git a/manifests/crds/apps.clusternet.io_subscriptions.yaml b/manifests/crds/apps.clusternet.io_subscriptions.yaml
index 49053eb..8b1ecd1 100644
--- a/manifests/crds/apps.clusternet.io_subscriptions.yaml
+++ b/manifests/crds/apps.clusternet.io_subscriptions.yaml
@@ -86,7 +86,7 @@ spec:
                       description: Name of the target resource. Either Name or FeedSelector should be set.
                       type: string
                     namespace:
-                      description: Namespace of the target resource. Default to use the same Namespace of Subscription.
+                      description: Namespace of the target resource.
                       type: string
                   required:
                   - apiVersion
diff --git a/pkg/apis/apps/v1alpha1/globalization.go b/pkg/apis/apps/v1alpha1/globalization.go
index db23b19..20a4fff 100644
--- a/pkg/apis/apps/v1alpha1/globalization.go
+++ b/pkg/apis/apps/v1alpha1/globalization.go
@@ -25,7 +25,7 @@ import (
 // +genclient
 // +genclient:nonNamespaced
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
-// +kubebuilder:resource:scope="Cluster",shortName=cloc;clocal,categories=clusternet
+// +kubebuilder:resource:scope="Cluster",shortName=glob;global,categories=clusternet
 // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
 
 // Globalization represents the cluster-scoped override rule for a group of resources.
@@ -38,12 +38,21 @@ type Globalization struct {
 
 // GlobalizationSpec defines the desired state of Globalization
 type GlobalizationSpec struct {
-	// OverrideType specifies the override type for this Globalization.
+	// OverridePolicy specifies the override policy for this Globalization.
 	//
 	// +optional
-	OverrideType OverrideType `json:"overrideType,omitempty"`
+	// +kubebuilder:validation:Type=string
+	// +kubebuilder:default=ApplyLater
+	OverridePolicy OverridePolicy `json:"overridePolicy,omitempty"`
+
+	// ClusterAffinity is a label query over managed clusters by labels.
+	// If no labels are specified, all clusters will be selected.
+	//
+	// +optional
+	ClusterAffinity *metav1.LabelSelector `json:"clusterAffinity,omitempty"`
 
 	// Overrides holds all the OverrideRule.
+	//
 	// +optional
 	Overrides []OverrideRule `json:"overrides,omitempty"`
 
@@ -51,7 +60,9 @@ type GlobalizationSpec struct {
 	// numbers are considered lower priority.
 	//
 	// +optional
-	// +kubebuilder:default=1000
+	// +kubebuilder:validation:Maximum=1000
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:default=500
 	Priority int32 `json:"priority,omitempty"`
 
 	// Feed holds references to the objects the Globalization applies to.
diff --git a/pkg/apis/apps/v1alpha1/localization.go b/pkg/apis/apps/v1alpha1/localization.go
index 1888720..359f3bb 100644
--- a/pkg/apis/apps/v1alpha1/localization.go
+++ b/pkg/apis/apps/v1alpha1/localization.go
@@ -37,10 +37,12 @@ type Localization struct {
 
 // LocalizationSpec defines the desired state of Localization
 type LocalizationSpec struct {
-	// OverrideType specifies the override type for this Localization.
+	// OverridePolicy specifies the override policy for this Localization.
 	//
 	// +optional
-	OverrideType OverrideType `json:"overrideType,omitempty"`
+	// +kubebuilder:validation:Type=string
+	// +kubebuilder:default=ApplyLater
+	OverridePolicy OverridePolicy `json:"overridePolicy,omitempty"`
 
 	// Overrides holds all the OverrideRule.
 	//
@@ -51,7 +53,9 @@ type LocalizationSpec struct {
 	// numbers are considered lower priority.
 	//
 	// +optional
-	// +kubebuilder:default=1000
+	// +kubebuilder:validation:Maximum=1000
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:default=500
 	Priority int32 `json:"priority,omitempty"`
 
 	// Feed holds references to the objects the Localization applies to.
@@ -60,11 +64,15 @@ type LocalizationSpec struct {
 	Feed `json:"feed,omitempty"`
 }
 
-type OverrideType string
+type OverridePolicy string
 
 const (
-	HelmOverride    OverrideType = "Helm"
-	DefaultOverride OverrideType = "Default"
+	// Apply overrides for all matched objects immediately, including those already populated
+	ApplyNow OverridePolicy = "ApplyNow"
+
+	// Apply overrides for all matched objects on next updates (including updates on Subscription,
+	// Manifest, HelmChart, etc) or new created objects.
+	ApplyLater OverridePolicy = "ApplyLater"
 )
 
 // OverrideRule holds information that describes a override rule.
@@ -77,7 +85,7 @@ type OverrideRule struct {
 	// OverrideValue represents override values.
 	//
 	// +optional
-	OverrideValue map[string]string `json:"value,omitempty"`
+	OverrideValue map[string]string `json:"overrideValue,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/pkg/apis/apps/v1alpha1/subscription.go b/pkg/apis/apps/v1alpha1/subscription.go
index f90047e..3537ff4 100644
--- a/pkg/apis/apps/v1alpha1/subscription.go
+++ b/pkg/apis/apps/v1alpha1/subscription.go
@@ -97,7 +97,6 @@ type Feed struct {
 	APIVersion string `json:"apiVersion"`
 
 	// Namespace of the target resource.
-	// Default to use the same Namespace of Subscription.
 	//
 	// +optional
 	Namespace string `json:"namespace,omitempty"`
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
index dcf432f..a967a64 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
@@ -316,6 +316,11 @@ func (in *GlobalizationList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GlobalizationSpec) DeepCopyInto(out *GlobalizationSpec) {
 	*out = *in
+	if in.ClusterAffinity != nil {
+		in, out := &in.ClusterAffinity, &out.ClusterAffinity
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Overrides != nil {
 		in, out := &in.Overrides, &out.Overrides
 		*out = make([]OverrideRule, len(*in))
diff --git a/pkg/controllers/apps/base/base.go b/pkg/controllers/apps/base/base.go
index 2cdcd60..7775a17 100644
--- a/pkg/controllers/apps/base/base.go
+++ b/pkg/controllers/apps/base/base.go
@@ -66,14 +66,14 @@ type Controller struct {
 
 	recorder record.EventRecorder
 
-	SyncHandler SyncHandlerFunc
+	syncHandlerFunc SyncHandlerFunc
 }
 
 func NewController(ctx context.Context, clusternetClient clusternetclientset.Interface,
 	baseInformer appinformers.BaseInformer, descInformer appinformers.DescriptionInformer,
-	recorder record.EventRecorder, syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+	recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
@@ -83,7 +83,7 @@ func NewController(ctx context.Context, clusternetClient clusternetclientset.Int
 		baseLister:       baseInformer.Lister(),
 		baseSynced:       baseInformer.Informer().HasSynced,
 		recorder:         recorder,
-		SyncHandler:      syncHandler,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of Base
@@ -156,6 +156,7 @@ func (c *Controller) addBase(obj interface{}) {
 		if err != nil {
 			klog.ErrorDepth(5, fmt.Sprintf("failed to patch Base labels: %v", err))
 			c.addBase(obj)
+			return
 		}
 	}
 
@@ -171,14 +172,6 @@ func (c *Controller) updateBase(old, cur interface{}) {
 		return
 	}
 
-	// Decide whether discovery has reported a spec change.
-	if reflect.DeepEqual(oldBase.Spec, newBase.Spec) {
-		klog.V(4).Infof("no updates on the spec of Base %q, skipping syncing", oldBase.Name)
-		return
-	}
-
-	klog.V(4).Infof("updating Base %q", klog.KObj(oldBase))
-
 	// label Base self uid
 	if val, ok := newBase.Labels[string(newBase.UID)]; !ok || val != controllerKind.Kind {
 		err := c.patchBaseLabels(newBase, map[string]*string{
@@ -187,9 +180,17 @@ func (c *Controller) updateBase(old, cur interface{}) {
 		if err != nil {
 			klog.ErrorDepth(5, fmt.Sprintf("failed to patch Base labels: %v", err))
 			c.updateBase(old, cur)
+			return
 		}
 	}
 
+	// Decide whether discovery has reported a spec change.
+	if reflect.DeepEqual(oldBase.Spec, newBase.Spec) {
+		klog.V(4).Infof("no updates on the spec of Base %q, skipping syncing", oldBase.Name)
+		return
+	}
+
+	klog.V(4).Infof("updating Base %q", klog.KObj(oldBase))
 	c.enqueue(newBase)
 }
 
@@ -354,7 +355,7 @@ func (c *Controller) syncHandler(key string) error {
 	base.Kind = controllerKind.Kind
 	base.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(base)
+	return c.syncHandlerFunc(base)
 }
 
 // enqueue takes a Base resource and converts it into a namespace/name
@@ -369,17 +370,9 @@ func (c *Controller) enqueue(base *appsapi.Base) {
 	c.workqueue.Add(key)
 }
 
-type LabelOption struct {
-	Meta Meta `json:"metadata"`
-}
-
-type Meta struct {
-	Labels map[string]*string `json:"labels"`
-}
-
 func (c *Controller) patchBaseLabels(base *appsapi.Base, labels map[string]*string) error {
 	klog.V(5).Infof("patching Base labels")
-	option := LabelOption{Meta: Meta{Labels: labels}}
+	option := utils.LabelOption{Meta: utils.Meta{Labels: labels}}
 	patchData, err := json.Marshal(option)
 	if err != nil {
 		return err
diff --git a/pkg/controllers/apps/description/description.go b/pkg/controllers/apps/description/description.go
index 5022e51..5e497ee 100644
--- a/pkg/controllers/apps/description/description.go
+++ b/pkg/controllers/apps/description/description.go
@@ -65,14 +65,14 @@ type Controller struct {
 
 	recorder record.EventRecorder
 
-	SyncHandler SyncHandlerFunc
+	syncHandlerFunc SyncHandlerFunc
 }
 
 func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
 	descInformer appInformers.DescriptionInformer, hrInformer appInformers.HelmReleaseInformer,
-	recorder record.EventRecorder, syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+	recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
@@ -83,7 +83,7 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		descSynced:       descInformer.Informer().HasSynced,
 		hrSynced:         hrInformer.Informer().HasSynced,
 		recorder:         recorder,
-		SyncHandler:      syncHandler,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of Description
@@ -330,7 +330,7 @@ func (c *Controller) syncHandler(key string) error {
 	desc.Kind = controllerKind.Kind
 	desc.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(desc)
+	return c.syncHandlerFunc(desc)
 }
 
 func (c *Controller) UpdateDescriptionStatus(desc *appsapi.Description, status *appsapi.DescriptionStatus) error {
diff --git a/pkg/controllers/apps/globalization/globalization.go b/pkg/controllers/apps/globalization/globalization.go
index e1452fa..6ef9d9b 100644
--- a/pkg/controllers/apps/globalization/globalization.go
+++ b/pkg/controllers/apps/globalization/globalization.go
@@ -18,25 +18,35 @@ package globalization
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"reflect"
 	"time"
 
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/workqueue"
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusternetClientSet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
-	appInformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
-	appListers "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	appinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/known"
+	"github.com/clusternet/clusternet/pkg/utils"
 )
 
 // controllerKind contains the schema.GroupVersionKind for this controller type.
-var controllerKind = appsapi.SchemeGroupVersion.WithKind("Globalization")
+var (
+	controllerKind = appsapi.SchemeGroupVersion.WithKind("Globalization")
+	chartKind      = appsapi.SchemeGroupVersion.WithKind("HelmChart")
+)
 
 type SyncHandlerFunc func(glob *appsapi.Globalization) error
 
@@ -44,7 +54,7 @@ type SyncHandlerFunc func(glob *appsapi.Globalization) error
 type Controller struct {
 	ctx context.Context
 
-	clusternetClient clusternetClientSet.Interface
+	clusternetClient clusternetclientset.Interface
 
 	// workqueue is a rate limited work queue. This is used to queue work to be
 	// processed instead of performing it as soon as a change happens. This
@@ -53,17 +63,26 @@ type Controller struct {
 	// simultaneously in two different workers.
 	workqueue workqueue.RateLimitingInterface
 
-	globLister appListers.GlobalizationLister
+	globLister applisters.GlobalizationLister
 	globSynced cache.InformerSynced
 
-	SyncHandler SyncHandlerFunc
+	chartLister applisters.HelmChartLister
+	chartSynced cache.InformerSynced
+
+	manifestLister applisters.ManifestLister
+	manifestSynced cache.InformerSynced
+
+	recorder record.EventRecorder
+
+	syncHandlerFunc SyncHandlerFunc
 }
 
-func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
-	globInformer appInformers.GlobalizationInformer,
-	syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+func NewController(ctx context.Context, clusternetClient clusternetclientset.Interface,
+	globInformer appinformers.GlobalizationInformer,
+	chartInformer appinformers.HelmChartInformer, manifestInformer appinformers.ManifestInformer,
+	recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
@@ -72,7 +91,12 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "globalization"),
 		globLister:       globInformer.Lister(),
 		globSynced:       globInformer.Informer().HasSynced,
-		SyncHandler:      syncHandler,
+		chartLister:      chartInformer.Lister(),
+		chartSynced:      chartInformer.Informer().HasSynced,
+		manifestLister:   manifestInformer.Lister(),
+		manifestSynced:   manifestInformer.Informer().HasSynced,
+		recorder:         recorder,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of Globalization
@@ -98,7 +122,7 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 
 	// Wait for the caches to be synced before starting workers
 	klog.V(5).Info("waiting for informer caches to sync")
-	if !cache.WaitForCacheSync(stopCh, c.globSynced) {
+	if !cache.WaitForCacheSync(stopCh, c.globSynced, c.chartSynced, c.manifestSynced) {
 		return
 	}
 
@@ -114,6 +138,39 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 func (c *Controller) addGlobalization(obj interface{}) {
 	glob := obj.(*appsapi.Globalization)
 	klog.V(4).Infof("adding Globalization %q", klog.KObj(glob))
+
+	// add finalizer
+	if !utils.ContainsString(glob.Finalizers, known.AppFinalizer) && glob.DeletionTimestamp == nil {
+		glob.Finalizers = append(glob.Finalizers, known.AppFinalizer)
+		_, err := c.clusternetClient.AppsV1alpha1().Globalizations().Update(context.TODO(),
+			glob, metav1.UpdateOptions{})
+		if err == nil {
+			msg := fmt.Sprintf("successfully inject finalizer %s to Globalization %s",
+				known.AppFinalizer, klog.KObj(glob))
+			klog.V(4).Info(msg)
+			c.recorder.Event(glob, corev1.EventTypeNormal, "FinalizerInjected", msg)
+		} else {
+			msg := fmt.Sprintf("failed to inject finalizer %s to Globalization %s: %v",
+				known.AppFinalizer, klog.KObj(glob), err)
+			klog.WarningDepth(4, msg)
+			c.recorder.Event(glob, corev1.EventTypeWarning, "FailedInjectingFinalizer", msg)
+			c.addGlobalization(obj)
+			return
+		}
+	}
+
+	// label matching Feeds uid to Globalization
+	labelsToPatch, err := c.getLabelsForPatching(glob)
+	if err == nil {
+		err = c.PatchGlobalizationLabels(glob, labelsToPatch)
+	}
+	if err != nil {
+		klog.ErrorDepth(5, fmt.Sprintf("failed to patch labels to Globalization %s: %v",
+			klog.KObj(glob), err))
+		c.addGlobalization(obj)
+		return
+	}
+
 	c.enqueue(glob)
 }
 
@@ -126,6 +183,18 @@ func (c *Controller) updateGlobalization(old, cur interface{}) {
 		return
 	}
 
+	// label matching Feeds uid to Globalization
+	labelsToPatch, err := c.getLabelsForPatching(newGlob)
+	if err == nil {
+		err = c.PatchGlobalizationLabels(newGlob, labelsToPatch)
+	}
+	if err != nil {
+		klog.ErrorDepth(5, fmt.Sprintf("failed to patch labels to Globalization %s: %v",
+			klog.KObj(newGlob), err))
+		c.updateGlobalization(old, cur)
+		return
+	}
+
 	// Decide whether discovery has reported a spec change.
 	if reflect.DeepEqual(oldGlob.Spec, newGlob.Spec) {
 		klog.V(4).Infof("no updates on the spec of Globalization %q, skipping syncing", oldGlob.Name)
@@ -144,7 +213,7 @@ func (c *Controller) deleteGlobalization(obj interface{}) {
 			utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
 			return
 		}
-		_, ok = tombstone.Obj.(*appsapi.Globalization)
+		glob, ok = tombstone.Obj.(*appsapi.Globalization)
 		if !ok {
 			utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Globalization %#v", obj))
 			return
@@ -247,7 +316,7 @@ func (c *Controller) syncHandler(key string) error {
 	glob.Kind = controllerKind.Kind
 	glob.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(glob)
+	return c.syncHandlerFunc(glob)
 }
 
 // enqueue takes a Globalization resource and converts it into a namespace/name
@@ -261,3 +330,53 @@ func (c *Controller) enqueue(glob *appsapi.Globalization) {
 	}
 	c.workqueue.Add(key)
 }
+
+func (c *Controller) getLabelsForPatching(glob *appsapi.Globalization) (map[string]*string, error) {
+	labelsToPatch := map[string]*string{}
+	if glob.Spec.Feed.Kind == chartKind.Kind {
+		charts, err := utils.ListChartsBySelector(c.chartLister, glob.Spec.Feed)
+		if err != nil {
+			return nil, err
+		}
+		for _, chart := range charts {
+			val, ok := glob.Labels[string(chart.UID)]
+			if !ok || val != chartKind.Kind {
+				labelsToPatch[string(chart.UID)] = &chartKind.Kind
+			}
+		}
+	} else {
+		manifests, err := utils.ListManifestsBySelector(c.manifestLister, glob.Spec.Feed)
+		if err != nil {
+			return nil, err
+		}
+		for _, manifest := range manifests {
+			kind := manifest.Labels[known.ConfigKindLabel]
+			val, ok := glob.Labels[string(manifest.UID)]
+			if !ok || val != kind {
+				labelsToPatch[string(manifest.UID)] = &kind
+			}
+		}
+	}
+
+	return labelsToPatch, nil
+}
+
+func (c *Controller) PatchGlobalizationLabels(glob *appsapi.Globalization, labels map[string]*string) error {
+	klog.V(5).Infof("patching Globalization labels")
+	if len(labels) == 0 {
+		return nil
+	}
+
+	option := utils.LabelOption{Meta: utils.Meta{Labels: labels}}
+	patchData, err := json.Marshal(option)
+	if err != nil {
+		return err
+	}
+
+	_, err = c.clusternetClient.AppsV1alpha1().Globalizations().Patch(context.TODO(),
+		glob.Name,
+		types.MergePatchType,
+		patchData,
+		metav1.PatchOptions{})
+	return err
+}
diff --git a/pkg/controllers/apps/helmchart/helmchart.go b/pkg/controllers/apps/helmchart/helmchart.go
index 5c1fd4f..3c2921a 100644
--- a/pkg/controllers/apps/helmchart/helmchart.go
+++ b/pkg/controllers/apps/helmchart/helmchart.go
@@ -32,9 +32,9 @@ import (
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusternetClientSet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
-	appInformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
-	appListers "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	appinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
 )
 
 // controllerKind contains the schema.GroupVersionKind for this controller type.
@@ -46,7 +46,7 @@ type SyncHandlerFunc func(chart *appsapi.HelmChart) error
 type Controller struct {
 	ctx context.Context
 
-	clusternetClient clusternetClientSet.Interface
+	clusternetClient clusternetclientset.Interface
 
 	// workqueue is a rate limited work queue. This is used to queue work to be
 	// processed instead of performing it as soon as a change happens. This
@@ -55,16 +55,16 @@ type Controller struct {
 	// simultaneously in two different workers.
 	workqueue workqueue.RateLimitingInterface
 
-	helmChartLister appListers.HelmChartLister
+	helmChartLister applisters.HelmChartLister
 	helmChartSynced cache.InformerSynced
 
-	SyncHandler SyncHandlerFunc
+	syncHandlerFunc SyncHandlerFunc
 }
 
-func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
-	helmChartInformer appInformers.HelmChartInformer, syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+func NewController(ctx context.Context, clusternetClient clusternetclientset.Interface,
+	helmChartInformer appinformers.HelmChartInformer, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
@@ -73,7 +73,7 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "helmChart"),
 		helmChartLister:  helmChartInformer.Lister(),
 		helmChartSynced:  helmChartInformer.Informer().HasSynced,
-		SyncHandler:      syncHandler,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of HelmChart
@@ -243,7 +243,7 @@ func (c *Controller) syncHandler(key string) error {
 	chart.Kind = controllerKind.Kind
 	chart.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(chart)
+	return c.syncHandlerFunc(chart)
 }
 
 func (c *Controller) UpdateChartStatus(chart *appsapi.HelmChart, status *appsapi.HelmChartStatus) error {
diff --git a/pkg/controllers/apps/helmrelease/helmrelease.go b/pkg/controllers/apps/helmrelease/helmrelease.go
index 0fbb211..1b8d410 100644
--- a/pkg/controllers/apps/helmrelease/helmrelease.go
+++ b/pkg/controllers/apps/helmrelease/helmrelease.go
@@ -33,9 +33,9 @@ import (
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusternetClientSet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
-	appInformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
-	appListers "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	appinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
 )
 
 // controllerKind contains the schema.GroupVersionKind for this controller type.
@@ -47,7 +47,7 @@ type SyncHandlerFunc func(helmrelease *appsapi.HelmRelease) error
 type Controller struct {
 	ctx context.Context
 
-	clusternetClient clusternetClientSet.Interface
+	clusternetClient clusternetclientset.Interface
 
 	// workqueue is a rate limited work queue. This is used to queue work to be
 	// processed instead of performing it as soon as a change happens. This
@@ -56,28 +56,30 @@ type Controller struct {
 	// simultaneously in two different workers.
 	workqueue workqueue.RateLimitingInterface
 
-	descLister appListers.DescriptionLister
-	hrLister   appListers.HelmReleaseLister
+	descLister applisters.DescriptionLister
+	descSynced cache.InformerSynced
+	hrLister   applisters.HelmReleaseLister
 	hrSynced   cache.InformerSynced
 
-	SyncHandler SyncHandlerFunc
+	syncHandlerFunc SyncHandlerFunc
 }
 
-func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
-	descLister appListers.DescriptionLister, hrInformer appInformers.HelmReleaseInformer,
-	syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+func NewController(ctx context.Context, clusternetClient clusternetclientset.Interface,
+	descInformer appinformers.DescriptionInformer, hrInformer appinformers.HelmReleaseInformer,
+	syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
 		ctx:              ctx,
 		clusternetClient: clusternetClient,
 		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "helmRelease"),
-		descLister:       descLister,
+		descLister:       descInformer.Lister(),
+		descSynced:       descInformer.Informer().HasSynced,
 		hrLister:         hrInformer.Lister(),
 		hrSynced:         hrInformer.Informer().HasSynced,
-		SyncHandler:      syncHandler,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of HelmRelease
@@ -103,7 +105,7 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 
 	// Wait for the caches to be synced before starting workers
 	klog.V(5).Info("waiting for informer caches to sync")
-	if !cache.WaitForCacheSync(stopCh, c.hrSynced) {
+	if !cache.WaitForCacheSync(stopCh, c.hrSynced, c.descSynced) {
 		return
 	}
 
@@ -252,7 +254,7 @@ func (c *Controller) syncHandler(key string) error {
 	hr.Kind = controllerKind.Kind
 	hr.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(hr)
+	return c.syncHandlerFunc(hr)
 }
 
 func (c *Controller) UpdateHelmReleaseStatus(hr *appsapi.HelmRelease, status *appsapi.HelmReleaseStatus) error {
diff --git a/pkg/controllers/apps/localization/localization.go b/pkg/controllers/apps/localization/localization.go
index 7829506..5f697f5 100644
--- a/pkg/controllers/apps/localization/localization.go
+++ b/pkg/controllers/apps/localization/localization.go
@@ -18,25 +18,35 @@ package localization
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"reflect"
 	"time"
 
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/workqueue"
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusternetClientSet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
-	appInformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
-	appListers "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	appinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/known"
+	"github.com/clusternet/clusternet/pkg/utils"
 )
 
 // controllerKind contains the schema.GroupVersionKind for this controller type.
-var controllerKind = appsapi.SchemeGroupVersion.WithKind("Localization")
+var (
+	controllerKind = appsapi.SchemeGroupVersion.WithKind("Localization")
+	chartKind      = appsapi.SchemeGroupVersion.WithKind("HelmChart")
+)
 
 type SyncHandlerFunc func(loc *appsapi.Localization) error
 
@@ -44,7 +54,7 @@ type SyncHandlerFunc func(loc *appsapi.Localization) error
 type Controller struct {
 	ctx context.Context
 
-	clusternetClient clusternetClientSet.Interface
+	clusternetClient clusternetclientset.Interface
 
 	// workqueue is a rate limited work queue. This is used to queue work to be
 	// processed instead of performing it as soon as a change happens. This
@@ -53,17 +63,26 @@ type Controller struct {
 	// simultaneously in two different workers.
 	workqueue workqueue.RateLimitingInterface
 
-	locLister appListers.LocalizationLister
+	locLister applisters.LocalizationLister
 	locSynced cache.InformerSynced
 
-	SyncHandler SyncHandlerFunc
+	chartLister applisters.HelmChartLister
+	chartSynced cache.InformerSynced
+
+	manifestLister applisters.ManifestLister
+	manifestSynced cache.InformerSynced
+
+	recorder record.EventRecorder
+
+	syncHandlerFunc SyncHandlerFunc
 }
 
-func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
-	locInformer appInformers.LocalizationInformer,
-	syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+func NewController(ctx context.Context, clusternetClient clusternetclientset.Interface,
+	locInformer appinformers.LocalizationInformer,
+	chartInformer appinformers.HelmChartInformer, manifestInformer appinformers.ManifestInformer,
+	recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
@@ -72,7 +91,12 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "localization"),
 		locLister:        locInformer.Lister(),
 		locSynced:        locInformer.Informer().HasSynced,
-		SyncHandler:      syncHandler,
+		chartLister:      chartInformer.Lister(),
+		chartSynced:      chartInformer.Informer().HasSynced,
+		manifestLister:   manifestInformer.Lister(),
+		manifestSynced:   manifestInformer.Informer().HasSynced,
+		recorder:         recorder,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of Localization
@@ -98,7 +122,7 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 
 	// Wait for the caches to be synced before starting workers
 	klog.V(5).Info("waiting for informer caches to sync")
-	if !cache.WaitForCacheSync(stopCh, c.locSynced) {
+	if !cache.WaitForCacheSync(stopCh, c.locSynced, c.chartSynced, c.manifestSynced) {
 		return
 	}
 
@@ -114,6 +138,39 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 func (c *Controller) addLocalization(obj interface{}) {
 	loc := obj.(*appsapi.Localization)
 	klog.V(4).Infof("adding Localization %q", klog.KObj(loc))
+
+	// add finalizer
+	if !utils.ContainsString(loc.Finalizers, known.AppFinalizer) && loc.DeletionTimestamp == nil {
+		loc.Finalizers = append(loc.Finalizers, known.AppFinalizer)
+		_, err := c.clusternetClient.AppsV1alpha1().Localizations(loc.Namespace).Update(context.TODO(),
+			loc, metav1.UpdateOptions{})
+		if err == nil {
+			msg := fmt.Sprintf("successfully inject finalizer %s to Localization %s",
+				known.AppFinalizer, klog.KObj(loc))
+			klog.V(4).Info(msg)
+			c.recorder.Event(loc, corev1.EventTypeNormal, "FinalizerInjected", msg)
+		} else {
+			msg := fmt.Sprintf("failed to inject finalizer %s to Localization %s: %v",
+				known.AppFinalizer, klog.KObj(loc), err)
+			klog.WarningDepth(4, msg)
+			c.recorder.Event(loc, corev1.EventTypeWarning, "FailedInjectingFinalizer", msg)
+			c.addLocalization(obj)
+			return
+		}
+	}
+
+	// label matching Feeds uid to Localization
+	labelsToPatch, err := c.getLabelsForPatching(loc)
+	if err == nil {
+		err = c.PatchLocalizationLabels(loc, labelsToPatch)
+	}
+	if err != nil {
+		klog.ErrorDepth(5, fmt.Sprintf("failed to patch labels to Globalization %s: %v",
+			klog.KObj(loc), err))
+		c.addLocalization(obj)
+		return
+	}
+
 	c.enqueue(loc)
 }
 
@@ -126,6 +183,18 @@ func (c *Controller) updateLocalization(old, cur interface{}) {
 		return
 	}
 
+	// label matching Feeds uid to Globalization
+	labelsToPatch, err := c.getLabelsForPatching(newLoc)
+	if err == nil {
+		err = c.PatchLocalizationLabels(newLoc, labelsToPatch)
+	}
+	if err != nil {
+		klog.ErrorDepth(5, fmt.Sprintf("failed to patch labels to Globalization %s: %v",
+			klog.KObj(newLoc), err))
+		c.updateLocalization(old, cur)
+		return
+	}
+
 	// Decide whether discovery has reported a spec change.
 	if reflect.DeepEqual(oldLoc.Spec, newLoc.Spec) {
 		klog.V(4).Infof("no updates on the spec of Localization %q, skipping syncing", oldLoc.Name)
@@ -144,7 +213,7 @@ func (c *Controller) deleteLocalization(obj interface{}) {
 			utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
 			return
 		}
-		_, ok = tombstone.Obj.(*appsapi.Localization)
+		loc, ok = tombstone.Obj.(*appsapi.Localization)
 		if !ok {
 			utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Localization %#v", obj))
 			return
@@ -247,7 +316,7 @@ func (c *Controller) syncHandler(key string) error {
 	loc.Kind = controllerKind.Kind
 	loc.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(loc)
+	return c.syncHandlerFunc(loc)
 }
 
 // enqueue takes a Localization resource and converts it into a namespace/name
@@ -261,3 +330,53 @@ func (c *Controller) enqueue(loc *appsapi.Localization) {
 	}
 	c.workqueue.Add(key)
 }
+
+func (c *Controller) getLabelsForPatching(loc *appsapi.Localization) (map[string]*string, error) {
+	labelsToPatch := map[string]*string{}
+	if loc.Spec.Feed.Kind == chartKind.Kind {
+		charts, err := utils.ListChartsBySelector(c.chartLister, loc.Spec.Feed)
+		if err != nil {
+			return nil, err
+		}
+		for _, chart := range charts {
+			val, ok := loc.Labels[string(chart.UID)]
+			if !ok || val != chartKind.Kind {
+				labelsToPatch[string(chart.UID)] = &chartKind.Kind
+			}
+		}
+	} else {
+		manifests, err := utils.ListManifestsBySelector(c.manifestLister, loc.Spec.Feed)
+		if err != nil {
+			return nil, err
+		}
+		for _, manifest := range manifests {
+			kind := manifest.Labels[known.ConfigKindLabel]
+			val, ok := loc.Labels[string(manifest.UID)]
+			if !ok || val != kind {
+				labelsToPatch[string(manifest.UID)] = &kind
+			}
+		}
+	}
+
+	return labelsToPatch, nil
+}
+
+func (c *Controller) PatchLocalizationLabels(loc *appsapi.Localization, labels map[string]*string) error {
+	klog.V(5).Infof("patching Localization labels")
+	if len(labels) == 0 {
+		return nil
+	}
+
+	option := utils.LabelOption{Meta: utils.Meta{Labels: labels}}
+	patchData, err := json.Marshal(option)
+	if err != nil {
+		return err
+	}
+
+	_, err = c.clusternetClient.AppsV1alpha1().Localizations(loc.Namespace).Patch(context.TODO(),
+		loc.Name,
+		types.MergePatchType,
+		patchData,
+		metav1.PatchOptions{})
+	return err
+}
diff --git a/pkg/controllers/apps/manifest/manifest.go b/pkg/controllers/apps/manifest/manifest.go
index 783248f..94599df 100644
--- a/pkg/controllers/apps/manifest/manifest.go
+++ b/pkg/controllers/apps/manifest/manifest.go
@@ -33,9 +33,9 @@ import (
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusternetClientSet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
-	appInformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
-	appListers "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	appinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions/apps/v1alpha1"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
 	"github.com/clusternet/clusternet/pkg/known"
 	"github.com/clusternet/clusternet/pkg/utils"
 )
@@ -49,7 +49,7 @@ type SyncHandlerFunc func(orig *appsapi.Manifest) error
 type Controller struct {
 	ctx context.Context
 
-	clusternetClient clusternetClientSet.Interface
+	clusternetClient clusternetclientset.Interface
 
 	// workqueue is a rate limited work queue. This is used to queue work to be
 	// processed instead of performing it as soon as a change happens. This
@@ -58,19 +58,19 @@ type Controller struct {
 	// simultaneously in two different workers.
 	workqueue workqueue.RateLimitingInterface
 
-	manifestLister appListers.ManifestLister
+	manifestLister applisters.ManifestLister
 	manifestSynced cache.InformerSynced
 
 	recorder record.EventRecorder
 
-	SyncHandler SyncHandlerFunc
+	syncHandlerFunc SyncHandlerFunc
 }
 
-func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
-	manifestInformer appInformers.ManifestInformer,
-	recorder record.EventRecorder, syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+func NewController(ctx context.Context, clusternetClient clusternetclientset.Interface,
+	manifestInformer appinformers.ManifestInformer,
+	recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
@@ -80,7 +80,7 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		manifestLister:   manifestInformer.Lister(),
 		manifestSynced:   manifestInformer.Informer().HasSynced,
 		recorder:         recorder,
-		SyncHandler:      syncHandler,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of Manifest
@@ -274,7 +274,7 @@ func (c *Controller) syncHandler(key string) error {
 	manifest.Kind = controllerKind.Kind
 	manifest.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(manifest)
+	return c.syncHandlerFunc(manifest)
 }
 
 // enqueue takes a Manifest resource and converts it into a namespace/name
diff --git a/pkg/controllers/apps/subscription/subscription.go b/pkg/controllers/apps/subscription/subscription.go
index 2909c4d..780f54c 100644
--- a/pkg/controllers/apps/subscription/subscription.go
+++ b/pkg/controllers/apps/subscription/subscription.go
@@ -66,14 +66,14 @@ type Controller struct {
 
 	recorder record.EventRecorder
 
-	SyncHandler SyncHandlerFunc
+	syncHandlerFunc SyncHandlerFunc
 }
 
 func NewController(ctx context.Context, clusternetClient clusternetclientset.Interface,
 	subsInformer appinformers.SubscriptionInformer, baseInformer appinformers.BaseInformer,
-	recorder record.EventRecorder, syncHandler SyncHandlerFunc) (*Controller, error) {
-	if syncHandler == nil {
-		return nil, fmt.Errorf("syncHandler must be set")
+	recorder record.EventRecorder, syncHandlerFunc SyncHandlerFunc) (*Controller, error) {
+	if syncHandlerFunc == nil {
+		return nil, fmt.Errorf("syncHandlerFunc must be set")
 	}
 
 	c := &Controller{
@@ -84,7 +84,7 @@ func NewController(ctx context.Context, clusternetClient clusternetclientset.Int
 		subsSynced:       subsInformer.Informer().HasSynced,
 		baseSynced:       baseInformer.Informer().HasSynced,
 		recorder:         recorder,
-		SyncHandler:      syncHandler,
+		syncHandlerFunc:  syncHandlerFunc,
 	}
 
 	// Manage the addition/update of Subscription
@@ -322,7 +322,7 @@ func (c *Controller) syncHandler(key string) error {
 	sub.Kind = controllerKind.Kind
 	sub.APIVersion = controllerKind.Version
 
-	return c.SyncHandler(sub)
+	return c.syncHandlerFunc(sub)
 }
 
 func (c *Controller) UpdateSubscriptionStatus(sub *appsapi.Subscription, status *appsapi.SubscriptionStatus) error {
diff --git a/pkg/hub/deployer/deployer.go b/pkg/hub/deployer/deployer.go
index 2a7330e..509fea3 100644
--- a/pkg/hub/deployer/deployer.go
+++ b/pkg/hub/deployer/deployer.go
@@ -52,6 +52,7 @@ import (
 	clusterlisters "github.com/clusternet/clusternet/pkg/generated/listers/clusters/v1beta1"
 	"github.com/clusternet/clusternet/pkg/hub/deployer/generic"
 	"github.com/clusternet/clusternet/pkg/hub/deployer/helm"
+	"github.com/clusternet/clusternet/pkg/hub/localizer"
 	"github.com/clusternet/clusternet/pkg/known"
 	"github.com/clusternet/clusternet/pkg/utils"
 )
@@ -72,8 +73,6 @@ type Deployer struct {
 
 	chartLister   applisters.HelmChartLister
 	descLister    applisters.DescriptionLister
-	subsLister    applisters.SubscriptionLister
-	hrLister      applisters.HelmReleaseLister
 	baseLister    applisters.BaseLister
 	mfstLister    applisters.ManifestLister
 	clusterLister clusterlisters.ManagedClusterLister
@@ -88,6 +87,8 @@ type Deployer struct {
 	helmDeployer    *helm.Deployer
 	genericDeployer *generic.Deployer
 
+	localizer *localizer.Localizer
+
 	broadcaster record.EventBroadcaster
 	recorder    record.EventRecorder
 }
@@ -99,8 +100,6 @@ func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clustern
 		ctx:              ctx,
 		chartLister:      clusternetInformerFactory.Apps().V1alpha1().HelmCharts().Lister(),
 		descLister:       clusternetInformerFactory.Apps().V1alpha1().Descriptions().Lister(),
-		subsLister:       clusternetInformerFactory.Apps().V1alpha1().Subscriptions().Lister(),
-		hrLister:         clusternetInformerFactory.Apps().V1alpha1().HelmReleases().Lister(),
 		clusterLister:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Lister(),
 		baseLister:       clusternetInformerFactory.Apps().V1alpha1().Bases().Lister(),
 		mfstLister:       clusternetInformerFactory.Apps().V1alpha1().Manifests().Lister(),
@@ -162,6 +161,12 @@ func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clustern
 	}
 	deployer.baseController = baseController
 
+	l, err := localizer.NewLocalizer(ctx, clusternetclient, clusternetInformerFactory, deployer.recorder)
+	if err != nil {
+		return nil, err
+	}
+	deployer.localizer = l
+
 	return deployer, nil
 }
 
@@ -173,6 +178,7 @@ func (deployer *Deployer) Run(workers int) {
 	go deployer.subsController.Run(workers, deployer.ctx.Done())
 	go deployer.mfstController.Run(workers, deployer.ctx.Done())
 	go deployer.baseController.Run(workers, deployer.ctx.Done())
+	go deployer.localizer.Run(workers)
 
 	<-deployer.ctx.Done()
 }
@@ -230,39 +236,6 @@ func (deployer *Deployer) handleSubscription(sub *appsapi.Subscription) error {
 	return nil
 }
 
-func (deployer *Deployer) getChartsBySelector(base *appsapi.Base, feed appsapi.Feed) ([]*appsapi.HelmChart, error) {
-	namespace := base.Labels[known.ConfigSubscriptionNamespaceLabel]
-	if len(feed.Namespace) > 0 {
-		namespace = feed.Namespace
-	}
-
-	if len(feed.Name) > 0 {
-		chart, err := deployer.chartLister.HelmCharts(namespace).Get(feed.Name)
-		if err != nil {
-			return nil, err
-		}
-		return []*appsapi.HelmChart{chart}, nil
-	}
-
-	if feed.FeedSelector != nil {
-		feedSelector, err := metav1.LabelSelectorAsSelector(feed.FeedSelector)
-		if err != nil {
-			return nil, err
-		}
-		return deployer.chartLister.HelmCharts(namespace).List(feedSelector)
-	}
-
-	return nil, fmt.Errorf("failed to find matched %s", formatFeed(feed))
-}
-
-func (deployer *Deployer) getManifestsBySelector(feed appsapi.Feed) ([]*appsapi.Manifest, error) {
-	selector, err := getLabelsSelectorFromFeed(feed)
-	if err != nil {
-		return nil, err
-	}
-	return deployer.mfstLister.Manifests(appsapi.ReservedNamespace).List(selector)
-}
-
 func (deployer *Deployer) populateBases(sub *appsapi.Subscription) error {
 	var mcls []*clusterapi.ManagedCluster
 	for _, subscriber := range sub.Spec.Subscribers {
@@ -474,7 +447,7 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 	var manifests []*appsapi.Manifest
 	for _, feed := range base.Spec.Feeds {
 		if feed.Kind == helmChartKind.Kind && feed.APIVersion == helmChartKind.GroupVersion().String() {
-			charts, err = deployer.getChartsBySelector(base, feed)
+			charts, err = utils.ListChartsBySelector(deployer.chartLister, feed)
 			if err == nil {
 				// verify HelmChart can be found
 				for _, chart := range charts {
@@ -501,7 +474,7 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 				err = apierrors.NewNotFound(schema.GroupResource{}, "")
 			}
 		} else {
-			manifests, err = deployer.getManifestsBySelector(feed)
+			manifests, err = utils.ListManifestsBySelector(deployer.mfstLister, feed)
 			if err == nil {
 				allManifests = append(allManifests, manifests...)
 			}
@@ -511,13 +484,13 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 		}
 
 		if apierrors.IsNotFound(err) {
-			msg := fmt.Sprintf("Base %s is using a nonexistent %s", klog.KObj(base), formatFeed(feed))
+			msg := fmt.Sprintf("Base %s is using a nonexistent %s", klog.KObj(base), utils.FormatFeed(feed))
 			klog.Error(msg)
 			deployer.recorder.Event(base, corev1.EventTypeWarning, fmt.Sprintf("Nonexistent%s", feed.Kind), msg)
 			return errors.New(msg)
 		}
 		if err != nil {
-			msg := fmt.Sprintf("failed to get matched objects %q for Base %s: %v", formatFeed(feed), klog.KObj(base), err)
+			msg := fmt.Sprintf("failed to get matched objects %q for Base %s: %v", utils.FormatFeed(feed), klog.KObj(base), err)
 			klog.Error(msg)
 			deployer.recorder.Event(base, corev1.EventTypeWarning, "FailedRetrievingObjects", msg)
 			return err
@@ -556,8 +529,6 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 		},
 	}
 
-	// TODO: apply overrides
-
 	var allErrs []error
 	if len(allChartRefs) > 0 {
 		desc := descTemplate.DeepCopy()
@@ -595,6 +566,14 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 }
 
 func (deployer *Deployer) syncDescriptions(base *appsapi.Base, description *appsapi.Description) error {
+	// apply overrides
+	if err := deployer.localizer.ApplyOverridesToDescription(description); err != nil {
+		msg := fmt.Sprintf("Failed to apply overrides for Description %s: %v", klog.KObj(description), err)
+		klog.ErrorDepth(5, msg)
+		deployer.recorder.Event(base, corev1.EventTypeWarning, "FailedApplyingOverrides", msg)
+		return err
+	}
+
 	desc, err := deployer.descLister.Descriptions(description.Namespace).Get(description.Name)
 	if err == nil {
 		if desc.DeletionTimestamp != nil {
@@ -699,7 +678,7 @@ func (deployer *Deployer) addLabelsToReferredManifests(b *appsapi.Base) error {
 	var allErrs []error
 	for _, feed := range b.Spec.Feeds {
 		if feed.Kind != helmChartKind.Kind {
-			manifests, err := deployer.getManifestsBySelector(feed)
+			manifests, err := utils.ListManifestsBySelector(deployer.mfstLister, feed)
 			if err == nil {
 				allManifests = append(allManifests, manifests...)
 			} else {
@@ -792,7 +771,7 @@ func (deployer *Deployer) removeLabelsFromReferredManifests(b *appsapi.Base) err
 }
 
 func (deployer *Deployer) patchManifestLabels(manifest *appsapi.Manifest, labels map[string]*string) error {
-	labelOption := base.LabelOption{Meta: base.Meta{Labels: labels}}
+	labelOption := utils.LabelOption{Meta: utils.Meta{Labels: labels}}
 	patchData, err := json.Marshal(labelOption)
 	if err != nil {
 		return err
diff --git a/pkg/hub/deployer/generic/generic.go b/pkg/hub/deployer/generic/generic.go
index 306c000..ce1aa6b 100644
--- a/pkg/hub/deployer/generic/generic.go
+++ b/pkg/hub/deployer/generic/generic.go
@@ -36,6 +36,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 	corev1lister "k8s.io/client-go/listers/core/v1"
 	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/retry"
@@ -46,7 +47,6 @@ import (
 	"github.com/clusternet/clusternet/pkg/controllers/apps/description"
 	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
 	clusternetinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
-	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
 	clusterlisters "github.com/clusternet/clusternet/pkg/generated/listers/clusters/v1beta1"
 	"github.com/clusternet/clusternet/pkg/known"
 	"github.com/clusternet/clusternet/pkg/utils"
@@ -60,8 +60,9 @@ type Deployer struct {
 	ctx context.Context
 
 	clusterLister clusterlisters.ManagedClusterLister
-	descLister    applisters.DescriptionLister
+	clusterSynced cache.InformerSynced
 	secretLister  corev1lister.SecretLister
+	secretSynced  cache.InformerSynced
 
 	clusternetClient *clusternetclientset.Clientset
 
@@ -77,8 +78,9 @@ func NewDeployer(ctx context.Context, clusternetClient *clusternetclientset.Clie
 	deployer := &Deployer{
 		ctx:              ctx,
 		clusterLister:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Lister(),
-		descLister:       clusternetInformerFactory.Apps().V1alpha1().Descriptions().Lister(),
+		clusterSynced:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Informer().HasSynced,
 		secretLister:     kubeInformerFactory.Core().V1().Secrets().Lister(),
+		secretSynced:     kubeInformerFactory.Core().V1().Secrets().Informer().HasSynced,
 		clusternetClient: clusternetClient,
 		recorder:         recorder,
 	}
@@ -101,6 +103,14 @@ func (deployer *Deployer) Run(workers int) {
 	klog.Info("starting generic deployer...")
 	defer klog.Info("shutting generic deployer")
 
+	// Wait for the caches to be synced before starting workers
+	klog.V(5).Info("waiting for informer caches to sync")
+	if !cache.WaitForCacheSync(deployer.ctx.Done(),
+		deployer.clusterSynced,
+		deployer.secretSynced) {
+		return
+	}
+
 	go deployer.descController.Run(workers, deployer.ctx.Done())
 
 	<-deployer.ctx.Done()
diff --git a/pkg/hub/deployer/helm/helm.go b/pkg/hub/deployer/helm/helm.go
index 0b3152b..1791eab 100644
--- a/pkg/hub/deployer/helm/helm.go
+++ b/pkg/hub/deployer/helm/helm.go
@@ -18,6 +18,8 @@ package helm
 
 import (
 	"context"
+	"encoding/json"
+	"errors"
 	"fmt"
 	"reflect"
 	"strings"
@@ -28,13 +30,14 @@ import (
 	"helm.sh/helm/v3/pkg/repo"
 	"helm.sh/helm/v3/pkg/storage/driver"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
-	kubeInformers "k8s.io/client-go/informers"
+	kubeinformers "k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes"
-	corev1Lister "k8s.io/client-go/listers/core/v1"
+	corev1lister "k8s.io/client-go/listers/core/v1"
+	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/klog/v2"
 	utilpointer "k8s.io/utils/pointer"
@@ -44,10 +47,10 @@ import (
 	"github.com/clusternet/clusternet/pkg/controllers/apps/helmchart"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/helmrelease"
 	"github.com/clusternet/clusternet/pkg/controllers/misc/secret"
-	clusternetClientSet "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
-	clusternetInformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
-	appListers "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
-	clusterListers "github.com/clusternet/clusternet/pkg/generated/listers/clusters/v1beta1"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	clusternetinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
+	clusterlisters "github.com/clusternet/clusternet/pkg/generated/listers/clusters/v1beta1"
 	"github.com/clusternet/clusternet/pkg/known"
 	"github.com/clusternet/clusternet/pkg/utils"
 )
@@ -65,22 +68,28 @@ type Deployer struct {
 
 	secretController *secret.Controller
 
-	clusternetClient *clusternetClientSet.Clientset
+	clusternetClient *clusternetclientset.Clientset
 	kubeClient       *kubernetes.Clientset
 
-	chartLister   appListers.HelmChartLister
-	hrLister      appListers.HelmReleaseLister
-	clusterLister clusterListers.ManagedClusterLister
+	chartLister   applisters.HelmChartLister
+	chartSynced   cache.InformerSynced
+	hrLister      applisters.HelmReleaseLister
+	hrSynced      cache.InformerSynced
+	descLister    applisters.DescriptionLister
+	descSynced    cache.InformerSynced
+	clusterLister clusterlisters.ManagedClusterLister
+	clusterSynced cache.InformerSynced
 
-	secretLister corev1Lister.SecretLister
+	secretLister corev1lister.SecretLister
+	secretSynced cache.InformerSynced
 
 	recorder record.EventRecorder
 }
 
 func NewDeployer(ctx context.Context,
-	clusternetClient *clusternetClientSet.Clientset, kubeClient *kubernetes.Clientset,
-	clusternetInformerFactory clusternetInformers.SharedInformerFactory,
-	kubeInformerFactory kubeInformers.SharedInformerFactory,
+	clusternetClient *clusternetclientset.Clientset, kubeClient *kubernetes.Clientset,
+	clusternetInformerFactory clusternetinformers.SharedInformerFactory,
+	kubeInformerFactory kubeinformers.SharedInformerFactory,
 	recorder record.EventRecorder) (*Deployer, error) {
 
 	deployer := &Deployer{
@@ -88,9 +97,15 @@ func NewDeployer(ctx context.Context,
 		clusternetClient: clusternetClient,
 		kubeClient:       kubeClient,
 		chartLister:      clusternetInformerFactory.Apps().V1alpha1().HelmCharts().Lister(),
+		chartSynced:      clusternetInformerFactory.Apps().V1alpha1().HelmCharts().Informer().HasSynced,
 		hrLister:         clusternetInformerFactory.Apps().V1alpha1().HelmReleases().Lister(),
+		hrSynced:         clusternetInformerFactory.Apps().V1alpha1().HelmReleases().Informer().HasSynced,
+		descLister:       clusternetInformerFactory.Apps().V1alpha1().Descriptions().Lister(),
+		descSynced:       clusternetInformerFactory.Apps().V1alpha1().Descriptions().Informer().HasSynced,
 		clusterLister:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Lister(),
+		clusterSynced:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Informer().HasSynced,
 		secretLister:     kubeInformerFactory.Core().V1().Secrets().Lister(),
+		secretSynced:     kubeInformerFactory.Core().V1().Secrets().Informer().HasSynced,
 		recorder:         recorder,
 	}
 
@@ -103,7 +118,7 @@ func NewDeployer(ctx context.Context,
 
 	hrController, err := helmrelease.NewController(ctx,
 		clusternetClient,
-		clusternetInformerFactory.Apps().V1alpha1().Descriptions().Lister(),
+		clusternetInformerFactory.Apps().V1alpha1().Descriptions(),
 		clusternetInformerFactory.Apps().V1alpha1().HelmReleases(),
 		deployer.handleHelmRelease)
 	if err != nil {
@@ -138,6 +153,17 @@ func (deployer *Deployer) Run(workers int) {
 	klog.Info("starting helm deployer...")
 	defer klog.Info("shutting helm deployer")
 
+	// Wait for the caches to be synced before starting workers
+	klog.V(5).Info("waiting for informer caches to sync")
+	if !cache.WaitForCacheSync(deployer.ctx.Done(),
+		deployer.chartSynced,
+		deployer.hrSynced,
+		deployer.descSynced,
+		deployer.clusterSynced,
+		deployer.secretSynced) {
+		return
+	}
+
 	go deployer.helmChartController.Run(workers, deployer.ctx.Done())
 	go deployer.helmReleaseController.Run(workers, deployer.ctx.Done())
 	go deployer.descriptionController.Run(workers, deployer.ctx.Done())
@@ -322,7 +348,7 @@ func (deployer *Deployer) syncHelmRelease(desc *appsapi.Description, helmRelease
 		}
 		return nil
 	}
-	if !errors.IsNotFound(err) {
+	if !apierrors.IsNotFound(err) {
 		return err
 	}
 
@@ -372,19 +398,22 @@ func (deployer *Deployer) handleHelmRelease(hr *appsapi.HelmRelease) error {
 		return err
 	}
 
-	var rel *release.Release
-	var vals map[string]interface{}
+	overrideValues, err := deployer.getOverrides(hr)
+	if err != nil {
+		return err
+	}
 
+	var rel *release.Release
 	// check whether the release is deployed
 	rel, err = cfg.Releases.Deployed(hr.Name)
 	if err != nil {
 		if strings.Contains(err.Error(), driver.ErrNoDeployedReleases.Error()) {
-			rel, err = InstallRelease(cfg, hr, chart, vals)
+			rel, err = InstallRelease(cfg, hr, chart, overrideValues)
 		}
 	} else {
 		// verify the release is changed or not
-		if ReleaseNeedsUpgrade(rel, hr, chart, vals) {
-			rel, err = UpgradeRelease(cfg, hr, chart, vals)
+		if ReleaseNeedsUpgrade(rel, hr, chart, overrideValues) {
+			rel, err = UpgradeRelease(cfg, hr, chart, overrideValues)
 		} else {
 			klog.V(5).Infof("HelmRelease %s is already updated. No need upgrading.", klog.KObj(hr))
 		}
@@ -446,3 +475,61 @@ func (deployer *Deployer) handleSecret(secret *corev1.Secret) error {
 	}
 	return err
 }
+
+func (deployer *Deployer) getOverrides(hr *appsapi.HelmRelease) (map[string]interface{}, error) {
+	var overrideValues map[string]interface{}
+	// get overrides
+	controllerRef := metav1.GetControllerOf(hr)
+	if controllerRef != nil {
+		desc := deployer.resolveControllerRef(hr.Namespace, controllerRef)
+		if desc == nil {
+			return overrideValues, nil
+		}
+
+		var found bool
+		var index int
+		for idx, chart := range desc.Spec.Charts {
+			if chart.Namespace == hr.Namespace && chart.Name == hr.Name {
+				found = true
+				index = idx
+				break
+			}
+		}
+		if !found {
+			msg := fmt.Sprintf("Description %s has no connection with HelmRelease %s", klog.KObj(desc), klog.KObj(hr))
+			klog.WarningDepth(5, msg)
+			deployer.recorder.Event(desc, corev1.EventTypeWarning, "DescriptionNotRelated", msg)
+			return overrideValues, nil
+		}
+		if len(desc.Spec.Raw) < index {
+			msg := fmt.Sprintf("unequal lengths of Spec.Raw and Spec.Charts in Description %s", klog.KObj(desc))
+			klog.ErrorDepth(5, msg)
+			deployer.recorder.Event(desc, corev1.EventTypeWarning, "UnequalLengths", msg)
+			return nil, errors.New(msg)
+		}
+		err := json.Unmarshal(desc.Spec.Raw[index], &overrideValues)
+		return overrideValues, err
+	}
+	return overrideValues, nil
+}
+
+// resolveControllerRef returns the controller referenced by a ControllerRef,
+// or nil if the ControllerRef could not be resolved to a matching controller
+// of the correct Kind.
+func (deployer *Deployer) resolveControllerRef(namespace string, controllerRef *metav1.OwnerReference) *appsapi.Description {
+	// We can't look up by UID, so look up by Name and then verify UID.
+	// Don't even try to look up by Name if it's the wrong Kind.
+	if controllerRef.Kind != descriptionKind.Kind {
+		return nil
+	}
+	desc, err := deployer.descLister.Descriptions(namespace).Get(controllerRef.Name)
+	if err != nil {
+		return nil
+	}
+	if desc.UID != controllerRef.UID {
+		// The controller we found with this Name is not the same one that the
+		// ControllerRef points to.
+		return nil
+	}
+	return desc
+}
diff --git a/pkg/hub/hub.go b/pkg/hub/hub.go
index b542975..9ce09a1 100644
--- a/pkg/hub/hub.go
+++ b/pkg/hub/hub.go
@@ -106,6 +106,8 @@ func NewHub(ctx context.Context, opts *options.HubServerOptions) (*Hub, error) {
 		clusternetInformerFactory.Apps().V1alpha1().HelmCharts().Informer()
 		clusternetInformerFactory.Apps().V1alpha1().Descriptions().Informer()
 		clusternetInformerFactory.Apps().V1alpha1().HelmReleases().Informer()
+		clusternetInformerFactory.Apps().V1alpha1().Localizations().Informer()
+		clusternetInformerFactory.Apps().V1alpha1().Globalizations().Informer()
 
 		d, err = deployer.NewDeployer(ctx, kubeclient, clusternetclient, clusternetInformerFactory, kubeInformerFactory)
 		if err != nil {
diff --git a/pkg/hub/localizer/localizer.go b/pkg/hub/localizer/localizer.go
new file mode 100644
index 0000000..1b88a05
--- /dev/null
+++ b/pkg/hub/localizer/localizer.go
@@ -0,0 +1,128 @@
+/*
+Copyright 2021 The Clusternet Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package localizer
+
+import (
+	"context"
+	"fmt"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/record"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/globalization"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/localization"
+	clusternetclientset "github.com/clusternet/clusternet/pkg/generated/clientset/versioned"
+	clusternetinformers "github.com/clusternet/clusternet/pkg/generated/informers/externalversions"
+	"github.com/clusternet/clusternet/pkg/known"
+	"github.com/clusternet/clusternet/pkg/utils"
+)
+
+// Localizer defines configuration for the application localization
+type Localizer struct {
+	ctx context.Context
+
+	clusternetClient *clusternetclientset.Clientset
+
+	locController  *localization.Controller
+	globController *globalization.Controller
+
+	recorder record.EventRecorder
+}
+
+func NewLocalizer(ctx context.Context,
+	clusternetClient *clusternetclientset.Clientset,
+	clusternetInformerFactory clusternetinformers.SharedInformerFactory,
+	recorder record.EventRecorder) (*Localizer, error) {
+
+	localizer := &Localizer{
+		ctx:              ctx,
+		clusternetClient: clusternetClient,
+		recorder:         recorder,
+	}
+
+	locController, err := localization.NewController(ctx, clusternetClient,
+		clusternetInformerFactory.Apps().V1alpha1().Localizations(),
+		clusternetInformerFactory.Apps().V1alpha1().HelmCharts(),
+		clusternetInformerFactory.Apps().V1alpha1().Manifests(),
+		recorder,
+		localizer.handleLocalization)
+	if err != nil {
+		return nil, err
+	}
+	localizer.locController = locController
+
+	globController, err := globalization.NewController(ctx,
+		clusternetClient,
+		clusternetInformerFactory.Apps().V1alpha1().Globalizations(),
+		clusternetInformerFactory.Apps().V1alpha1().HelmCharts(),
+		clusternetInformerFactory.Apps().V1alpha1().Manifests(),
+		recorder,
+		localizer.handleGlobalization)
+	if err != nil {
+		return nil, err
+	}
+	localizer.globController = globController
+
+	return localizer, nil
+}
+
+func (l *Localizer) Run(workers int) {
+	klog.Info("starting Clusternet localizer ...")
+
+	go l.locController.Run(workers, l.ctx.Done())
+	go l.globController.Run(workers, l.ctx.Done())
+
+	<-l.ctx.Done()
+}
+
+func (l *Localizer) handleLocalization(loc *appsapi.Localization) error {
+	if loc.DeletionTimestamp != nil {
+		// remove finalizer
+		loc.Finalizers = utils.RemoveString(loc.Finalizers, known.AppFinalizer)
+		_, err := l.clusternetClient.AppsV1alpha1().Localizations(loc.Namespace).Update(context.TODO(), loc, metav1.UpdateOptions{})
+		if err != nil {
+			klog.WarningDepth(4,
+				fmt.Sprintf("failed to remove finalizer %s from Localization %s: %v", known.AppFinalizer, klog.KObj(loc), err))
+		}
+		return err
+	}
+
+	return nil
+}
+
+func (l *Localizer) handleGlobalization(glob *appsapi.Globalization) error {
+	if glob.DeletionTimestamp != nil {
+		// remove finalizer
+		glob.Finalizers = utils.RemoveString(glob.Finalizers, known.AppFinalizer)
+		_, err := l.clusternetClient.AppsV1alpha1().Globalizations().Update(context.TODO(), glob, metav1.UpdateOptions{})
+		if err != nil {
+			klog.WarningDepth(4,
+				fmt.Sprintf("failed to remove finalizer %s from Globalization %s: %v", known.AppFinalizer, klog.KObj(glob), err))
+		}
+		return err
+	}
+
+	return nil
+}
+
+func (l *Localizer) ApplyOverridesToDescription(desc *appsapi.Description) error {
+	// TODO
+
+	return nil
+}
diff --git a/pkg/hub/deployer/utils.go b/pkg/utils/feed.go
similarity index 50%
rename from pkg/hub/deployer/utils.go
rename to pkg/utils/feed.go
index b5e0cbe..79b753f 100644
--- a/pkg/hub/deployer/utils.go
+++ b/pkg/utils/feed.go
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package deployer
+package utils
 
 import (
+	"errors"
 	"fmt"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,18 +25,11 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	applisters "github.com/clusternet/clusternet/pkg/generated/listers/apps/v1alpha1"
 	"github.com/clusternet/clusternet/pkg/known"
 )
 
-func formatFeed(feed appsapi.Feed) string {
-	if len(feed.Name) != 0 {
-		return fmt.Sprintf("%s %s/%s", feed.Kind, feed.Namespace, feed.Name)
-	}
-
-	return fmt.Sprintf("%s with selector %q", feed.Kind, feed.FeedSelector.String())
-}
-
-func getLabelsSelectorFromFeed(feed appsapi.Feed) (labels.Selector, error) {
+func GetLabelsSelectorFromFeed(feed appsapi.Feed) (labels.Selector, error) {
 	var gv schema.GroupVersion
 	var err error
 	if len(feed.APIVersion) > 0 {
@@ -46,13 +40,12 @@ func getLabelsSelectorFromFeed(feed appsapi.Feed) (labels.Selector, error) {
 	}
 
 	labelSet := labels.Set{
-		known.ConfigGroupLabel:   gv.Group,
-		known.ConfigVersionLabel: gv.Version,
-		known.ConfigKindLabel:    feed.Kind,
-	}
-	if len(feed.Namespace) > 0 {
-		labelSet[known.ConfigNamespaceLabel] = feed.Namespace
+		known.ConfigGroupLabel:     gv.Group,
+		known.ConfigVersionLabel:   gv.Version,
+		known.ConfigKindLabel:      feed.Kind,
+		known.ConfigNamespaceLabel: feed.Namespace,
 	}
+
 	if len(feed.Name) > 0 {
 		labelSet[known.ConfigNameLabel] = feed.Name
 	}
@@ -72,3 +65,50 @@ func getLabelsSelectorFromFeed(feed appsapi.Feed) (labels.Selector, error) {
 
 	return selector, nil
 }
+
+func ListManifestsBySelector(manifestLister applisters.ManifestLister, feed appsapi.Feed) ([]*appsapi.Manifest, error) {
+	if manifestLister == nil {
+		return nil, errors.New("manifestLister is nil when listing charts by selector")
+	}
+
+	selector, err := GetLabelsSelectorFromFeed(feed)
+	if err != nil {
+		return nil, err
+	}
+	return manifestLister.Manifests(appsapi.ReservedNamespace).List(selector)
+}
+
+func ListChartsBySelector(chartLister applisters.HelmChartLister, feed appsapi.Feed) ([]*appsapi.HelmChart, error) {
+	if chartLister == nil {
+		return nil, errors.New("chartLister is nil when listing charts by selector")
+	}
+
+	if len(feed.Namespace) == 0 {
+		return nil, fmt.Errorf("namespace is not set in Feed %s", FormatFeed(feed))
+	}
+	if len(feed.Name) > 0 {
+		chart, err := chartLister.HelmCharts(feed.Namespace).Get(feed.Name)
+		if err != nil {
+			return nil, err
+		}
+		return []*appsapi.HelmChart{chart}, nil
+	}
+
+	if feed.FeedSelector != nil {
+		feedSelector, err := metav1.LabelSelectorAsSelector(feed.FeedSelector)
+		if err != nil {
+			return nil, err
+		}
+		return chartLister.HelmCharts(feed.Namespace).List(feedSelector)
+	}
+
+	return nil, fmt.Errorf("failed to find matched %s", FormatFeed(feed))
+}
+
+func FormatFeed(feed appsapi.Feed) string {
+	if len(feed.Name) != 0 {
+		return fmt.Sprintf("%s %s/%s", feed.Kind, feed.Namespace, feed.Name)
+	}
+
+	return fmt.Sprintf("%s with selector %q", feed.Kind, feed.FeedSelector.String())
+}
diff --git a/pkg/utils/labels.go b/pkg/utils/labels.go
new file mode 100644
index 0000000..f336b75
--- /dev/null
+++ b/pkg/utils/labels.go
@@ -0,0 +1,25 @@
+/*
+Copyright 2021 The Clusternet Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+type LabelOption struct {
+	Meta Meta `json:"metadata"`
+}
+
+type Meta struct {
+	Labels map[string]*string `json:"labels"`
+}


Test patch:
diff --git a/pkg/controllers/apps/base/base_test.go b/pkg/utils/labels_test.go
similarity index 99%
rename from pkg/controllers/apps/base/base_test.go
rename to pkg/utils/labels_test.go
index 6f9cf76..a39a106 100644
--- a/pkg/controllers/apps/base/base_test.go
+++ b/pkg/utils/labels_test.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package base
+package utils
 
 import (
 	"encoding/json"