Repository: clusternet/clusternet
Pull request #49 (2021-08-06 03:54:30)

deploy descriptions with generic deployer
<!--  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_descriptions.yaml b/manifests/crds/apps.clusternet.io_descriptions.yaml
index 73eb134..3ed854d 100644
--- a/manifests/crds/apps.clusternet.io_descriptions.yaml
+++ b/manifests/crds/apps.clusternet.io_descriptions.yaml
@@ -65,12 +65,13 @@ spec:
                 description: Deployer indicates the deployer for this Description
                 enum:
                 - Helm
-                - Default
+                - Generic
                 type: string
               raw:
                 description: Raw is the underlying serialization of all objects.
                 items:
-                  type: object
+                  format: byte
+                  type: string
                 type: array
             required:
             - deployer
diff --git a/pkg/apis/apps/v1alpha1/description.go b/pkg/apis/apps/v1alpha1/description.go
index ad22760..c53978c 100644
--- a/pkg/apis/apps/v1alpha1/description.go
+++ b/pkg/apis/apps/v1alpha1/description.go
@@ -18,7 +18,6 @@ package v1alpha1
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 )
 
 // Important: Run "make generated" to regenerate code after modifying this file
@@ -47,7 +46,7 @@ type DescriptionSpec struct {
 	// +required
 	// +kubebuilder:validation:Required
 	// +kubebuilder:validation:Type=string
-	// +kubebuilder:validation:Enum=Helm;Default
+	// +kubebuilder:validation:Enum=Helm;Generic
 	Deployer DescriptionDeployer `json:"deployer"`
 
 	// Charts describe all the helm charts to be installed
@@ -58,7 +57,7 @@ type DescriptionSpec struct {
 	// Raw is the underlying serialization of all objects.
 	//
 	// +optional
-	Raw []runtime.RawExtension `json:"raw,omitempty"`
+	Raw [][]byte `json:"raw,omitempty"`
 }
 
 // DescriptionStatus defines the observed state of Description
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
index dc2b2d0..dcf432f 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
@@ -194,9 +194,13 @@ func (in *DescriptionSpec) DeepCopyInto(out *DescriptionSpec) {
 	}
 	if in.Raw != nil {
 		in, out := &in.Raw, &out.Raw
-		*out = make([]runtime.RawExtension, len(*in))
+		*out = make([][]byte, len(*in))
 		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = make([]byte, len(*in))
+				copy(*out, *in)
+			}
 		}
 	}
 	return
diff --git a/pkg/hub/deployer/deployer.go b/pkg/hub/deployer/deployer.go
index 1dc3685..79b4c11 100644
--- a/pkg/hub/deployer/deployer.go
+++ b/pkg/hub/deployer/deployer.go
@@ -19,15 +19,16 @@ package deployer
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"reflect"
 	"sync"
 
 	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"
-	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -43,13 +44,13 @@ import (
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
 	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/base"
-	"github.com/clusternet/clusternet/pkg/controllers/apps/description"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/manifest"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/subscription"
 	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/hub/deployer/generic"
 	"github.com/clusternet/clusternet/pkg/hub/deployer/helm"
 	"github.com/clusternet/clusternet/pkg/known"
 	"github.com/clusternet/clusternet/pkg/utils"
@@ -59,7 +60,6 @@ var (
 	helmChartKind    = appsapi.SchemeGroupVersion.WithKind("HelmChart")
 	subscriptionKind = appsapi.SchemeGroupVersion.WithKind("Subscription")
 	baseKind         = appsapi.SchemeGroupVersion.WithKind("Base")
-	descriptionKind  = appsapi.SchemeGroupVersion.WithKind("Description")
 )
 
 const (
@@ -78,15 +78,15 @@ type Deployer struct {
 	mfstLister    applisters.ManifestLister
 	clusterLister clusterlisters.ManagedClusterLister
 
-	kubeclient       *kubernetes.Clientset
-	clusternetclient *clusternetclientset.Clientset
+	kubeClient       *kubernetes.Clientset
+	clusternetClient *clusternetclientset.Clientset
 
 	subsController *subscription.Controller
-	descController *description.Controller
 	mfstController *manifest.Controller
 	baseController *base.Controller
 
-	helmDeployer *helm.HelmDeployer
+	helmDeployer    *helm.Deployer
+	genericDeployer *generic.Deployer
 
 	broadcaster record.EventBroadcaster
 	recorder    record.EventRecorder
@@ -104,26 +104,32 @@ func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clustern
 		clusterLister:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Lister(),
 		baseLister:       clusternetInformerFactory.Apps().V1alpha1().Bases().Lister(),
 		mfstLister:       clusternetInformerFactory.Apps().V1alpha1().Manifests().Lister(),
-		kubeclient:       kubeclient,
-		clusternetclient: clusternetclient,
+		kubeClient:       kubeclient,
+		clusternetClient: clusternetclient,
 		broadcaster:      record.NewBroadcaster(),
 	}
 
 	//deployer.broadcaster.StartStructuredLogging(5)
-	if deployer.kubeclient != nil {
+	if deployer.kubeClient != nil {
 		klog.Infof("sending events to api server")
-		deployer.broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: deployer.kubeclient.CoreV1().Events("")})
+		deployer.broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: deployer.kubeClient.CoreV1().Events("")})
 	} else {
 		klog.Warningf("no api server defined - no events will be sent to API server.")
 	}
 	deployer.recorder = deployer.broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "clusternet-hub"})
 
-	helmDeployer, err := helm.NewHelmDeployer(ctx, clusternetclient, kubeclient, clusternetInformerFactory, kubeInformerFactory, deployer.recorder)
+	helmDeployer, err := helm.NewDeployer(ctx, clusternetclient, kubeclient, clusternetInformerFactory, kubeInformerFactory, deployer.recorder)
 	if err != nil {
 		return nil, err
 	}
 	deployer.helmDeployer = helmDeployer
 
+	genericDeployer, err := generic.NewDeployer(ctx, clusternetclient, clusternetInformerFactory, kubeInformerFactory, deployer.recorder)
+	if err != nil {
+		return nil, err
+	}
+	deployer.genericDeployer = genericDeployer
+
 	subsController, err := subscription.NewController(ctx,
 		clusternetclient,
 		clusternetInformerFactory.Apps().V1alpha1().Subscriptions(),
@@ -135,17 +141,6 @@ func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clustern
 	}
 	deployer.subsController = subsController
 
