Repository: clusternet/clusternet
Pull request #46 (2021-08-05 04:08:07)

populate Base and Description
<!--  Thanks for sending a pull request!

-->

#### What type of PR is this?
kind/feature

#### What this PR does / why we need it:
- populate base
- populate description
- add controller refs

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
supercede #34 

#### 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 18f0309..73eb134 100644
--- a/manifests/crds/apps.clusternet.io_descriptions.yaml
+++ b/manifests/crds/apps.clusternet.io_descriptions.yaml
@@ -69,8 +69,9 @@ spec:
                 type: string
               raw:
                 description: Raw is the underlying serialization of all objects.
-                format: byte
-                type: string
+                items:
+                  type: object
+                type: array
             required:
             - deployer
             type: object
diff --git a/pkg/apis/apps/v1alpha1/description.go b/pkg/apis/apps/v1alpha1/description.go
index 07eb94a..ad22760 100644
--- a/pkg/apis/apps/v1alpha1/description.go
+++ b/pkg/apis/apps/v1alpha1/description.go
@@ -18,6 +18,7 @@ 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
@@ -57,7 +58,7 @@ type DescriptionSpec struct {
 	// Raw is the underlying serialization of all objects.
 	//
 	// +optional
-	Raw []byte `json:"raw,omitempty"`
+	Raw []runtime.RawExtension `json:"raw,omitempty"`
 }
 
 // DescriptionStatus defines the observed state of Description
@@ -75,7 +76,7 @@ type DescriptionDeployer string
 
 const (
 	DescriptionHelmDeployer    DescriptionDeployer = "Helm"
-	DescriptionDefaultDeployer DescriptionDeployer = "Default"
+	DescriptionGenericDeployer DescriptionDeployer = "Generic"
 )
 
 type DescriptionPhase string
diff --git a/pkg/apis/apps/v1alpha1/manifest.go b/pkg/apis/apps/v1alpha1/manifest.go
index 77412ab..39d3496 100644
--- a/pkg/apis/apps/v1alpha1/manifest.go
+++ b/pkg/apis/apps/v1alpha1/manifest.go
@@ -21,6 +21,11 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+const (
+	// objects will be store into Manifest with ReservedNamespace
+	ReservedNamespace = "clusternet-reserved"
+)
+
 // +genclient
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 // +kubebuilder:resource:scope="Namespaced",categories=clusternet
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
index c1d1338..dc2b2d0 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
@@ -194,8 +194,10 @@ func (in *DescriptionSpec) DeepCopyInto(out *DescriptionSpec) {
 	}
 	if in.Raw != nil {
 		in, out := &in.Raw, &out.Raw
-		*out = make([]byte, len(*in))
-		copy(*out, *in)
+		*out = make([]runtime.RawExtension, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
 	}
 	return
 }
diff --git a/pkg/controllers/apps/base/base.go b/pkg/controllers/apps/base/base.go
index 3351add..6984df2 100644
--- a/pkg/controllers/apps/base/base.go
+++ b/pkg/controllers/apps/base/base.go
@@ -22,10 +22,14 @@ import (
 	"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"
 
@@ -33,6 +37,8 @@ import (
 	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.
@@ -56,12 +62,14 @@ type Controller struct {
 	baseLister appListers.BaseLister
 	baseSynced cache.InformerSynced
 
+	recorder record.EventRecorder
+
 	SyncHandler SyncHandlerFunc
 }
 
 func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
-	baseInformer appInformers.BaseInformer,
-	syncHandler SyncHandlerFunc) (*Controller, error) {
+	baseInformer appInformers.BaseInformer, descInformer appInformers.DescriptionInformer,
+	recorder record.EventRecorder, syncHandler SyncHandlerFunc) (*Controller, error) {
 	if syncHandler == nil {
 		return nil, fmt.Errorf("syncHandler must be set")
 	}
@@ -72,6 +80,7 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "base"),
 		baseLister:       baseInformer.Lister(),
 		baseSynced:       baseInformer.Informer().HasSynced,
+		recorder:         recorder,
 		SyncHandler:      syncHandler,
 	}
 
@@ -82,6 +91,10 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		DeleteFunc: c.deleteBase,
 	})
 
+	descInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		DeleteFunc: c.deleteDescription,
+	})
+
 	return c, nil
 }
 
@@ -114,6 +127,27 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 func (c *Controller) addBase(obj interface{}) {
 	base := obj.(*appsapi.Base)
 	klog.V(4).Infof("adding Base %q", klog.KObj(base))
+
+	// add finalizer
+	if base.DeletionTimestamp == nil {
+		if !utils.ContainsString(base.Finalizers, known.AppFinalizer) {
+			base.Finalizers = append(base.Finalizers, known.AppFinalizer)
+		}
+		_, err := c.clusternetClient.AppsV1alpha1().Bases(base.Namespace).Update(context.TODO(),
+			base, metav1.UpdateOptions{})
+		if err == nil {
+			msg := fmt.Sprintf("successfully inject finalizer %s to Base %s", known.AppFinalizer, klog.KObj(base))
+			klog.V(4).Info(msg)
+			c.recorder.Event(base, corev1.EventTypeNormal, "FinalizerInjected", msg)
+		} else {
+			msg := fmt.Sprintf("failed to inject finalizer %s to Base %s: %v", known.AppFinalizer, klog.KObj(base), err)
+			klog.WarningDepth(4, msg)
+			c.recorder.Event(base, corev1.EventTypeWarning, "FailedInjectingFinalizer", msg)
+			c.addBase(obj)
+			return
+		}
+	}
+
 	c.enqueue(base)
 }
 
@@ -154,6 +188,56 @@ func (c *Controller) deleteBase(obj interface{}) {
 	c.enqueue(base)
 }
 
+func (c *Controller) deleteDescription(obj interface{}) {
+	desc, ok := obj.(*appsapi.Description)
+	if !ok {
+		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
+			return
+		}
+		_, ok = tombstone.Obj.(*appsapi.Description)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Description %#v", obj))
+			return
+		}
+	}
+
+	controllerRef := &metav1.OwnerReference{
+		Kind: desc.Labels[known.ConfigKindLabel],
+		Name: desc.Labels[known.ConfigNameLabel],
+		UID:  types.UID(desc.Labels[known.ConfigUIDLabel]),
+	}
+	base := c.resolveControllerRef(desc.Labels[known.ConfigNamespaceLabel], controllerRef)
+	if base == nil {
+		return
+	}
+	klog.V(4).Infof("deleting Description %q", klog.KObj(desc))
+	c.enqueue(base)
+}
+
+// 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 (c *Controller) resolveControllerRef(namespace string, controllerRef *metav1.OwnerReference) *appsapi.Base {
+	// 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 != controllerKind.Kind {
+		return nil
+	}
+
+	base, err := c.baseLister.Bases(namespace).Get(controllerRef.Name)
+	if err != nil {
+		return nil
+	}
+	if base.UID != controllerRef.UID {
+		// The controller we found with this Name is not the same one that the
+		// ControllerRef points to.
+		return nil
+	}
+	return base
+}
+
 // runWorker is a long-running function that will continually call the
 // processNextWorkItem function in order to read and process a message on the
 // workqueue.