-	descController, err := description.NewController(ctx,
-		clusternetclient,
-		clusternetInformerFactory.Apps().V1alpha1().Descriptions(),
-		clusternetInformerFactory.Apps().V1alpha1().HelmReleases(),
-		deployer.recorder,
-		deployer.handleDescription)
-	if err != nil {
-		return nil, err
-	}
-	deployer.descController = descController
-
 	mfstController, err := manifest.NewController(ctx,
 		clusternetclient,
 		clusternetInformerFactory.Apps().V1alpha1().Manifests(),
@@ -174,8 +169,8 @@ func (deployer *Deployer) Run(workers int) {
 	klog.Infof("starting Clusternet deployer ...")
 
 	go deployer.helmDeployer.Run(workers)
+	go deployer.genericDeployer.Run(workers)
 	go deployer.subsController.Run(workers, deployer.ctx.Done())
-	go deployer.descController.Run(workers, deployer.ctx.Done())
 	go deployer.mfstController.Run(workers, deployer.ctx.Done())
 	go deployer.baseController.Run(workers, deployer.ctx.Done())
 
@@ -200,7 +195,7 @@ func (deployer *Deployer) handleSubscription(sub *appsapi.Subscription) error {
 			if base.DeletionTimestamp != nil {
 				continue
 			}
-			err = deployer.clusternetclient.AppsV1alpha1().Bases(base.Namespace).Delete(context.TODO(), base.Name, metav1.DeleteOptions{
+			err = deployer.clusternetClient.AppsV1alpha1().Bases(base.Namespace).Delete(context.TODO(), base.Name, metav1.DeleteOptions{
 				PropagationPolicy: &deletePropagationBackground,
 			})
 			if err != nil {
@@ -212,7 +207,7 @@ func (deployer *Deployer) handleSubscription(sub *appsapi.Subscription) error {
 		}
 
 		sub.Finalizers = utils.RemoveString(sub.Finalizers, known.AppFinalizer)
-		_, err = deployer.clusternetclient.AppsV1alpha1().Subscriptions(sub.Namespace).Update(context.TODO(), sub, metav1.UpdateOptions{})
+		_, err = deployer.clusternetClient.AppsV1alpha1().Subscriptions(sub.Namespace).Update(context.TODO(), sub, metav1.UpdateOptions{})
 		if err != nil {
 			klog.WarningDepth(4,
 				fmt.Sprintf("failed to remove finalizer %s from Subscription %s: %v", known.AppFinalizer, klog.KObj(sub), err))
@@ -222,6 +217,8 @@ func (deployer *Deployer) handleSubscription(sub *appsapi.Subscription) error {
 
 	if sub.Spec.SchedulerName != defaultScheduler {
 		klog.V(4).Infof("Subscription %s is using customized scheduler %q ", klog.KObj(sub), sub.Spec.SchedulerName)
+		deployer.recorder.Event(sub, corev1.EventTypeNormal, "SkipScheduling",
+			fmt.Sprintf("customized scheduler %s is specified", sub.Spec.SchedulerName))
 		return nil
 	}
 
@@ -238,19 +235,28 @@ func (deployer *Deployer) getChartsBySelector(base *appsapi.Base, feed appsapi.F
 	if len(feed.Namespace) > 0 {
 		namespace = feed.Namespace
 	}
-	selector, err := getLabelsSelectorFromFeed(feed, namespace)
-	if err != nil {
-		return nil, err
+
+	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
 	}
-	return deployer.chartLister.HelmCharts(namespace).List(selector)
-}
 
-func (deployer *Deployer) getManifestsBySelector(base *appsapi.Base, feed appsapi.Feed) ([]*appsapi.Manifest, error) {
-	namespace := base.Labels[known.ConfigSubscriptionNamespaceLabel]
-	if len(feed.Namespace) > 0 {
-		namespace = feed.Namespace
+	if feed.FeedSelector != nil {
+		feedSelector, err := metav1.LabelSelectorAsSelector(feed.FeedSelector)
+		if err != nil {
+			return nil, err
+		}
+		return deployer.chartLister.HelmCharts(namespace).List(feedSelector)
 	}
-	selector, err := getLabelsSelectorFromFeed(feed, namespace)
+
+	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
 	}
@@ -339,7 +345,7 @@ func (deployer *Deployer) populateBases(sub *appsapi.Subscription) error {
 		if err != nil {
 			allErrs = append(allErrs, err)
 		} else {
-			err = deployer.clusternetclient.AppsV1alpha1().Bases(ns).Delete(context.TODO(), name, metav1.DeleteOptions{
+			err = deployer.clusternetClient.AppsV1alpha1().Bases(ns).Delete(context.TODO(), name, metav1.DeleteOptions{
 				PropagationPolicy: &deletePropagationBackground,
 			})
 			if err != nil {
@@ -352,8 +358,7 @@ func (deployer *Deployer) populateBases(sub *appsapi.Subscription) error {
 }
 
 func (deployer *Deployer) syncBase(sub *appsapi.Subscription, base *appsapi.Base) error {
-	curBase, err := deployer.baseLister.Bases(base.Namespace).Get(base.Name)
-	if err == nil {
+	if curBase, err := deployer.baseLister.Bases(base.Namespace).Get(base.Name); err == nil {
 		if curBase.DeletionTimestamp != nil {
 			return fmt.Errorf("Base %s is deleting, will resync later", klog.KObj(curBase))
 		}
@@ -372,7 +377,7 @@ func (deployer *Deployer) syncBase(sub *appsapi.Subscription, base *appsapi.Base
 				curBase.Finalizers = append(curBase.Finalizers, known.AppFinalizer)
 			}
 
-			_, err = deployer.clusternetclient.AppsV1alpha1().Bases(curBase.Namespace).Update(context.TODO(),
+			_, err = deployer.clusternetClient.AppsV1alpha1().Bases(curBase.Namespace).Update(context.TODO(),
 				curBase, metav1.UpdateOptions{})
 			if err == nil {
 				msg := fmt.Sprintf("Base %s is updated successfully", klog.KObj(curBase))
@@ -382,9 +387,13 @@ func (deployer *Deployer) syncBase(sub *appsapi.Subscription, base *appsapi.Base
 			return err
 		}
 		return nil
+	} else {
+		if !apierrors.IsNotFound(err) {
+			return err
+		}
 	}
 
-	_, err = deployer.clusternetclient.AppsV1alpha1().Bases(curBase.Namespace).Create(context.TODO(),
+	_, err := deployer.clusternetClient.AppsV1alpha1().Bases(base.Namespace).Create(context.TODO(),
 		base, metav1.CreateOptions{})
 	if err == nil {
 		msg := fmt.Sprintf("Base %s is created successfully", klog.KObj(base))
@@ -417,7 +426,7 @@ func (deployer *Deployer) handleBase(base *appsapi.Base) error {
 			if desc.DeletionTimestamp != nil {
 				continue
 			}
-			err = deployer.clusternetclient.AppsV1alpha1().Descriptions(base.Namespace).Delete(context.TODO(), desc.Name, metav1.DeleteOptions{
+			err = deployer.clusternetClient.AppsV1alpha1().Descriptions(base.Namespace).Delete(context.TODO(), desc.Name, metav1.DeleteOptions{
 				PropagationPolicy: &deletePropagationBackground,
 			})
 			if err != nil {
@@ -429,7 +438,7 @@ func (deployer *Deployer) handleBase(base *appsapi.Base) error {
 		}
 
 		base.Finalizers = utils.RemoveString(base.Finalizers, known.AppFinalizer)
-		_, err = deployer.clusternetclient.AppsV1alpha1().Bases(base.Namespace).Update(context.TODO(), base, metav1.UpdateOptions{})
+		_, err = deployer.clusternetClient.AppsV1alpha1().Bases(base.Namespace).Update(context.TODO(), base, metav1.UpdateOptions{})
 		if err != nil {
 			klog.WarningDepth(4,
 				fmt.Sprintf("failed to remove finalizer %s from Base %s: %v", known.AppFinalizer, klog.KObj(base), err))
@@ -477,18 +486,24 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 					}
 				}
 			}
+			if charts == nil {
+				err = apierrors.NewNotFound(schema.GroupResource{}, "")
+			}
 		} else {
-			manifests, err = deployer.getManifestsBySelector(base, feed)
+			manifests, err = deployer.getManifestsBySelector(feed)
 			if err == nil {
 				allManifests = append(allManifests, manifests...)
 			}
+			if manifests == nil {
+				err = apierrors.NewNotFound(schema.GroupResource{}, "")
+			}
 		}
 
-		if errors.IsNotFound(err) {
+		if apierrors.IsNotFound(err) {
 			msg := fmt.Sprintf("Base %s is using a nonexistent %s", klog.KObj(base), formatFeed(feed))
 			klog.Error(msg)
 			deployer.recorder.Event(base, corev1.EventTypeWarning, fmt.Sprintf("Nonexistent%s", feed.Kind), msg)
-			return nil
+			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)
@@ -498,11 +513,6 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 		}
 	}
 
-	if len(allChartRefs) == 0 && len(allManifests) == 0 {
-		deployer.recorder.Event(base, corev1.EventTypeWarning, "NoResourcesMatched", "No resources get matched")
-		return nil
-	}
-
 	descTemplate := &appsapi.Description{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: base.Namespace,
@@ -553,9 +563,9 @@ func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
 	}
 
 	if len(allManifests) > 0 {
-		var rawObjects []runtime.RawExtension
+		var rawObjects [][]byte
 		for _, manifest := range allManifests {
-			rawObjects = append(rawObjects, manifest.Template)
+			rawObjects = append(rawObjects, manifest.Template.Raw)
 		}
 		desc := descTemplate.DeepCopy()
 		desc.Name = fmt.Sprintf("%s-generic", base.Name)
@@ -594,7 +604,7 @@ func (deployer *Deployer) syncDescriptions(base *appsapi.Base, description *apps
 				desc.Finalizers = append(desc.Finalizers, known.AppFinalizer)
 			}
 
-			_, err = deployer.clusternetclient.AppsV1alpha1().Descriptions(desc.Namespace).Update(context.TODO(),
+			_, err = deployer.clusternetClient.AppsV1alpha1().Descriptions(desc.Namespace).Update(context.TODO(),
 				desc, metav1.UpdateOptions{})
 			if err == nil {
 				msg := fmt.Sprintf("Description %s is updated successfully", klog.KObj(description))
@@ -606,7 +616,7 @@ func (deployer *Deployer) syncDescriptions(base *appsapi.Base, description *apps
 		return nil
 	}
 
-	_, err = deployer.clusternetclient.AppsV1alpha1().Descriptions(description.Namespace).Create(context.TODO(),
+	_, err = deployer.clusternetClient.AppsV1alpha1().Descriptions(description.Namespace).Create(context.TODO(),
 		description, metav1.CreateOptions{})
 	if err == nil {
 		msg := fmt.Sprintf("Description %s is created successfully", klog.KObj(description))
@@ -616,62 +626,13 @@ func (deployer *Deployer) syncDescriptions(base *appsapi.Base, description *apps
 	return err
 }
 
-func (deployer *Deployer) handleDescription(desc *appsapi.Description) error {
-	if desc.DeletionTimestamp != nil {
-		// make sure all controllees have been deleted
-		hrs, err := deployer.hrLister.HelmReleases(desc.Namespace).List(labels.SelectorFromSet(labels.Set{
-			known.ConfigKindLabel:      descriptionKind.Kind,
-			known.ConfigNameLabel:      desc.Name,
-			known.ConfigNamespaceLabel: desc.Namespace,
-		}))
-		if err != nil {
-			return err
-		}
-
-		deletePropagationBackground := metav1.DeletePropagationBackground
-		for _, hr := range hrs {
-			if metav1.IsControlledBy(hr, desc) {
-				if hr.DeletionTimestamp == nil {
-					return deployer.clusternetclient.AppsV1alpha1().HelmReleases(hr.Namespace).Delete(context.TODO(), hr.Name, metav1.DeleteOptions{
-						PropagationPolicy: &deletePropagationBackground,
-					})
-				}
-				return fmt.Errorf("waiting for HelmRelease %s getting deleted", klog.KObj(hr))
-			}
-		}
-
-		if hrs != nil {
-			return fmt.Errorf("waiting for HelmRelease belongs to Description %s getting deleted", klog.KObj(desc))
-		}
-
-		desc.Finalizers = utils.RemoveString(desc.Finalizers, known.AppFinalizer)
-		_, err = deployer.clusternetclient.AppsV1alpha1().Descriptions(desc.Namespace).Update(context.TODO(), desc, metav1.UpdateOptions{})
-		if err != nil {
-			klog.WarningDepth(4,
-				fmt.Sprintf("failed to remove finalizer %s from Description %s: %v", known.AppFinalizer, klog.KObj(desc), err))
-		}
-		return err
-	}
-
-	if desc.Spec.Deployer == appsapi.DescriptionHelmDeployer {
-		err := deployer.helmDeployer.PopulateHelmRelease(desc)
-		if err != nil {
-			return err
-		}
-	}
-
-	// TODO: @dixudx: generic deployer
-
-	return nil
-}
-
 func (deployer *Deployer) handleManifest(manifest *appsapi.Manifest) error {
 	if manifest.DeletionTimestamp != nil {
 		// TODO: other strategies
 
 		// remove finalizer
 		manifest.Finalizers = utils.RemoveString(manifest.Finalizers, known.AppFinalizer)
-		_, err := deployer.clusternetclient.AppsV1alpha1().Manifests(manifest.Namespace).Update(context.TODO(), manifest, metav1.UpdateOptions{})
+		_, err := deployer.clusternetClient.AppsV1alpha1().Manifests(manifest.Namespace).Update(context.TODO(), manifest, metav1.UpdateOptions{})
 		if err != nil {
 			klog.WarningDepth(4,
 				fmt.Sprintf("failed to remove finalizer %s from Manifest %s: %v", known.AppFinalizer, klog.KObj(manifest), err))
@@ -727,18 +688,17 @@ func (deployer *Deployer) patchLabelsToReferredManifests(b *appsapi.Base) error
 	var allErrs []error
 	for _, feed := range b.Spec.Feeds {
 		if feed.Kind != helmChartKind.Kind {
-			manifests, err := deployer.getManifestsBySelector(b, feed)
+			manifests, err := deployer.getManifestsBySelector(feed)
 			if err == nil {
 				allManifests = append(allManifests, manifests...)
 			}
 
-			if errors.IsNotFound(err) {
+			if apierrors.IsNotFound(err) {
 				continue
 			}
 			if err != nil {
 				allErrs = append(allErrs, err)
 			}
-
 		}
 	}
 
@@ -750,7 +710,7 @@ func (deployer *Deployer) patchLabelsToReferredManifests(b *appsapi.Base) error
 			defer wg.Done()
 
 			curVal, ok := manifest.Labels[string(b.UID)]
-			var labelsToPatch map[string]*string
+			labelsToPatch := map[string]*string{}
 			if b.DeletionTimestamp != nil {
 				if !ok {
 					return
@@ -786,7 +746,7 @@ func (deployer *Deployer) patchManifestLabels(manifest *appsapi.Manifest, labels
 		return err
 	}
 
-	_, err = deployer.clusternetclient.AppsV1alpha1().Manifests(manifest.Namespace).Patch(context.TODO(),
+	_, err = deployer.clusternetClient.AppsV1alpha1().Manifests(manifest.Namespace).Patch(context.TODO(),
 		manifest.Name,
 		types.MergePatchType,
 		patchData,
diff --git a/pkg/hub/deployer/generic/generic.go b/pkg/hub/deployer/generic/generic.go
new file mode 100644
index 0000000..4fe5eb4
--- /dev/null
+++ b/pkg/hub/deployer/generic/generic.go
@@ -0,0 +1,306 @@
+/*
+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 generic
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/labels"
+	utilerrors "k8s.io/apimachinery/pkg/util/errors"
+	"k8s.io/apimachinery/pkg/util/wait"
+	cacheddiscovery "k8s.io/client-go/discovery/cached/memory"
+	"k8s.io/client-go/dynamic"
+	kubeinformers "k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes"
+	corev1lister "k8s.io/client-go/listers/core/v1"
+	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/tools/record"
+	"k8s.io/client-go/util/retry"
+	"k8s.io/klog/v2"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
+	"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"
+)
+
+const (
+	defaultRetries = 3
+)
+
+type Deployer struct {
+	ctx context.Context
+
+	clusterLister clusterlisters.ManagedClusterLister
+	descLister    applisters.DescriptionLister
+	secretLister  corev1lister.SecretLister
+
+	clusternetClient *clusternetclientset.Clientset
+
+	descController *description.Controller
+
+	recorder record.EventRecorder
+}
+
+func NewDeployer(ctx context.Context, clusternetClient *clusternetclientset.Clientset,
+	clusternetInformerFactory clusternetinformers.SharedInformerFactory, kubeInformerFactory kubeinformers.SharedInformerFactory,
+	recorder record.EventRecorder) (*Deployer, error) {
+
+	deployer := &Deployer{
+		ctx:              ctx,
+		clusterLister:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Lister(),
+		descLister:       clusternetInformerFactory.Apps().V1alpha1().Descriptions().Lister(),
+		secretLister:     kubeInformerFactory.Core().V1().Secrets().Lister(),
+		clusternetClient: clusternetClient,
+		recorder:         recorder,
+	}
+
+	descController, err := description.NewController(ctx,
+		clusternetClient,
+		clusternetInformerFactory.Apps().V1alpha1().Descriptions(),
+		clusternetInformerFactory.Apps().V1alpha1().HelmReleases(),
+		deployer.recorder,
+		deployer.handleDescription)
+	if err != nil {
+		return nil, err
+	}
+	deployer.descController = descController
+
+	return deployer, nil
+}
+
+func (deployer *Deployer) Run(workers int) {
+	klog.Info("starting generic deployer...")
+	defer klog.Info("shutting generic deployer")
+
+	go deployer.descController.Run(workers, deployer.ctx.Done())
+
+	<-deployer.ctx.Done()
+}
+
+func (deployer *Deployer) handleDescription(desc *appsapi.Description) error {
+	if desc.Spec.Deployer != appsapi.DescriptionGenericDeployer {
+		return nil
+	}
+
+	// check whether ManagedCluster will enable deploying Description with Pusher/Dual mode
+	mcls, err := deployer.clusterLister.ManagedClusters(desc.Namespace).List(
+		labels.SelectorFromSet(labels.Set{
+			known.ClusterIDLabel: desc.Labels[known.ClusterIDLabel],
+		}))
+	if err != nil {
+		return err
+	}
+	if mcls == nil {
+		deployer.recorder.Event(desc, corev1.EventTypeWarning, "ManagedClusterNotFound",
+			fmt.Sprintf("can not find a ManagedCluster with uid=%s in current namespace", desc.Labels[known.ClusterIDLabel]))
+		return fmt.Errorf("failed to find a ManagedCluster declaration in namespace %s", desc.Namespace)
+	}
+	if !mcls[0].Status.AppPusher {
+		deployer.recorder.Event(desc, corev1.EventTypeNormal, "", "target cluster has disabled AppPusher")
+		klog.V(5).Infof("ManagedCluster with uid=%s has disabled AppPusher", mcls[0].UID)
+		return nil
+	}
+
+	if desc.DeletionTimestamp != nil {
+		return deployer.deleteDescription(desc)
+	}
+
+	return deployer.createOrUpdateDescription(desc)
+}
+
+func (deployer *Deployer) createOrUpdateDescription(desc *appsapi.Description) error {
+	dynamicClient, discoveryRESTMapper, err := deployer.getDynamicClient(desc)
+	if err != nil {
+		return err
+	}
+
+	var allErrs []error
+	wg := sync.WaitGroup{}
+	objectsToBeDeployed := desc.Spec.Raw
+	errCh := make(chan error, len(objectsToBeDeployed))
+	for _, object := range objectsToBeDeployed {
+		resource := &unstructured.Unstructured{}
+		err := resource.UnmarshalJSON(object)
+		if err != nil {
+			allErrs = append(allErrs, err)
+			msg := fmt.Sprintf("failed to unmarshal resource: %v", err)
+			klog.ErrorDepth(5, msg)
+			deployer.recorder.Event(desc, corev1.EventTypeWarning, "FailedMarshalingResource", msg)
+		} else {
+			wg.Add(1)
+			go func(resource *unstructured.Unstructured) {
+				defer wg.Done()
+
+				err := deployer.applyResourceWithRetry(dynamicClient, discoveryRESTMapper, resource, defaultRetries)
+				if err != nil {
+					errCh <- err
+				}
+			}(resource)
+		}
+	}
+	wg.Wait()
+
+	// collect errors
+	close(errCh)
+	for err := range errCh {
+		allErrs = append(allErrs, err)
+	}
+
+	err = utilerrors.NewAggregate(allErrs)
+	if err != nil {
+		msg := fmt.Sprintf("failed to deploying Description %s: %v", klog.KObj(desc), err)
+		klog.ErrorDepth(5, msg)
+		deployer.recorder.Event(desc, corev1.EventTypeWarning, "FailedDeployingDescription", msg)
+	} else {
+		klog.V(5).Infof("Description %s is deployed successfully", klog.KObj(desc))
+		deployer.recorder.Event(desc, corev1.EventTypeNormal, "SuccessfullyDeployedDescription", "")
+	}
+	return err
+}
+
+func (deployer *Deployer) deleteDescription(desc *appsapi.Description) error {
+	dynamicClient, discoveryRESTMapper, err := deployer.getDynamicClient(desc)
+	if err != nil {
+		return err
+	}
+
+	var allErrs []error
+	wg := sync.WaitGroup{}
+	objectsToBeDeleted := desc.Spec.Raw
+	errCh := make(chan error, len(objectsToBeDeleted))
+	for _, object := range objectsToBeDeleted {
+		resource := &unstructured.Unstructured{}
+		err := resource.UnmarshalJSON(object)
+		if err != nil {
+			allErrs = append(allErrs, err)
+			msg := fmt.Sprintf("failed to unmarshal resource: %v", err)
+			klog.ErrorDepth(5, msg)
+			deployer.recorder.Event(desc, corev1.EventTypeWarning, "FailedMarshalingResource", msg)
+		} else {
+			wg.Add(1)
+			go func(resource *unstructured.Unstructured) {
+				defer wg.Done()
+
+				err := deployer.deleteResourceWithRetry(dynamicClient, discoveryRESTMapper, resource, defaultRetries)
+				if err != nil {
+					errCh <- err
+				}
+			}(resource)
+		}
+	}
+	wg.Wait()
+
+	// collect errors
+	close(errCh)
+	for err := range errCh {
+		allErrs = append(allErrs, err)
+	}
+
+	err = utilerrors.NewAggregate(allErrs)
+	if err != nil {
+		msg := fmt.Sprintf("failed to deleting Description %s: %v", klog.KObj(desc), err)
+		klog.ErrorDepth(5, msg)
+		deployer.recorder.Event(desc, corev1.EventTypeWarning, "FailedDeletingDescription", msg)
+	} else {
+		klog.V(5).Infof("Description %s is deleted successfully", klog.KObj(desc))
+	}
+	return err
+}
+
+func (deployer *Deployer) applyResourceWithRetry(dynamicClient dynamic.Interface, restMapper meta.RESTMapper, resource *unstructured.Unstructured, retries int) error {
+	backoff := retry.DefaultBackoff
+	backoff.Steps = retries
+	return wait.ExponentialBackoffWithContext(deployer.ctx, backoff, func() (done bool, err error) {
+		restMapping, err := restMapper.RESTMapping(resource.GroupVersionKind().GroupKind(), resource.GroupVersionKind().Version)
+		if err != nil {
+			return false, nil
+		}
+
+		_, err = dynamicClient.Resource(restMapping.Resource).Namespace(resource.GetNamespace()).Create(context.TODO(), resource, metav1.CreateOptions{})
+		if err != nil {
+			if apierrors.IsAlreadyExists(err) {
+				_, err = dynamicClient.Resource(restMapping.Resource).Namespace(resource.GetNamespace()).Update(context.TODO(), resource, metav1.UpdateOptions{})
+				if err != nil {
+					return false, nil
+				}
+			}
+			return false, nil
+		}
+		return true, nil
+	})
+}
+
+func (deployer *Deployer) deleteResourceWithRetry(dynamicClient dynamic.Interface, restMapper meta.RESTMapper, resource *unstructured.Unstructured, retries int) error {
+	backoff := retry.DefaultBackoff
+	backoff.Steps = retries
+	deletePropagationBackground := metav1.DeletePropagationBackground
+	return wait.ExponentialBackoffWithContext(deployer.ctx, backoff, func() (done bool, err error) {
+		restMapping, err := restMapper.RESTMapping(resource.GroupVersionKind().GroupKind(), resource.GroupVersionKind().Version)
+		if err != nil {
+			return false, nil
+		}
+
+		if err := dynamicClient.Resource(restMapping.Resource).Namespace(resource.GetNamespace()).
+			Delete(context.TODO(), resource.GetName(), metav1.DeleteOptions{PropagationPolicy: &deletePropagationBackground}); err != nil {
+			return false, nil
+		}
+		return true, nil
+	})
+}
+
+func (deployer *Deployer) getDynamicClient(desc *appsapi.Description) (dynamic.Interface, meta.RESTMapper, error) {
+	config, err := utils.GetChildClusterConfig(deployer.secretLister, deployer.clusterLister, desc.Namespace, desc.Labels[known.ClusterIDLabel])
+	if err != nil {
+		return nil, nil, err
+	}
+
+	clientConfig := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{})
+	restConfig, err := clientConfig.ClientConfig()
+	if err != nil {
+		return nil, nil, err
+	}
+	restConfig.QPS = 5
+	restConfig.Burst = 10
+
+	kubeclient, err := kubernetes.NewForConfig(restConfig)
+	if err != nil {
+		return nil, nil, err
+	}
+	discoveryClient := cacheddiscovery.NewMemCacheClient(kubeclient.Discovery())
+	discoveryRESTMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
+
+	dynamicClient, err := dynamic.NewForConfig(restConfig)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return dynamicClient, discoveryRESTMapper, nil
+
+}
diff --git a/pkg/hub/deployer/helm/helm.go b/pkg/hub/deployer/helm/helm.go
index ed90a09..0eb5402 100644
--- a/pkg/hub/deployer/helm/helm.go
+++ b/pkg/hub/deployer/helm/helm.go
@@ -18,7 +18,6 @@ package helm
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"reflect"
 	"strings"
@@ -29,20 +28,19 @@ 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"
 	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"
 	"k8s.io/client-go/kubernetes"
 	corev1Lister "k8s.io/client-go/listers/core/v1"
-	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/klog/v2"
 	utilpointer "k8s.io/utils/pointer"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
-	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
-	proxiesapi "github.com/clusternet/clusternet/pkg/apis/proxies/v1alpha1"
+	"github.com/clusternet/clusternet/pkg/controllers/apps/description"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/helmchart"
 	"github.com/clusternet/clusternet/pkg/controllers/apps/helmrelease"
 	"github.com/clusternet/clusternet/pkg/controllers/misc/secret"
@@ -58,16 +56,17 @@ var (
 	descriptionKind = appsapi.SchemeGroupVersion.WithKind("Description")
 )
 
-type HelmDeployer struct {
+type Deployer struct {
 	ctx context.Context
 
 	helmChartController   *helmchart.Controller
 	helmReleaseController *helmrelease.Controller
+	descriptionController *description.Controller
 
 	secretController *secret.Controller
 
-	clusternetclient *clusternetClientSet.Clientset
-	kubeclient       *kubernetes.Clientset
+	clusternetClient *clusternetClientSet.Clientset
+	kubeClient       *kubernetes.Clientset
 
 	chartLister   appListers.HelmChartLister
 	hrLister      appListers.HelmReleaseLister
@@ -78,16 +77,16 @@ type HelmDeployer struct {
 	recorder record.EventRecorder
 }
 
-func NewHelmDeployer(ctx context.Context,
-	clusternetclient *clusternetClientSet.Clientset, kubeclient *kubernetes.Clientset,
+func NewDeployer(ctx context.Context,
+	clusternetClient *clusternetClientSet.Clientset, kubeClient *kubernetes.Clientset,
 	clusternetInformerFactory clusternetInformers.SharedInformerFactory,
 	kubeInformerFactory kubeInformers.SharedInformerFactory,
-	recorder record.EventRecorder) (*HelmDeployer, error) {
+	recorder record.EventRecorder) (*Deployer, error) {
 
-	hd := &HelmDeployer{
+	deployer := &Deployer{
 		ctx:              ctx,
-		clusternetclient: clusternetclient,
-		kubeclient:       kubeclient,
+		clusternetClient: clusternetClient,
+		kubeClient:       kubeClient,
 		chartLister:      clusternetInformerFactory.Apps().V1alpha1().HelmCharts().Lister(),
 		hrLister:         clusternetInformerFactory.Apps().V1alpha1().HelmReleases().Lister(),
 		clusterLister:    clusternetInformerFactory.Clusters().V1beta1().ManagedClusters().Lister(),
@@ -95,66 +94,146 @@ func NewHelmDeployer(ctx context.Context,
 		recorder:         recorder,
 	}
 
-	helmChartController, err := helmchart.NewController(ctx, clusternetclient,
-		clusternetInformerFactory.Apps().V1alpha1().HelmCharts(), hd.handleHelmChart)
+	helmChartController, err := helmchart.NewController(ctx, clusternetClient,
+		clusternetInformerFactory.Apps().V1alpha1().HelmCharts(), deployer.handleHelmChart)
 	if err != nil {
 		return nil, err
 	}
-	hd.helmChartController = helmChartController
+	deployer.helmChartController = helmChartController
 
 	hrController, err := helmrelease.NewController(ctx,
-		clusternetclient,
+		clusternetClient,
 		clusternetInformerFactory.Apps().V1alpha1().HelmReleases(),
-		hd.handleHelmRelease)
+		deployer.handleHelmRelease)
 	if err != nil {
 		return nil, err
 	}
-	hd.helmReleaseController = hrController
+	deployer.helmReleaseController = hrController
 
 	secretController, err := secret.NewController(ctx,
-		kubeclient,
+		kubeClient,
 		kubeInformerFactory.Core().V1().Secrets(),
-		hd.handleSecret)
+		deployer.handleSecret)
 	if err != nil {
 		return nil, err
 	}
-	hd.secretController = secretController
+	deployer.secretController = secretController
 
-	return hd, nil
+	descController, err := description.NewController(ctx,
+		clusternetClient,
+		clusternetInformerFactory.Apps().V1alpha1().Descriptions(),
+		clusternetInformerFactory.Apps().V1alpha1().HelmReleases(),
+		deployer.recorder,
+		deployer.handleDescription)
+	if err != nil {
+		return nil, err
+	}
+	deployer.descriptionController = descController
+
+	return deployer, nil
 }
 
-func (hd *HelmDeployer) Run(workers int) {
+func (deployer *Deployer) Run(workers int) {
 	klog.Info("starting helm deployer...")
 	defer klog.Info("shutting helm deployer")
 
-	go hd.helmChartController.Run(workers, hd.ctx.Done())
-	go hd.helmReleaseController.Run(workers, hd.ctx.Done())
+	go deployer.helmChartController.Run(workers, deployer.ctx.Done())
+	go deployer.helmReleaseController.Run(workers, deployer.ctx.Done())
+	go deployer.descriptionController.Run(workers, deployer.ctx.Done())
 	// 1 worker may get hang up, so we set minimum 2 workers here
-	go hd.secretController.Run(2, hd.ctx.Done())
+	go deployer.secretController.Run(2, deployer.ctx.Done())
 
-	<-hd.ctx.Done()
+	<-deployer.ctx.Done()
 }
 
-func (hd *HelmDeployer) handleHelmChart(chart *appsapi.HelmChart) error {
+func (deployer *Deployer) handleDescription(desc *appsapi.Description) error {
+	if desc.Spec.Deployer != appsapi.DescriptionHelmDeployer {
+		return nil
+	}
+
+	// TODO: may ignore checking AppPusher for helm charts?
+	// check whether ManagedCluster will enable deploying Description with Pusher/Dual mode
+	mcls, err := deployer.clusterLister.ManagedClusters(desc.Namespace).List(
+		labels.SelectorFromSet(labels.Set{
+			known.ClusterIDLabel: desc.Labels[known.ClusterIDLabel],
+		}))
+	if err != nil {
+		return err
+	}
+	if mcls == nil {
+		deployer.recorder.Event(desc, corev1.EventTypeWarning, "ManagedClusterNotFound",
+			fmt.Sprintf("can not find a ManagedCluster with uid=%s in current namespace", desc.Labels[known.ClusterIDLabel]))
+		return fmt.Errorf("failed to find a ManagedCluster declaration in namespace %s", desc.Namespace)
+	}
+	if !mcls[0].Status.AppPusher {
+		deployer.recorder.Event(desc, corev1.EventTypeNormal, "", "target cluster has disabled AppPusher")
+		klog.V(5).Infof("ManagedCluster with uid=%s has disabled AppPusher", mcls[0].UID)
+		return nil
+	}
+
+	if desc.DeletionTimestamp != nil {
+		// make sure all controllees have been deleted
+		hrs, err := deployer.hrLister.HelmReleases(desc.Namespace).List(labels.SelectorFromSet(labels.Set{
+			known.ConfigKindLabel:      descriptionKind.Kind,
+			known.ConfigNameLabel:      desc.Name,
+			known.ConfigNamespaceLabel: desc.Namespace,
+		}))
+		if err != nil {
+			return err
+		}
+
+		deletePropagationBackground := metav1.DeletePropagationBackground
+		for _, hr := range hrs {
+			if metav1.IsControlledBy(hr, desc) {
+				if hr.DeletionTimestamp == nil {
+					return deployer.clusternetClient.AppsV1alpha1().HelmReleases(hr.Namespace).Delete(context.TODO(), hr.Name, metav1.DeleteOptions{
+						PropagationPolicy: &deletePropagationBackground,
+					})
+				}
+				return fmt.Errorf("waiting for HelmRelease %s getting deleted", klog.KObj(hr))
+			}
+		}
+
+		if hrs != nil {
+			return fmt.Errorf("waiting for HelmRelease belongs to Description %s getting deleted", klog.KObj(desc))
+		}
+
+		desc.Finalizers = utils.RemoveString(desc.Finalizers, known.AppFinalizer)
+		_, err = deployer.clusternetClient.AppsV1alpha1().Descriptions(desc.Namespace).Update(context.TODO(), desc, metav1.UpdateOptions{})
+		if err != nil {
+			klog.WarningDepth(4,
+				fmt.Sprintf("failed to remove finalizer %s from Description %s: %v", known.AppFinalizer, klog.KObj(desc), err))
+		}
+		return err
+	}
+
+	if err := deployer.PopulateHelmRelease(desc); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (deployer *Deployer) handleHelmChart(chart *appsapi.HelmChart) error {
 	_, err := repo.FindChartInRepoURL(chart.Spec.Repository, chart.Spec.Chart, chart.Spec.ChartVersion,
 		"", "", "",
 		getter.All(settings))
 	if err != nil {
 		// failed to find chart
-		return hd.helmChartController.UpdateChartStatus(chart, &appsapi.HelmChartStatus{
+		return deployer.helmChartController.UpdateChartStatus(chart, &appsapi.HelmChartStatus{
 			Phase:  appsapi.HelmChartNotFound,
 			Reason: err.Error(),
 		})
 	}
-	return hd.helmChartController.UpdateChartStatus(chart, &appsapi.HelmChartStatus{
+	return deployer.helmChartController.UpdateChartStatus(chart, &appsapi.HelmChartStatus{
 		Phase: appsapi.HelmChartFound,
 	})
 }
 
-func (hd *HelmDeployer) PopulateHelmRelease(desc *appsapi.Description) error {
+func (deployer *Deployer) PopulateHelmRelease(desc *appsapi.Description) error {
 	var allErrs []error
 	for _, chartRef := range desc.Spec.Charts {
-		chart, err := hd.chartLister.HelmCharts(chartRef.Namespace).Get(chartRef.Name)
+		chart, err := deployer.chartLister.HelmCharts(chartRef.Namespace).Get(chartRef.Name)
 		if err != nil {
 			return err
 		}
@@ -196,20 +275,20 @@ func (hd *HelmDeployer) PopulateHelmRelease(desc *appsapi.Description) error {
 			},
 		}
 
-		err = hd.syncHelmRelease(desc, hr)
+		err = deployer.syncHelmRelease(desc, hr)
 		if err != nil {
 			allErrs = append(allErrs, err)
 			msg := fmt.Sprintf("Failed to sync HelmRelease %s: %v", klog.KObj(hr), err)
 			klog.ErrorDepth(5, msg)
-			hd.recorder.Event(desc, corev1.EventTypeWarning, "HelmReleaseFailure", msg)
+			deployer.recorder.Event(desc, corev1.EventTypeWarning, "HelmReleaseFailure", msg)
 		}
 	}
 
 	return utilerrors.NewAggregate(allErrs)
 }
 
-func (hd *HelmDeployer) syncHelmRelease(desc *appsapi.Description, helmRelease *appsapi.HelmRelease) error {
-	hr, err := hd.hrLister.HelmReleases(helmRelease.Namespace).Get(helmRelease.Name)
+func (deployer *Deployer) syncHelmRelease(desc *appsapi.Description, helmRelease *appsapi.HelmRelease) error {
+	hr, err := deployer.hrLister.HelmReleases(helmRelease.Namespace).Get(helmRelease.Name)
 	if err == nil {
 		if hr.DeletionTimestamp != nil {
 			return fmt.Errorf("HelmRelease %s is deleting, will resync later", klog.KObj(hr))
@@ -229,67 +308,36 @@ func (hd *HelmDeployer) syncHelmRelease(desc *appsapi.Description, helmRelease *
 				hr.Finalizers = append(hr.Finalizers, known.AppFinalizer)
 			}
 
-			_, err = hd.clusternetclient.AppsV1alpha1().HelmReleases(hr.Namespace).Update(context.TODO(),
+			_, err = deployer.clusternetClient.AppsV1alpha1().HelmReleases(hr.Namespace).Update(context.TODO(),
 				hr, metav1.UpdateOptions{})
 			if err == nil {
 				msg := fmt.Sprintf("HelmReleases %s is updated successfully", klog.KObj(hr))
 				klog.V(4).Info(msg)
-				hd.recorder.Event(desc, corev1.EventTypeNormal, "HelmReleaseUpdated", msg)
+				deployer.recorder.Event(desc, corev1.EventTypeNormal, "HelmReleaseUpdated", msg)
 			}
 			return err
 		}
 		return nil
 	}
+	if !errors.IsNotFound(err) {
+		return err
+	}
 
-	_, err = hd.clusternetclient.AppsV1alpha1().HelmReleases(helmRelease.Namespace).Create(context.TODO(),
+	_, err = deployer.clusternetClient.AppsV1alpha1().HelmReleases(helmRelease.Namespace).Create(context.TODO(),
 		helmRelease, metav1.CreateOptions{})
 	if err == nil {
 		msg := fmt.Sprintf("HelmReleases %s is created successfully", klog.KObj(helmRelease))
 		klog.V(4).Info(msg)
-		hd.recorder.Event(desc, corev1.EventTypeNormal, "HelmReleasesCreated", msg)
+		deployer.recorder.Event(desc, corev1.EventTypeNormal, "HelmReleasesCreated", msg)
 	}
 	return err
 }
 
-func (hd *HelmDeployer) handleHelmRelease(hr *appsapi.HelmRelease) error {
-	// TODO add recorder
-
-	childClusterSecret, err := hd.secretLister.Secrets(hr.Namespace).Get(known.ChildClusterSecretName)
-	if err != nil {
-		return err
-	}
-
-	mcls, err := hd.clusterLister.ManagedClusters(hr.Namespace).List(
-		labels.SelectorFromSet(labels.Set{
-			known.ClusterIDLabel: hr.Labels[known.ClusterIDLabel],
-		}))
+func (deployer *Deployer) handleHelmRelease(hr *appsapi.HelmRelease) error {
+	config, err := utils.GetChildClusterConfig(deployer.secretLister, deployer.clusterLister, hr.Namespace, hr.Labels[known.ClusterIDLabel])
 	if err != nil {
 		return err
 	}
-	if mcls == nil {
-		return fmt.Errorf("failed to find a ManagedCluster declaration in namespace %s", hr.Namespace)
-	}
-
-	var config *clientcmdapi.Config
-	if len(mcls) > 1 {
-		klog.Warningf("found multiple ManagedCluster declarations in namespace %s", hr.Namespace)
-	}
-	if mcls[0].Status.UseSocket {
-		childClusterAPIServer, err := getChildAPIServerProxyURL(mcls[0])
-		if err != nil {
-			return err
-		}
-		config = utils.CreateKubeConfigForSocketProxyWithToken(
-			childClusterAPIServer,
-			string(childClusterSecret.Data[corev1.ServiceAccountTokenKey]),
-		)
-	} else {
-		config = utils.CreateKubeConfigWithToken(
-			string(childClusterSecret.Data[known.ClusterAPIServerURLKey]),
-			string(childClusterSecret.Data[corev1.ServiceAccountTokenKey]),
-			childClusterSecret.Data[corev1.ServiceAccountRootCAKey],
-		)
-	}
 
 	deployCtx, err := newDeployContext(config)
 	if err != nil {
@@ -310,14 +358,14 @@ func (hd *HelmDeployer) handleHelmRelease(hr *appsapi.HelmRelease) error {
 		}
 
 		hr.Finalizers = utils.RemoveString(hr.Finalizers, known.AppFinalizer)
-		_, err = hd.clusternetclient.AppsV1alpha1().HelmReleases(hr.Namespace).Update(context.TODO(), hr, metav1.UpdateOptions{})
+		_, err = deployer.clusternetClient.AppsV1alpha1().HelmReleases(hr.Namespace).Update(context.TODO(), hr, metav1.UpdateOptions{})
 		return err
 	}
 
 	// install or upgrade helm release
 	chart, err := LocateHelmChart(hr.Spec.Repository, hr.Spec.Chart, hr.Spec.ChartVersion)
 	if err != nil {
-		hd.recorder.Event(hr, corev1.EventTypeWarning, "ChartLocateFailure", err.Error())
+		deployer.recorder.Event(hr, corev1.EventTypeWarning, "ChartLocateFailure", err.Error())
 		return err
 	}
 
@@ -345,7 +393,7 @@ func (hd *HelmDeployer) handleHelmRelease(hr *appsapi.HelmRelease) error {
 			return UpdateRepo(hr.Spec.Repository)
 		}
 
-		if err := hd.helmReleaseController.UpdateHelmReleaseStatus(hr, &appsapi.HelmReleaseStatus{
+		if err := deployer.helmReleaseController.UpdateHelmReleaseStatus(hr, &appsapi.HelmReleaseStatus{
 			Phase: release.StatusFailed,
 			Notes: err.Error(),
 		}); err != nil {
@@ -365,10 +413,10 @@ func (hd *HelmDeployer) handleHelmRelease(hr *appsapi.HelmRelease) error {
 		status.Notes = rel.Info.Notes
 	}
 
-	return hd.helmReleaseController.UpdateHelmReleaseStatus(hr, status)
+	return deployer.helmReleaseController.UpdateHelmReleaseStatus(hr, status)
 }
 
-func (hd *HelmDeployer) handleSecret(secret *corev1.Secret) error {
+func (deployer *Deployer) handleSecret(secret *corev1.Secret) error {
 	if secret.DeletionTimestamp == nil {
 		return nil
 	}
@@ -377,32 +425,21 @@ func (hd *HelmDeployer) handleSecret(secret *corev1.Secret) error {
 		return nil
 	}
 
-	// check wether HelmReleases get cleaned up
-	hrs, err := hd.hrLister.HelmReleases(secret.Namespace).List(labels.SelectorFromSet(labels.Set{}))
+	// check whether HelmReleases get cleaned up
+	hrs, err := deployer.hrLister.HelmReleases(secret.Namespace).List(labels.SelectorFromSet(labels.Set{}))
 	if err != nil {
 		return err
 	}
 
-	if len(hrs) > 0 {
+	if hrs != nil {
 		return fmt.Errorf("waiting all HelmReleases in namespace %s get cleanedup", secret.Namespace)
 	}
 
 	secret.Finalizers = utils.RemoveString(secret.Finalizers, known.AppFinalizer)
-	_, err = hd.kubeclient.CoreV1().Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
+	_, err = deployer.kubeClient.CoreV1().Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
 	if err != nil {
 		klog.WarningDepth(4,
 			fmt.Sprintf("failed to remove finalizer %s from Secrets %s: %v", known.AppFinalizer, klog.KObj(secret), err))
 	}
 	return err
 }
-
-func getChildAPIServerProxyURL(mcls *clusterapi.ManagedCluster) (string, error) {
-	if mcls == nil {
-		return "", errors.New("unable to generate child cluster apiserver proxy url from nil ManagedCluster object")
-	}
-
-	return strings.Join([]string{
-		strings.TrimRight(mcls.Status.ParentAPIServerURL, "/"),
-		"apis", proxiesapi.SchemeGroupVersion.String(), "sockets", string(mcls.Spec.ClusterID),
-		"proxy/direct"}, "/"), nil
-}
diff --git a/pkg/hub/deployer/utils.go b/pkg/hub/deployer/utils.go
index e497636..b5e0cbe 100644
--- a/pkg/hub/deployer/utils.go
+++ b/pkg/hub/deployer/utils.go
@@ -35,7 +35,7 @@ func formatFeed(feed appsapi.Feed) string {
 	return fmt.Sprintf("%s with selector %q", feed.Kind, feed.FeedSelector.String())
 }
 
-func getLabelsSelectorFromFeed(feed appsapi.Feed, namespace string) (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 +46,12 @@ func getLabelsSelectorFromFeed(feed appsapi.Feed, namespace string) (labels.Sele
 	}
 
 	labelSet := labels.Set{
-		known.ConfigGroupLabel:     gv.Group,
-		known.ConfigVersionLabel:   gv.Version,
-		known.ConfigKindLabel:      feed.Kind,
-		known.ConfigNamespaceLabel: namespace,
+		known.ConfigGroupLabel:   gv.Group,
+		known.ConfigVersionLabel: gv.Version,
+		known.ConfigKindLabel:    feed.Kind,
 	}
 	if len(feed.Namespace) > 0 {
-		labelSet[known.ConfigNamespaceLabel] = namespace
+		labelSet[known.ConfigNamespaceLabel] = feed.Namespace
 	}
 	if len(feed.Name) > 0 {
 		labelSet[known.ConfigNameLabel] = feed.Name
diff --git a/pkg/utils/kubeconfig.go b/pkg/utils/kubeconfig.go
index d51e70c..03aab8e 100644
--- a/pkg/utils/kubeconfig.go
+++ b/pkg/utils/kubeconfig.go
@@ -17,12 +17,23 @@ limitations under the License.
 package utils
 
 import (
+	"errors"
 	"fmt"
+	"strings"
 
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apiserver/pkg/authentication/user"
+	corev1lister "k8s.io/client-go/listers/core/v1"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
 	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+	"k8s.io/klog/v2"
+
+	clusterapi "github.com/clusternet/clusternet/pkg/apis/clusters/v1beta1"
+	proxiesapi "github.com/clusternet/clusternet/pkg/apis/proxies/v1alpha1"
+	clusterlisters "github.com/clusternet/clusternet/pkg/generated/listers/clusters/v1beta1"
+	"github.com/clusternet/clusternet/pkg/known"
 )
 
 // createBasicKubeConfig creates a basic, general KubeConfig object that then can be extended
@@ -122,3 +133,54 @@ func applyDefaultRateLimiter(config *rest.Config, flowRate int) *rest.Config {
 
 	return config
 }
+
+func GetChildClusterConfig(secretLister corev1lister.SecretLister, clusterLister clusterlisters.ManagedClusterLister, namespace string, clusterID string) (*clientcmdapi.Config, error) {
+	childClusterSecret, err := secretLister.Secrets(namespace).Get(known.ChildClusterSecretName)
+	if err != nil {
+		return nil, err
+	}
+
+	mcls, err := clusterLister.ManagedClusters(namespace).List(
+		labels.SelectorFromSet(labels.Set{
+			known.ClusterIDLabel: clusterID,
+		}))
+	if err != nil {
+		return nil, err
+	}
+	if mcls == nil {
+		return nil, fmt.Errorf("failed to find a ManagedCluster declaration in namespace %s", namespace)
+	}
+
+	var config *clientcmdapi.Config
+	if len(mcls) > 1 {
+		klog.Warningf("found multiple ManagedCluster declarations in namespace %s", namespace)
+	}
+	if mcls[0].Status.UseSocket {
+		childClusterAPIServer, err := getChildAPIServerProxyURL(mcls[0])
+		if err != nil {
+			return nil, err
+		}
+		config = CreateKubeConfigForSocketProxyWithToken(
+			childClusterAPIServer,
+			string(childClusterSecret.Data[corev1.ServiceAccountTokenKey]),
+		)
+	} else {
+		config = CreateKubeConfigWithToken(
+			string(childClusterSecret.Data[known.ClusterAPIServerURLKey]),
+			string(childClusterSecret.Data[corev1.ServiceAccountTokenKey]),
+			childClusterSecret.Data[corev1.ServiceAccountRootCAKey],
+		)
+	}
+	return config, nil
+}
+
+func getChildAPIServerProxyURL(mcls *clusterapi.ManagedCluster) (string, error) {
+	if mcls == nil {
+		return "", errors.New("unable to generate child cluster apiserver proxy url from nil ManagedCluster object")
+	}
+
+	return strings.Join([]string{
+		strings.TrimRight(mcls.Status.ParentAPIServerURL, "/"),
+		"apis", proxiesapi.SchemeGroupVersion.String(), "sockets", string(mcls.Spec.ClusterID),
+		"proxy/direct"}, "/"), nil
+}


Test patch:
diff --git a/pkg/hub/deployer/helm/helm_test.go b/pkg/utils/kubeconfig_test.go
similarity index 99%
rename from pkg/hub/deployer/helm/helm_test.go
rename to pkg/utils/kubeconfig_test.go
index 0196095..ac40d42 100644
--- a/pkg/hub/deployer/helm/helm_test.go
+++ b/pkg/utils/kubeconfig_test.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package helm
+package utils
 
 import (
 	"testing"