diff --git a/pkg/controllers/apps/description/description.go b/pkg/controllers/apps/description/description.go
index 0c6e6c5..284da22 100644
--- a/pkg/controllers/apps/description/description.go
+++ b/pkg/controllers/apps/description/description.go
@@ -22,11 +22,13 @@ import (
 	"reflect"
 	"time"
 
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	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/retry"
 	"k8s.io/client-go/util/workqueue"
 	"k8s.io/klog/v2"
@@ -35,6 +37,8 @@ import (
 	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.
@@ -59,12 +63,14 @@ type Controller struct {
 	descSynced cache.InformerSynced
 	hrSynced   cache.InformerSynced
 
+	recorder record.EventRecorder
+
 	SyncHandler SyncHandlerFunc
 }
 
 func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
 	descInformer appInformers.DescriptionInformer, hrInformer appInformers.HelmReleaseInformer,
-	syncHandler SyncHandlerFunc) (*Controller, error) {
+	recorder record.EventRecorder, syncHandler SyncHandlerFunc) (*Controller, error) {
 	if syncHandler == nil {
 		return nil, fmt.Errorf("syncHandler must be set")
 	}
@@ -76,6 +82,7 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		descLister:       descInformer.Lister(),
 		descSynced:       descInformer.Informer().HasSynced,
 		hrSynced:         hrInformer.Informer().HasSynced,
+		recorder:         recorder,
 		SyncHandler:      syncHandler,
 	}
 
@@ -87,7 +94,6 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 	})
 
 	hrInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
-		UpdateFunc: c.updateHelmRelease,
 		DeleteFunc: c.deleteHelmRelease,
 	})
 
@@ -123,6 +129,27 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
 func (c *Controller) addDescription(obj interface{}) {
 	desc := obj.(*appsapi.Description)
 	klog.V(4).Infof("adding Description %q", klog.KObj(desc))
+
+	// add finalizer
+	if desc.DeletionTimestamp == nil {
+		if !utils.ContainsString(desc.Finalizers, known.AppFinalizer) {
+			desc.Finalizers = append(desc.Finalizers, known.AppFinalizer)
+		}
+		_, err := c.clusternetClient.AppsV1alpha1().Descriptions(desc.Namespace).Update(context.TODO(),
+			desc, metav1.UpdateOptions{})
+		if err == nil {
+			msg := fmt.Sprintf("successfully inject finalizer %s to Description %s", known.AppFinalizer, klog.KObj(desc))
+			klog.V(4).Info(msg)
+			c.recorder.Event(desc, corev1.EventTypeNormal, "FinalizerInjected", msg)
+		} else {
+			msg := fmt.Sprintf("failed to inject finalizer %s to Description %s: %v", known.AppFinalizer, klog.KObj(desc), err)
+			klog.WarningDepth(4, msg)
+			c.recorder.Event(desc, corev1.EventTypeWarning, "FailedInjectingFinalizer", msg)
+			c.addDescription(obj)
+			return
+		}
+	}
+
 	c.enqueue(desc)
 }
 
@@ -163,27 +190,6 @@ func (c *Controller) deleteDescription(obj interface{}) {
 	c.enqueue(desc)
 }
 
-func (c *Controller) updateHelmRelease(old, cur interface{}) {
-	// this update could be an update of removing finalizers
-	hr := cur.(*appsapi.HelmRelease)
-
-	if hr.DeletionTimestamp == nil {
-		return
-	}
-
-	controllerRef := metav1.GetControllerOf(hr)
-	if controllerRef == nil {
-		// No controller should care about orphans being deleted.
-		return
-	}
-	desc := c.resolveControllerRef(hr.Namespace, controllerRef)
-	if desc == nil {
-		return
-	}
-	klog.V(4).Infof("deleting HelmRelease %q", klog.KObj(hr))
-	c.enqueue(desc)
-}
-
 func (c *Controller) deleteHelmRelease(obj interface{}) {
 	hr, ok := obj.(*appsapi.HelmRelease)
 	if !ok {
diff --git a/pkg/controllers/apps/manifest/manifest.go b/pkg/controllers/apps/manifest/manifest.go
new file mode 100644
index 0000000..8a6ce29
--- /dev/null
+++ b/pkg/controllers/apps/manifest/manifest.go
@@ -0,0 +1,263 @@
+/*
+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 manifest
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"time"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/tools/cache"
+	"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"
+)
+
+// controllerKind contains the schema.GroupVersionKind for this controller type.
+var controllerKind = appsapi.SchemeGroupVersion.WithKind("Manifest")
+
+type SyncHandlerFunc func(orig *appsapi.Manifest) error
+
+// Controller is a controller that handle Manifest
+type Controller struct {
+	ctx context.Context
+
+	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
+	// means we can ensure we only process a fixed amount of resources at a
+	// time, and makes it easy to ensure we are never processing the same item
+	// simultaneously in two different workers.
+	workqueue workqueue.RateLimitingInterface
+
+	manifestLister appListers.ManifestLister
+	manifestSynced cache.InformerSynced
+
+	SyncHandler SyncHandlerFunc
+}
+
+func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
+	manifestInformer appInformers.ManifestInformer,
+	syncHandler SyncHandlerFunc) (*Controller, error) {
+	if syncHandler == nil {
+		return nil, fmt.Errorf("syncHandler must be set")
+	}
+
+	c := &Controller{
+		ctx:              ctx,
+		clusternetClient: clusternetClient,
+		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "manifest"),
+		manifestLister:   manifestInformer.Lister(),
+		manifestSynced:   manifestInformer.Informer().HasSynced,
+		SyncHandler:      syncHandler,
+	}
+
+	// Manage the addition/update of Manifest
+	manifestInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc:    c.addManifest,
+		UpdateFunc: c.updateManifest,
+		DeleteFunc: c.deleteManifest,
+	})
+
+	return c, nil
+}
+
+// Run will set up the event handlers for types we are interested in, as well
+// as syncing informer caches and starting workers. It will block until stopCh
+// is closed, at which point it will shutdown the workqueue and wait for
+// workers to finish processing their current work items.
+func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
+	defer utilruntime.HandleCrash()
+	defer c.workqueue.ShutDown()
+
+	klog.Info("starting manifest controller...")
+	defer klog.Info("shutting down manifest controller")
+
+	// 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.manifestSynced) {
+		return
+	}
+
+	klog.V(5).Infof("starting %d worker threads", workers)
+	// Launch workers to process Manifest resources
+	for i := 0; i < workers; i++ {
+		go wait.Until(c.runWorker, time.Second, stopCh)
+	}
+
+	<-stopCh
+}
+
+func (c *Controller) addManifest(obj interface{}) {
+	manifest := obj.(*appsapi.Manifest)
+	klog.V(4).Infof("adding Manifest %q", klog.KObj(manifest))
+	c.enqueue(manifest)
+}
+
+func (c *Controller) updateManifest(old, cur interface{}) {
+	oldManifest := old.(*appsapi.Manifest)
+	newManifest := cur.(*appsapi.Manifest)
+
+	if newManifest.DeletionTimestamp != nil {
+		c.enqueue(newManifest)
+		return
+	}
+
+	// Decide whether discovery has reported a spec change.
+	if reflect.DeepEqual(oldManifest.Template, newManifest.Template) {
+		klog.V(4).Infof("no updates on Manifest template %q, skipping syncing", oldManifest.Name)
+		return
+	}
+
+	klog.V(4).Infof("updating Manifest %q", klog.KObj(oldManifest))
+	c.enqueue(newManifest)
+}
+
+func (c *Controller) deleteManifest(obj interface{}) {
+	manifest, ok := obj.(*appsapi.Manifest)
+	if !ok {
+		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
+			return
+		}
+		_, ok = tombstone.Obj.(*appsapi.Manifest)
+		if !ok {
+			utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Manifest %#v", obj))
+			return
+		}
+	}
+	klog.V(4).Infof("deleting Manifest %q", klog.KObj(manifest))
+	c.enqueue(manifest)
+}
+
+// runWorker is a long-running function that will continually call the
+// processNextWorkItem function in order to read and process a message on the
+// workqueue.
+func (c *Controller) runWorker() {
+	for c.processNextWorkItem() {
+	}
+}
+
+// processNextWorkItem will read a single work item off the workqueue and
+// attempt to process it, by calling the syncHandler.
+func (c *Controller) processNextWorkItem() bool {
+	obj, shutdown := c.workqueue.Get()
+
+	if shutdown {
+		return false
+	}
+
+	// We wrap this block in a func so we can defer c.workqueue.Done.
+	err := func(obj interface{}) error {
+		// We call Done here so the workqueue knows we have finished
+		// processing this item. We also must remember to call Forget if we
+		// do not want this work item being re-queued. For example, we do
+		// not call Forget if a transient error occurs, instead the item is
+		// put back on the workqueue and attempted again after a back-off
+		// period.
+		defer c.workqueue.Done(obj)
+		var key string
+		var ok bool
+		// We expect strings to come off the workqueue. These are of the
+		// form namespace/name. We do this as the delayed nature of the
+		// workqueue means the items in the informer cache may actually be
+		// more up to date that when the item was initially put onto the
+		// workqueue.
+		if key, ok = obj.(string); !ok {
+			// As the item in the workqueue is actually invalid, we call
+			// Forget here else we'd go into a loop of attempting to
+			// process a work item that is invalid.
+			c.workqueue.Forget(obj)
+			utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
+			return nil
+		}
+		// Run the syncHandler, passing it the namespace/name string of the
+		// Manifest resource to be synced.
+		if err := c.syncHandler(key); err != nil {
+			// Put the item back on the workqueue to handle any transient errors.
+			c.workqueue.AddRateLimited(key)
+			return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
+		}
+		// Finally, if no error occurs we Forget this item so it does not
+		// get queued again until another change happens.
+		c.workqueue.Forget(obj)
+		klog.Infof("successfully synced Manifest %q", key)
+		return nil
+	}(obj)
+
+	if err != nil {
+		utilruntime.HandleError(err)
+		return true
+	}
+
+	return true
+}
+
+// syncHandler compares the actual state with the desired, and attempts to
+// converge the two. It then updates the Status block of the Manifest resource
+// with the current status of the resource.
+func (c *Controller) syncHandler(key string) error {
+	// If an error occurs during handling, we'll requeue the item so we can
+	// attempt processing again later. This could have been caused by a
+	// temporary network failure, or any other transient reason.
+
+	// Convert the namespace/name string into a distinct namespace and name
+	ns, name, err := cache.SplitMetaNamespaceKey(key)
+	if err != nil {
+		utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
+		return nil
+	}
+
+	klog.V(4).Infof("start processing Manifest %q", key)
+	// Get the Manifest resource with this name
+	manifest, err := c.manifestLister.Manifests(ns).Get(name)
+	// The Manifest resource may no longer exist, in which case we stop processing.
+	if errors.IsNotFound(err) {
+		klog.V(2).Infof("Manifest %q has been deleted", key)
+		return nil
+	}
+	if err != nil {
+		return err
+	}
+
+	manifest.Kind = controllerKind.Kind
+	manifest.APIVersion = controllerKind.Version
+
+	return c.SyncHandler(manifest)
+}
+
+// enqueue takes a Manifest resource and converts it into a namespace/name
+// string which is then put onto the work queue. This method should *not* be
+// passed resources of any type other than Manifest.
+func (c *Controller) enqueue(manifest *appsapi.Manifest) {
+	key, err := cache.MetaNamespaceKeyFunc(manifest)
+	if err != nil {
+		utilruntime.HandleError(err)
+		return
+	}
+	c.workqueue.Add(key)
+}
diff --git a/pkg/controllers/apps/subscription/subscription.go b/pkg/controllers/apps/subscription/subscription.go
index dc04a1e..176922b 100644
--- a/pkg/controllers/apps/subscription/subscription.go
+++ b/pkg/controllers/apps/subscription/subscription.go
@@ -22,20 +22,22 @@ import (
 	"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/retry"
 	"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"
 )
@@ -49,7 +51,7 @@ type SyncHandlerFunc func(subscription *appsapi.Subscription) 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,16 +60,18 @@ type Controller struct {
 	// simultaneously in two different workers.
 	workqueue workqueue.RateLimitingInterface
 
-	subsLister appListers.SubscriptionLister
+	subsLister applisters.SubscriptionLister
 	subsSynced cache.InformerSynced
-	descSynced cache.InformerSynced
+	baseSynced cache.InformerSynced
+
+	recorder record.EventRecorder
 
 	SyncHandler SyncHandlerFunc
 }
 
-func NewController(ctx context.Context, clusternetClient clusternetClientSet.Interface,
-	subsInformer appInformers.SubscriptionInformer, descInformer appInformers.DescriptionInformer,
-	syncHandler SyncHandlerFunc) (*Controller, error) {
+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")
 	}
@@ -78,7 +82,8 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "subscription"),
 		subsLister:       subsInformer.Lister(),
 		subsSynced:       subsInformer.Informer().HasSynced,
-		descSynced:       descInformer.Informer().HasSynced,
+		baseSynced:       baseInformer.Informer().HasSynced,
+		recorder:         recorder,
 		SyncHandler:      syncHandler,
 	}
 
@@ -89,8 +94,8 @@ func NewController(ctx context.Context, clusternetClient clusternetClientSet.Int
 		DeleteFunc: c.deleteSubscription,
 	})
 
-	descInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
-		DeleteFunc: c.deleteDescription,
+	baseInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		DeleteFunc: c.deleteBase,
 	})
 
 	return c, nil
@@ -109,7 +114,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.subsSynced, c.descSynced) {
+	if !cache.WaitForCacheSync(stopCh, c.subsSynced, c.baseSynced) {
 		return
 	}
 
@@ -136,10 +141,11 @@ func (c *Controller) addSubscription(obj interface{}) {
 		if err == nil {
 			msg := fmt.Sprintf("successfully inject finalizer %s to Subscription %s", known.AppFinalizer, klog.KObj(subs))
 			klog.V(4).Info(msg)
-			// todo: add recorder
+			c.recorder.Event(subs, corev1.EventTypeNormal, "FinalizerInjected", msg)
 		} else {
-			klog.WarningDepth(4,
-				fmt.Sprintf("failed to inject finalizer %s to Subscription %s: %v", known.AppFinalizer, klog.KObj(subs), err))
+			msg := fmt.Sprintf("failed to inject finalizer %s to Subscription %s: %v", known.AppFinalizer, klog.KObj(subs), err)
+			klog.WarningDepth(4, msg)
+			c.recorder.Event(subs, corev1.EventTypeWarning, "FailedInjectingFinalizer", msg)
 			c.addSubscription(obj)
 			return
 		}
@@ -186,48 +192,38 @@ func (c *Controller) deleteSubscription(obj interface{}) {
 	c.enqueue(subs)
 }
 
-func (c *Controller) deleteDescription(obj interface{}) {
-	desc, ok := obj.(*appsapi.Description)
+func (c *Controller) deleteBase(obj interface{}) {
+	base, ok := obj.(*appsapi.Base)
 	if !ok {
 		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
 		if !ok {
 			utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
 			return
 		}
-		_, ok = tombstone.Obj.(*appsapi.Description)
+		_, ok = tombstone.Obj.(*appsapi.Base)
 		if !ok {
-			utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Description %#v", obj))
+			utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Base %#v", obj))
 			return
 		}
 	}
 
-	controllerRef := &metav1.OwnerReference{
-		Kind: desc.Labels[known.ConfigKindLabel],
-		Name: desc.Labels[known.ConfigNameLabel],
-		UID:  types.UID(desc.Labels[known.ConfigUIDLabel]),
-	}
-	subs := c.resolveControllerRef(desc.Labels[known.ConfigNamespaceLabel], controllerRef)
-	if subs == nil {
+	sub := c.resolveControllerRef(base.Labels[known.ConfigNameLabel], base.Labels[known.ConfigNamespaceLabel], types.UID(base.Labels[known.ConfigUIDLabel]))
+	if sub == nil {
 		return
 	}
-	klog.V(4).Infof("deleting Description %q", klog.KObj(desc))
-	c.enqueue(subs)
+	klog.V(4).Infof("deleting Base %q", klog.KObj(base))
+	c.enqueue(sub)
 }
 
 // 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 (c *Controller) resolveControllerRef(namespace string, controllerRef *metav1.OwnerReference) *appsapi.Subscription {
-	// 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 != controllerKind.Kind {
-		return nil
-	}
-	subs, err := c.subsLister.Subscriptions(namespace).Get(controllerRef.Name)
+func (c *Controller) resolveControllerRef(name, namespace string, uid types.UID) *appsapi.Subscription {
+	subs, err := c.subsLister.Subscriptions(namespace).Get(name)
 	if err != nil {
 		return nil
 	}
-	if subs.UID != controllerRef.UID {
+	if subs.UID != uid {
 		// The controller we found with this Name is not the same one that the
 		// ControllerRef points to.
 		return nil
diff --git a/pkg/hub/deployer/deployer.go b/pkg/hub/deployer/deployer.go
index fc489e5..22f9d25 100644
--- a/pkg/hub/deployer/deployer.go
+++ b/pkg/hub/deployer/deployer.go
@@ -25,22 +25,28 @@ import (
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
-	kubeInformers "k8s.io/client-go/informers"
+	"k8s.io/apimachinery/pkg/util/sets"
+	kubeinformers "k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes/scheme"
 	v1core "k8s.io/client-go/kubernetes/typed/core/v1"
+	"k8s.io/client-go/tools/cache"
 	"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"
+	"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"
+	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/helm"
 	"github.com/clusternet/clusternet/pkg/known"
 	"github.com/clusternet/clusternet/pkg/utils"
@@ -49,23 +55,33 @@ import (
 var (
 	helmChartKind    = appsapi.SchemeGroupVersion.WithKind("HelmChart")
 	subscriptionKind = appsapi.SchemeGroupVersion.WithKind("Subscription")
+	baseKind         = appsapi.SchemeGroupVersion.WithKind("Base")
 	descriptionKind  = appsapi.SchemeGroupVersion.WithKind("Description")
 )
 
+const (
+	defaultScheduler = "default"
+)
+
 // Deployer defines configuration for the application deployer
 type Deployer struct {
 	ctx context.Context
 
-	chartLister   appListers.HelmChartLister
-	descLister    appListers.DescriptionLister
-	hrLister      appListers.HelmReleaseLister
-	clusterLister clusterListers.ManagedClusterLister
+	chartLister   applisters.HelmChartLister
+	descLister    applisters.DescriptionLister
+	subsLister    applisters.SubscriptionLister
+	hrLister      applisters.HelmReleaseLister
+	baseLister    applisters.BaseLister
+	mfstLister    applisters.ManifestLister
+	clusterLister clusterlisters.ManagedClusterLister
 
 	kubeclient       *kubernetes.Clientset
-	clusternetclient *clusternetClientSet.Clientset
+	clusternetclient *clusternetclientset.Clientset
 
 	subsController *subscription.Controller
 	descController *description.Controller
+	mfstController *manifest.Controller
+	baseController *base.Controller
 
 	helmDeployer *helm.HelmDeployer
 
@@ -73,15 +89,18 @@ type Deployer struct {
 	recorder    record.EventRecorder
 }
 
-func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clusternetclient *clusternetClientSet.Clientset,
-	clusternetInformerFactory clusternetInformers.SharedInformerFactory, kubeInformerFactory kubeInformers.SharedInformerFactory) (*Deployer, error) {
+func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clusternetclient *clusternetclientset.Clientset,
+	clusternetInformerFactory clusternetinformers.SharedInformerFactory, kubeInformerFactory kubeinformers.SharedInformerFactory) (*Deployer, error) {
 
 	deployer := &Deployer{
 		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(),
 		kubeclient:       kubeclient,
 		clusternetclient: clusternetclient,
 		broadcaster:      record.NewBroadcaster(),
@@ -105,7 +124,8 @@ func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clustern
 	subsController, err := subscription.NewController(ctx,
 		clusternetclient,
 		clusternetInformerFactory.Apps().V1alpha1().Subscriptions(),
-		clusternetInformerFactory.Apps().V1alpha1().Descriptions(),
+		clusternetInformerFactory.Apps().V1alpha1().Bases(),
+		deployer.recorder,
 		deployer.handleSubscription)
 	if err != nil {
 		return nil, err
@@ -116,12 +136,33 @@ func NewDeployer(ctx context.Context, kubeclient *kubernetes.Clientset, clustern
 		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(),
+		deployer.handleManifest)
+	if err != nil {
+		return nil, err
+	}
+	deployer.mfstController = mfstController
+
+	baseController, err := base.NewController(ctx,
+		clusternetclient,
+		clusternetInformerFactory.Apps().V1alpha1().Bases(),
+		clusternetInformerFactory.Apps().V1alpha1().Descriptions(),
+		deployer.recorder,
+		deployer.handleBase)
+	if err != nil {
+		return nil, err
+	}
+	deployer.baseController = baseController
+
 	return deployer, nil
 }
 
@@ -131,134 +172,90 @@ func (deployer *Deployer) Run(workers int) {
 	go deployer.helmDeployer.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())
 
 	<-deployer.ctx.Done()
 }
 
-func (deployer *Deployer) handleSubscription(subs *appsapi.Subscription) error {
-	if subs.DeletionTimestamp != nil {
-		descs, err := deployer.descLister.List(labels.SelectorFromSet(labels.Set{
+func (deployer *Deployer) handleSubscription(sub *appsapi.Subscription) error {
+	if sub.DeletionTimestamp != nil {
+		bases, err := deployer.baseLister.List(labels.SelectorFromSet(labels.Set{
 			known.ConfigKindLabel:      subscriptionKind.Kind,
-			known.ConfigNameLabel:      subs.Name,
-			known.ConfigNamespaceLabel: subs.Namespace,
+			known.ConfigNameLabel:      sub.Name,
+			known.ConfigNamespaceLabel: sub.Namespace,
+			known.ConfigUIDLabel:       string(sub.UID),
 		}))
 		if err != nil {
 			return err
 		}
-		// delete all matching Description
+
+		// delete all matching Base
 		deletePropagationBackground := metav1.DeletePropagationBackground
-		for _, desc := range descs {
-			if desc.DeletionTimestamp != nil {
+		for _, base := range bases {
+			if base.DeletionTimestamp != nil {
 				continue
 			}
-			err = deployer.clusternetclient.AppsV1alpha1().Descriptions(desc.Namespace).Delete(context.TODO(), desc.Name, metav1.DeleteOptions{
+			err = deployer.clusternetclient.AppsV1alpha1().Bases(base.Namespace).Delete(context.TODO(), base.Name, metav1.DeleteOptions{
 				PropagationPolicy: &deletePropagationBackground,
 			})
 			if err != nil {
 				return err
 			}
 		}
-		if descs != nil {
-			return fmt.Errorf("waiting for Descriptions belongs to Subscription %s getting deleted", klog.KObj(subs))
+		if bases != nil {
+			return fmt.Errorf("waiting for Bases belongs to Subscription %s getting deleted", klog.KObj(sub))
 		}
 
-		subs.Finalizers = utils.RemoveString(subs.Finalizers, known.AppFinalizer)
-		_, err = deployer.clusternetclient.AppsV1alpha1().Subscriptions(subs.Namespace).Update(context.TODO(), subs, metav1.UpdateOptions{})
+		sub.Finalizers = utils.RemoveString(sub.Finalizers, known.AppFinalizer)
+		_, 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(subs), err))
+				fmt.Sprintf("failed to remove finalizer %s from Subscription %s: %v", known.AppFinalizer, klog.KObj(sub), err))
 		}
 		return err
 	}
 
-	var charts []*appsapi.HelmChart
-	for _, feed := range subs.Spec.Feeds {
-		if feed.Kind != helmChartKind.Kind && feed.APIVersion != helmChartKind.Version {
-			msg := fmt.Sprintf("Subscription only support %s for now", helmChartKind.String())
-			klog.Error(msg)
-			deployer.recorder.Event(subs, corev1.EventTypeWarning, "OnlySupportHelmChart", msg)
-			return nil
-		}
-
-		chartList, err := deployer.getChartsBySelector(subs, feed)
-		if errors.IsNotFound(err) {
-			msg := fmt.Sprintf("Subscription %s is using a nonexistent HelmChart %s/%s", klog.KObj(subs), subs.Namespace, feed.Name)
-			klog.Error(msg)
-			deployer.recorder.Event(subs, corev1.EventTypeWarning, "NonexistentHelmChart", msg)
-			return nil
-		}
-		if err != nil {
-			msg := fmt.Sprintf("failed to get charts matching %q for Subscription %s: %v", feed, klog.KObj(subs), err)
-			klog.Error(msg)
-			deployer.recorder.Event(subs, corev1.EventTypeWarning, "FailedRetrievingHelmCharts", msg)
-			return err
-		}
-		charts = append(charts, chartList...)
-	}
-
-	if len(charts) == 0 {
-		deployer.recorder.Event(subs, corev1.EventTypeWarning, "NoHelmCharts", "No helm charts get matched")
+	if sub.Spec.SchedulerName != defaultScheduler {
+		klog.V(4).Infof("Subscription %s is using customized scheduler %q ", klog.KObj(sub), sub.Spec.SchedulerName)
 		return nil
 	}
 
-	// verify HelmChart can be found
-	var chartRefs []appsapi.ChartReference
-	for _, chart := range charts {
-		if len(chart.Status.Phase) == 0 {
-			msg := fmt.Sprintf("HelmChart %s is in verifying", klog.KObj(chart))
-			klog.Warning(msg)
-			deployer.recorder.Event(subs, corev1.EventTypeWarning, "VerifyingHelmChart", msg)
-			return fmt.Errorf(msg)
-		}
-
-		if chart.Status.Phase != appsapi.HelmChartFound {
-			deployer.recorder.Event(subs, corev1.EventTypeWarning, "HelmChartNotFound",
-				fmt.Sprintf("helm chart %s is not found", klog.KObj(chart)))
-			return nil
-		}
-
-		chartRefs = append(chartRefs, appsapi.ChartReference{
-			Name:      chart.Name,
-			Namespace: chart.Namespace,
-		})
+	err := deployer.populateBases(sub)
+	if err != nil {
+		return err
 	}
 
-	deployer.recorder.Event(subs, corev1.EventTypeNormal, "HelmChartsMatched", "helm charts get matched")
-
-	return deployer.populateDescriptionsForHelm(subs, chartRefs)
+	return nil
 }
 
-func (deployer *Deployer) getChartsBySelector(subs *appsapi.Subscription, feed appsapi.Feed) ([]*appsapi.HelmChart, error) {
-	namespace := subs.Namespace
-	if len(feed.Namespace) != 0 {
+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
+	selector, err := getLabelsSelectorFromFeed(feed, namespace)
+	if err != nil {
+		return nil, err
 	}
+	return deployer.chartLister.HelmCharts(namespace).List(selector)
+}
 
-	if feed.FeedSelector != nil {
-		selector, err := metav1.LabelSelectorAsSelector(feed.FeedSelector)
-		if err != nil {
-			return nil, err
-		}
-		chartList, err := deployer.chartLister.HelmCharts(subs.Namespace).List(selector)
-		if err != nil {
-			return nil, err
-		}
-		return chartList, nil
+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
 	}
-
-	return []*appsapi.HelmChart{}, nil
+	selector, err := getLabelsSelectorFromFeed(feed, namespace)
+	if err != nil {
+		return nil, err
+	}
+	return deployer.mfstLister.Manifests(appsapi.ReservedNamespace).List(selector)
 }
 
-func (deployer *Deployer) populateDescriptionsForHelm(subs *appsapi.Subscription, chartRefs []appsapi.ChartReference) error {
+func (deployer *Deployer) populateBases(sub *appsapi.Subscription) error {
 	var mcls []*clusterapi.ManagedCluster
-	for _, subscriber := range subs.Spec.Subscribers {
+	for _, subscriber := range sub.Spec.Subscribers {
 		selector, err := metav1.LabelSelectorAsSelector(subscriber.ClusterAffinity)
 		if err != nil {
 			return err
@@ -269,70 +266,305 @@ func (deployer *Deployer) populateDescriptionsForHelm(subs *appsapi.Subscription
 		}
 
 		if clusters == nil {
-			deployer.recorder.Event(subs, corev1.EventTypeWarning, "NoClusters", "No clusters get matched")
+			deployer.recorder.Event(sub, corev1.EventTypeWarning, "NoClusters", "No clusters get matched")
 			return nil
 		}
 
 		mcls = append(mcls, clusters...)
 	}
 
+	allExistingBases, err := deployer.baseLister.List(labels.SelectorFromSet(labels.Set{
+		known.ConfigKindLabel:      subscriptionKind.Kind,
+		known.ConfigNameLabel:      sub.Name,
+		known.ConfigNamespaceLabel: sub.Namespace,
+		known.ConfigUIDLabel:       string(sub.UID),
+	}))
+	if err != nil {
+		return err
+	}
+	// Bases to be deleted
+	basesToBeDeleted := sets.String{}
+	for _, base := range allExistingBases {
+		basesToBeDeleted.Insert(fmt.Sprintf("%s/%s", base.Namespace, base.Name))
+	}
+
 	var allErrs []error
 	for _, cluster := range mcls {
-		if !cluster.Status.AppPusher {
-			msg := fmt.Sprintf("skip deploying Subscription %s to cluster %s for disabling AppPusher",
-				klog.KObj(subs), cluster.Spec.ClusterID)
-			klog.V(4).Info(msg)
-			deployer.recorder.Event(subs, corev1.EventTypeNormal, "SkipDeploying", msg)
-			continue
-		}
-		if cluster.Spec.SyncMode == clusterapi.Pull {
-			msg := fmt.Sprintf("skip deploying Subscription %s to cluster %s with sync mode setting to Pull",
-				klog.KObj(subs), cluster.Spec.ClusterID)
-			klog.V(4).Info(msg)
-			deployer.recorder.Event(subs, corev1.EventTypeNormal, "SkipDeploying", msg)
-			continue
-		}
-
-		description := &appsapi.Description{
+		base := &appsapi.Base{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:      subs.Name,
+				Name:      sub.Name,
 				Namespace: cluster.Namespace,
 				Labels: map[string]string{
 					known.ObjectCreatedByLabel: known.ClusternetHubName,
+					known.ConfigKindLabel:      subscriptionKind.Kind,
+					known.ConfigNameLabel:      sub.Name,
+					known.ConfigNamespaceLabel: sub.Namespace,
+					known.ConfigUIDLabel:       string(sub.UID),
 					known.ClusterIDLabel:       cluster.Labels[known.ClusterIDLabel],
 					known.ClusterNameLabel:     cluster.Labels[known.ClusterNameLabel],
-					known.ConfigKindLabel:      subscriptionKind.Kind,
-					known.ConfigNameLabel:      subs.Name,
-					known.ConfigNamespaceLabel: subs.Namespace,
-					known.ConfigUIDLabel:       string(subs.UID),
 					// add subscription info
-					known.ConfigSubscriptionNameLabel:      subs.Name,
-					known.ConfigSubscriptionNamespaceLabel: subs.Namespace,
-					known.ConfigSubscriptionUIDLabel:       string(subs.UID),
+					known.ConfigSubscriptionNameLabel:      sub.Name,
+					known.ConfigSubscriptionNamespaceLabel: sub.Namespace,
+					known.ConfigSubscriptionUIDLabel:       string(sub.UID),
 				},
 				Finalizers: []string{
 					known.AppFinalizer,
 				},
+				// Base and Subscription are in different namespaces
+			},
+			Spec: appsapi.BaseSpec{
+				Feeds: sub.Spec.Feeds,
+			},
+		}
+
+		basesToBeDeleted.Delete(fmt.Sprintf("%s/%s", base.Namespace, base.Name))
+
+		err := deployer.syncBase(sub, base)
+		if err != nil {
+			allErrs = append(allErrs, err)
+			msg := fmt.Sprintf("Failed to sync Base %s: %v", klog.KObj(base), err)
+			klog.ErrorDepth(5, msg)
+			deployer.recorder.Event(sub, corev1.EventTypeWarning, "FailedSyncingBase", msg)
+		}
+	}
+
+	deletePropagationBackground := metav1.DeletePropagationBackground
+	for key := range basesToBeDeleted {
+		// Convert the namespace/name string into a distinct namespace and name
+		ns, name, err := cache.SplitMetaNamespaceKey(key)
+		if err != nil {
+			allErrs = append(allErrs, err)
+		} else {
+			err = deployer.clusternetclient.AppsV1alpha1().Bases(ns).Delete(context.TODO(), name, metav1.DeleteOptions{
+				PropagationPolicy: &deletePropagationBackground,
+			})
+			if err != nil {
+				allErrs = append(allErrs, err)
+			}
+		}
+	}
+
+	return utilerrors.NewAggregate(allErrs)
+}
+
+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.DeletionTimestamp != nil {
+			return fmt.Errorf("Base %s is deleting, will resync later", klog.KObj(curBase))
+		}
+
+		// update it
+		if !reflect.DeepEqual(curBase.Spec, base.Spec) {
+			if curBase.Labels == nil {
+				curBase.Labels = make(map[string]string)
+			}
+			for key, value := range base.Labels {
+				curBase.Labels[key] = value
+			}
+
+			curBase.Spec = base.Spec
+			if !utils.ContainsString(curBase.Finalizers, known.AppFinalizer) {
+				curBase.Finalizers = append(curBase.Finalizers, known.AppFinalizer)
+			}
+
+			_, 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))
+				klog.V(4).Info(msg)
+				deployer.recorder.Event(sub, corev1.EventTypeNormal, "BaseUpdated", msg)
+			}
+			return err
+		}
+		return nil
+	}
+
+	_, err = deployer.clusternetclient.AppsV1alpha1().Bases(curBase.Namespace).Create(context.TODO(),
+		base, metav1.CreateOptions{})
+	if err == nil {
+		msg := fmt.Sprintf("Base %s is created successfully", klog.KObj(base))
+		klog.V(4).Info(msg)
+		deployer.recorder.Event(sub, corev1.EventTypeNormal, "BaseCreated", msg)
+	}
+	return err
+}
+
+func (deployer *Deployer) handleBase(base *appsapi.Base) error {
+	if base.DeletionTimestamp != nil {
+		descs, err := deployer.descLister.List(labels.SelectorFromSet(labels.Set{
+			known.ConfigKindLabel:      baseKind.Kind,
+			known.ConfigNameLabel:      base.Name,
+			known.ConfigNamespaceLabel: base.Namespace,
+			known.ConfigUIDLabel:       string(base.UID),
+		}))
+		if err != nil {
+			return err
+		}
+
+		// delete all matching Descriptions
+		deletePropagationBackground := metav1.DeletePropagationBackground
+		for _, desc := range descs {
+			if desc.DeletionTimestamp != nil {
+				continue
+			}
+			err = deployer.clusternetclient.AppsV1alpha1().Descriptions(base.Namespace).Delete(context.TODO(), desc.Name, metav1.DeleteOptions{
+				PropagationPolicy: &deletePropagationBackground,
+			})
+			if err != nil {
+				return err
+			}
+		}
+		if descs != nil {
+			return fmt.Errorf("waiting for Descriptions belongs to Base %s getting deleted", klog.KObj(base))
+		}
+
+		base.Finalizers = utils.RemoveString(base.Finalizers, known.AppFinalizer)
+		_, 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))
+		}
+		return err
+	}
+
+	err := deployer.populateDescriptions(base)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (deployer *Deployer) populateDescriptions(base *appsapi.Base) error {
+	var allChartRefs []appsapi.ChartReference
+	var allManifests []*appsapi.Manifest
+
+	var err error
+	var charts []*appsapi.HelmChart
+	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)
+			if err == nil {
+				// verify HelmChart can be found
+				for _, chart := range charts {
+					if len(chart.Status.Phase) == 0 {
+						msg := fmt.Sprintf("HelmChart %s is in verifying", klog.KObj(chart))
+						klog.Warning(msg)
+						deployer.recorder.Event(base, corev1.EventTypeWarning, "VerifyingHelmChart", msg)
+						return fmt.Errorf(msg)
+					}
+
+					if chart.Status.Phase != appsapi.HelmChartFound {
+						deployer.recorder.Event(base, corev1.EventTypeWarning, "HelmChartNotFound",
+							fmt.Sprintf("helm chart %s is not found", klog.KObj(chart)))
+						return nil
+					} else {
+						allChartRefs = append(allChartRefs, appsapi.ChartReference{
+							Namespace: chart.Namespace,
+							Name:      chart.Name,
+						})
+					}
+				}
+			}
+		} else {
+			manifests, err = deployer.getManifestsBySelector(base, feed)
+			if err == nil {
+				allManifests = append(allManifests, manifests...)
+			}
+		}
+
+		if errors.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
+		}
+		if err != nil {
+			msg := fmt.Sprintf("failed to get matched objects %q for Base %s: %v", formatFeed(feed), klog.KObj(base), err)
+			klog.Error(msg)
+			deployer.recorder.Event(base, corev1.EventTypeWarning, "FailedRetrievingObjects", msg)
+			return err
+		}
+	}
+
+	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,
+			Labels: map[string]string{
+				known.ObjectCreatedByLabel: known.ClusternetHubName,
+				known.ConfigKindLabel:      baseKind.Kind,
+				known.ConfigNameLabel:      base.Name,
+				known.ConfigNamespaceLabel: base.Namespace,
+				known.ConfigUIDLabel:       string(base.UID),
+				known.ClusterIDLabel:       base.Labels[known.ClusterIDLabel],
+				known.ClusterNameLabel:     base.Labels[known.ClusterNameLabel],
+				// add subscription info
+				known.ConfigSubscriptionNameLabel:      base.Labels[known.ConfigSubscriptionNameLabel],
+				known.ConfigSubscriptionNamespaceLabel: base.Labels[known.ConfigSubscriptionNamespaceLabel],
+				known.ConfigSubscriptionUIDLabel:       base.Labels[known.ConfigSubscriptionUIDLabel],
+			},
+			Finalizers: []string{
+				known.AppFinalizer,
 			},
-			Spec: appsapi.DescriptionSpec{
-				Deployer: appsapi.DescriptionHelmDeployer,
-				Charts:   chartRefs,
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					APIVersion:         baseKind.Version,
+					Kind:               baseKind.Kind,
+					Name:               base.Name,
+					UID:                base.UID,
+					Controller:         utilpointer.BoolPtr(true),
+					BlockOwnerDeletion: utilpointer.BoolPtr(true),
+				},
 			},
+		},
+	}
+
+	// TODO: apply overrides
+
+	var allErrs []error
+	if len(allChartRefs) > 0 {
+		desc := descTemplate.DeepCopy()
+		desc.Name = fmt.Sprintf("%s-helm", base.Name)
+		desc.Spec.Deployer = appsapi.DescriptionHelmDeployer
+		desc.Spec.Charts = allChartRefs
+		err := deployer.syncDescriptions(base, desc)
+		if err != nil {
+			allErrs = append(allErrs, err)
+			msg := fmt.Sprintf("Failed to sync Description %s: %v", klog.KObj(desc), err)
+			klog.ErrorDepth(5, msg)
+			deployer.recorder.Event(base, corev1.EventTypeWarning, "FailedSyncingDescription", msg)
 		}
+	}
 
-		err := deployer.syncDescriptions(subs, description)
+	if len(allManifests) > 0 {
+		var rawObjects []runtime.RawExtension
+		for _, manifest := range allManifests {
+			rawObjects = append(rawObjects, manifest.Template)
+		}
+		desc := descTemplate.DeepCopy()
+		desc.Name = fmt.Sprintf("%s-generic", base.Name)
+		desc.Spec.Deployer = appsapi.DescriptionGenericDeployer
+		desc.Spec.Raw = rawObjects
+		err := deployer.syncDescriptions(base, desc)
 		if err != nil {
 			allErrs = append(allErrs, err)
-			msg := fmt.Sprintf("Failed to sync Description %s: %v", klog.KObj(description), err)
+			msg := fmt.Sprintf("Failed to sync Description %s: %v", klog.KObj(desc), err)
 			klog.ErrorDepth(5, msg)
-			deployer.recorder.Event(subs, corev1.EventTypeWarning, "DescriptionFailure", msg)
+			deployer.recorder.Event(base, corev1.EventTypeWarning, "FailedSyncingDescription", msg)
 		}
 	}
 
 	return utilerrors.NewAggregate(allErrs)
 }
 
-func (deployer *Deployer) syncDescriptions(subs *appsapi.Subscription, description *appsapi.Description) error {
+func (deployer *Deployer) syncDescriptions(base *appsapi.Base, description *appsapi.Description) error {
 	desc, err := deployer.descLister.Descriptions(description.Namespace).Get(description.Name)
 	if err == nil {
 		if desc.DeletionTimestamp != nil {
@@ -358,7 +590,7 @@ func (deployer *Deployer) syncDescriptions(subs *appsapi.Subscription, descripti
 			if err == nil {
 				msg := fmt.Sprintf("Description %s is updated successfully", klog.KObj(description))
 				klog.V(4).Info(msg)
-				deployer.recorder.Event(subs, corev1.EventTypeNormal, "DescriptionUpdated", msg)
+				deployer.recorder.Event(base, corev1.EventTypeNormal, "DescriptionUpdated", msg)
 			}
 			return err
 		}
@@ -370,7 +602,7 @@ func (deployer *Deployer) syncDescriptions(subs *appsapi.Subscription, descripti
 	if err == nil {
 		msg := fmt.Sprintf("Description %s is created successfully", klog.KObj(description))
 		klog.V(4).Info(msg)
-		deployer.recorder.Event(subs, corev1.EventTypeNormal, "DescriptionCreated", msg)
+		deployer.recorder.Event(base, corev1.EventTypeNormal, "DescriptionCreated", msg)
 	}
 	return err
 }
@@ -419,5 +651,13 @@ func (deployer *Deployer) handleDescription(desc *appsapi.Description) error {
 		}
 	}
 
+	// TODO: @dixudx: generic deployer
+
+	return nil
+}
+
+func (deployer *Deployer) handleManifest(manifest *appsapi.Manifest) error {
+	// TODO: @dixudx
+
 	return nil
 }
diff --git a/pkg/hub/deployer/utils.go b/pkg/hub/deployer/utils.go
new file mode 100644
index 0000000..e497636
--- /dev/null
+++ b/pkg/hub/deployer/utils.go
@@ -0,0 +1,75 @@
+/*
+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 deployer
+
+import (
+	"fmt"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	appsapi "github.com/clusternet/clusternet/pkg/apis/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, namespace string) (labels.Selector, error) {
+	var gv schema.GroupVersion
+	var err error
+	if len(feed.APIVersion) > 0 {
+		gv, err = schema.ParseGroupVersion(feed.APIVersion)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	labelSet := labels.Set{
+		known.ConfigGroupLabel:     gv.Group,
+		known.ConfigVersionLabel:   gv.Version,
+		known.ConfigKindLabel:      feed.Kind,
+		known.ConfigNamespaceLabel: namespace,
+	}
+	if len(feed.Namespace) > 0 {
+		labelSet[known.ConfigNamespaceLabel] = namespace
+	}
+	if len(feed.Name) > 0 {
+		labelSet[known.ConfigNameLabel] = feed.Name
+	}
+	selector := labels.SelectorFromSet(labelSet)
+
+	if feed.FeedSelector != nil {
+		feedSelector, err := metav1.LabelSelectorAsSelector(feed.FeedSelector)
+		if err != nil {
+			return nil, err
+		}
+
+		reqs, _ := feedSelector.Requirements()
+		for _, r := range reqs {
+			selector.Add(r)
+		}
+	}
+
+	return selector, nil
+}
diff --git a/pkg/registry/shadow/template/rest.go b/pkg/registry/shadow/template/rest.go
index 1341671..da19f12 100644
--- a/pkg/registry/shadow/template/rest.go
+++ b/pkg/registry/shadow/template/rest.go
@@ -52,9 +52,6 @@ const (
 	CoreGroupPrefix  = "api"
 	NamedGroupPrefix = "apis"
 
-	// cluster-scoped objects will be store into Manifest with ReservedNamespace
-	ReservedNamespace = "clusternet-reserved"
-
 	// default value for deleteCollectionWorkers
 	DefaultDeleteCollectionWorkers = 2
 )
@@ -98,7 +95,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
 	manifest := &appsapi.Manifest{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      r.generateNameForManifest(result.GetName()),
-			Namespace: ReservedNamespace,
+			Namespace: appsapi.ReservedNamespace,
 			Labels: map[string]string{
 				known.ConfigGroupLabel:     r.group,
 				known.ConfigVersionLabel:   r.version,
@@ -126,9 +123,9 @@ func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions)
 	var manifest *appsapi.Manifest
 	var err error
 	if len(options.ResourceVersion) == 0 {
-		manifest, err = r.clusternetInformerFactory.Apps().V1alpha1().Manifests().Lister().Manifests(ReservedNamespace).Get(r.generateNameForManifest(name))
+		manifest, err = r.clusternetInformerFactory.Apps().V1alpha1().Manifests().Lister().Manifests(appsapi.ReservedNamespace).Get(r.generateNameForManifest(name))
 	} else {
-		manifest, err = r.clusternetClient.AppsV1alpha1().Manifests(ReservedNamespace).Get(ctx, r.generateNameForManifest(name), *options)
+		manifest, err = r.clusternetClient.AppsV1alpha1().Manifests(appsapi.ReservedNamespace).Get(ctx, r.generateNameForManifest(name), *options)
 	}
 	if err != nil {
 		if errors.IsNotFound(err) {
@@ -155,7 +152,7 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
 		return nil, false, err
 	}
 
-	manifest, err := r.clusternetInformerFactory.Apps().V1alpha1().Manifests().Lister().Manifests(ReservedNamespace).Get(r.generateNameForManifest(name))
+	manifest, err := r.clusternetInformerFactory.Apps().V1alpha1().Manifests().Lister().Manifests(appsapi.ReservedNamespace).Get(r.generateNameForManifest(name))
 	if err != nil {
 		if errors.IsNotFound(err) {
 			return nil, false, errors.NewNotFound(schema.GroupResource{Group: r.group, Resource: r.name}, name)
@@ -182,7 +179,7 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
 
 	manifest.Template.Reset()
 	manifest.Template.Object = result
-	manifest, err = r.clusternetClient.AppsV1alpha1().Manifests(ReservedNamespace).Update(ctx, manifest, *options)
+	manifest, err = r.clusternetClient.AppsV1alpha1().Manifests(appsapi.ReservedNamespace).Update(ctx, manifest, *options)
 	if err != nil {
 		return nil, false, err
 	}
@@ -194,7 +191,7 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
 // Delete removes the item from storage.
 // options can be mutated by rest.BeforeDelete due to a graceful deletion strategy.
 func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
-	err := r.clusternetClient.AppsV1alpha1().Manifests(ReservedNamespace).Delete(ctx, r.generateNameForManifest(name), *options)
+	err := r.clusternetClient.AppsV1alpha1().Manifests(appsapi.ReservedNamespace).Delete(ctx, r.generateNameForManifest(name), *options)
 	if err != nil {
 		if errors.IsNotFound(err) {
 			err = errors.NewNotFound(schema.GroupResource{Group: r.group, Resource: r.name}, name)
@@ -298,7 +295,7 @@ func (r *REST) Watch(ctx context.Context, options *internalversion.ListOptions)
 	}
 
 	klog.V(5).Infof("%v", label)
-	watcher, err := r.clusternetClient.AppsV1alpha1().Manifests(ReservedNamespace).Watch(ctx, metav1.ListOptions{
+	watcher, err := r.clusternetClient.AppsV1alpha1().Manifests(appsapi.ReservedNamespace).Watch(ctx, metav1.ListOptions{
 		LabelSelector:        label.String(),
 		FieldSelector:        "", // explicitly set FieldSelector to an empty string
 		Watch:                options.Watch,
@@ -339,7 +336,7 @@ func (r *REST) List(ctx context.Context, options *internalversion.ListOptions) (
 		return nil, err
 	}
 
-	manifests, err := r.clusternetClient.AppsV1alpha1().Manifests(ReservedNamespace).List(ctx, metav1.ListOptions{
+	manifests, err := r.clusternetClient.AppsV1alpha1().Manifests(appsapi.ReservedNamespace).List(ctx, metav1.ListOptions{
 		LabelSelector:        label.String(),
 		FieldSelector:        "", // explicitly set FieldSelector to an empty string
 		Watch:                options.Watch,
@@ -470,9 +467,9 @@ func (r *REST) dryRunCreate(ctx context.Context, obj runtime.Object, createValid
 	u.SetLabels(labels)
 
 	if r.kind != "Namespace" && r.namespaced {
-		u.SetNamespace(ReservedNamespace)
+		u.SetNamespace(appsapi.ReservedNamespace)
 	}
-	dryRunNamespace := ReservedNamespace
+	dryRunNamespace := appsapi.ReservedNamespace
 	if r.kind == "Namespace" {
 		dryRunNamespace = ""
 	}


Test patch:
diff --git a/pkg/registry/shadow/template/transformer_test.go b/pkg/registry/shadow/template/transformer_test.go
index 717c7f2..493ae5f 100644
--- a/pkg/registry/shadow/template/transformer_test.go
+++ b/pkg/registry/shadow/template/transformer_test.go
@@ -49,7 +49,7 @@ func TestTransformManifest(t *testing.T) {
 						known.ConfigNameLabel:      "boo",
 						known.ConfigNamespaceLabel: "ns1",
 					},
-					Namespace:       ReservedNamespace,
+					Namespace:       appsapi.ReservedNamespace,
 					Name:            "bars-boo",
 					ResourceVersion: "1860247",
 					UID:             "13ff776c-1e91-4a84-b77d-6c35f3a52fed",