Repository: kubean-io/kubean
Pull request #1025 (2023-11-20 02:37:49)

Refactor controllers of Manifest and Localartifact
<!--  

Thanks for sending a pull request!  Here are some tips for you:

* make sure your commit is signed off

-->

#### What type of PR is this?
/kind enhancement

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind enhancement
/kind feature

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind design
/kind regression
-->

#### What this PR does / why we need it:
Refactor controllers of Manifest and Localartifact

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

#### Special notes for your reviewer:



Patch:
diff --git a/api/constants/constants.go b/api/constants/constants.go
index 87444c9e0..b489ad3ab 100644
--- a/api/constants/constants.go
+++ b/api/constants/constants.go
@@ -11,3 +11,7 @@ const Group_vars_yml = "group_vars.yml"
 const SSH_privatekey = "ssh-privatekey"
 
 const KubeanClusterHasCompleted = "hasCompleted"
+
+const KeySprayRelease = "kubean.io/sprayRelease"
+
+const KeySprayCommit = "kubean.io/sprayCommit"
diff --git a/pkg/controllers/infomanifest/controller.go b/pkg/controllers/infomanifest/controller.go
index 546cf13f0..b60a8e9b5 100644
--- a/pkg/controllers/infomanifest/controller.go
+++ b/pkg/controllers/infomanifest/controller.go
@@ -7,7 +7,6 @@ import (
 	"context"
 	"fmt"
 	"reflect"
-	"sort"
 	"time"
 
 	"github.com/kubean-io/kubean/pkg/util"
@@ -30,8 +29,6 @@ import (
 
 const Loop = time.Second * 30
 
-const OriginLabel = "origin"
-
 const LocalServiceConfigMap = "kubean-localservice"
 
 type Controller struct {
@@ -47,77 +44,6 @@ func (c *Controller) Start(ctx context.Context) error {
 	return nil
 }
 
-// FetchLatestInfoManifest , get infomanifest exclude the global-infomanifest.
-func (c *Controller) FetchLatestInfoManifest() (*manifestv1alpha1.Manifest, error) {
-	result, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().List(context.Background(), metav1.ListOptions{})
-	if err != nil {
-		return nil, err
-	}
-	items := make([]*manifestv1alpha1.Manifest, 0)
-	for i := range result.Items {
-		item := result.Items[i]
-		if item.Name == constants.InfoManifestGlobal {
-			continue
-		}
-		items = append(items, &item)
-	}
-	if len(items) == 0 {
-		return nil, fmt.Errorf("not found the latest InfoManifest")
-	}
-	sort.Slice(items, func(i, j int) bool {
-		return items[i].CreationTimestamp.After(items[j].CreationTimestamp.Time)
-	})
-	return items[0], nil
-}
-
-func NewGlobalInfoManifest(latestInfoManifest *manifestv1alpha1.Manifest) *manifestv1alpha1.Manifest {
-	return &manifestv1alpha1.Manifest{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "Manifest",
-			APIVersion: "kubean.io/v1alpha1",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:   constants.InfoManifestGlobal,
-			Labels: map[string]string{OriginLabel: latestInfoManifest.Name},
-		},
-		Spec: latestInfoManifest.Spec,
-	}
-}
-
-func (c *Controller) FetchGlobalInfoManifest() (*manifestv1alpha1.Manifest, error) {
-	global, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().Get(context.Background(), constants.InfoManifestGlobal, metav1.GetOptions{})
-	if err != nil {
-		return nil, err
-	}
-	return global, nil
-}
-
-func (c *Controller) EnsureGlobalInfoManifestBeingLatest(latestInfoManifest *manifestv1alpha1.Manifest) (*manifestv1alpha1.Manifest, error) {
-	currentGlobalInfoManifest, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().Get(context.Background(), constants.InfoManifestGlobal, metav1.GetOptions{})
-	if err != nil && apierrors.IsNotFound(err) {
-		// create global-infomanifest
-		global, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), NewGlobalInfoManifest(latestInfoManifest), metav1.CreateOptions{})
-		if err != nil {
-			return nil, err
-		}
-		return global, nil
-	}
-	if err != nil {
-		// other error
-		return nil, err
-	}
-	if currentGlobalInfoManifest.Labels == nil || len(currentGlobalInfoManifest.Labels) == 0 || currentGlobalInfoManifest.Labels[OriginLabel] != latestInfoManifest.Name {
-		currentGlobalInfoManifest.Labels = map[string]string{OriginLabel: latestInfoManifest.Name}
-		currentGlobalInfoManifest.Spec = latestInfoManifest.Spec
-		global, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().Update(context.Background(), currentGlobalInfoManifest, metav1.UpdateOptions{})
-		if err != nil {
-			return nil, err
-		}
-		return global, nil
-	}
-	return currentGlobalInfoManifest, nil
-}
-
 func (c *Controller) FetchLocalServiceCM(namespace string) (*corev1.ConfigMap, error) {
 	if localServiceCM, _ := c.ClientSet.CoreV1().ConfigMaps(namespace).Get(context.Background(), LocalServiceConfigMap, metav1.GetOptions{}); localServiceCM != nil {
 		return localServiceCM, nil
@@ -146,54 +72,62 @@ func (c *Controller) ParseConfigMapToLocalService(localServiceConfigMap *corev1.
 	return localService, nil
 }
 
-func (c *Controller) UpdateGlobalLocalService() {
+// UpdateLocalService sync the content of local-service configmap into spec.
+func (c *Controller) UpdateLocalService(manifests []manifestv1alpha1.Manifest) bool {
 	if c.IsOnlineENV() {
 		// if not airgap env , then do nothing and return
-		return
+		return false
 	}
 	localServiceConfigMap, err := c.FetchLocalServiceCM(util.GetCurrentNSOrDefault())
 	if err != nil {
-		klog.Warningf("ignoring %s", err.Error())
-		return
+		klog.Warningf("ignoring error: %s", err.Error())
+		return false
 	}
 	localService, err := c.ParseConfigMapToLocalService(localServiceConfigMap)
 	if err != nil {
-		klog.Warningf("ignoring %s", err.Error())
-		return
+		klog.Warningf("ignoring error: %s", err.Error())
+		return false
 	}
-	global, err := c.FetchGlobalInfoManifest()
 	if err != nil {
-		klog.Warningf("ignoring %s", err.Error())
-		return
-	}
-	if !reflect.DeepEqual(&global.Spec.LocalService, localService) {
-		global.Spec.LocalService = *localService
-		klog.Warningf("update the global InfoManifest LocalService")
-		_, err = c.InfoManifestClientSet.KubeanV1alpha1().Manifests().Update(context.Background(), global, metav1.UpdateOptions{})
-		if err != nil {
-			klog.Warningf("ignoring %s", err.Error())
+		klog.Warningf("ignoring error: %s", err.Error())
+		return false
+	}
+	for _, manifest := range manifests {
+		if !reflect.DeepEqual(&manifest.Spec.LocalService, localService) {
+			manifest.Spec.LocalService = *localService
+			klog.Infof("Update local-service for %s", manifest.Name)
+			_, err = c.InfoManifestClientSet.KubeanV1alpha1().Manifests().Update(context.Background(), &manifest, metav1.UpdateOptions{})
+			if err != nil {
+				klog.Warningf("ignoring error: %s", err.Error())
+			}
+			return true
 		}
 	}
+	return false
 }
 
-// UpdateLocalAvailableImage update image infos to global-infomanifest-cr.
-func (c *Controller) UpdateLocalAvailableImage() {
-	global, err := c.FetchGlobalInfoManifest()
-	if err != nil {
-		klog.Warningf("ignoring %s", err.Error())
-		return
-	}
-	imageRepo := "ghcr.m.daocloud.io"
-	if len(global.Spec.LocalService.GetGHCRImageRepo()) != 0 {
-		imageRepo = global.Spec.LocalService.GetGHCRImageRepo() // ghcr.io
-	}
-	newImageName := fmt.Sprintf("%s/kubean-io/spray-job:%s", imageRepo, global.Spec.KubeanVersion)
-	if global.Status.LocalAvailable.KubesprayImage != newImageName {
-		global.Status.LocalAvailable.KubesprayImage = newImageName
-		_, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().UpdateStatus(context.Background(), global, metav1.UpdateOptions{})
-		if err != nil {
-			klog.Warningf("ignoring %s", err.Error())
-			return
+// UpdateLocalAvailableImage update image infos into status.
+func (c *Controller) UpdateLocalAvailableImage(manifests []manifestv1alpha1.Manifest) {
+	for _, manifest := range manifests {
+		imageRepo := "ghcr.m.daocloud.io"
+		if len(manifest.Spec.LocalService.GetGHCRImageRepo()) != 0 {
+			imageRepo = manifest.Spec.LocalService.GetGHCRImageRepo() // ghcr.io
+		}
+		var newImageName string
+		sprayRelease := manifest.ObjectMeta.Annotations[constants.KeySprayRelease]
+		sprayCommit := manifest.ObjectMeta.Annotations[constants.KeySprayCommit]
+		if sprayRelease != "" && sprayCommit != "" {
+			newImageName = fmt.Sprintf("%s/kubean-io/spray-job:%s-%s", imageRepo, sprayRelease, sprayCommit)
+		} else {
+			newImageName = fmt.Sprintf("%s/kubean-io/spray-job:%s", imageRepo, manifest.Spec.KubeanVersion)
+		}
+		if manifest.Status.LocalAvailable.KubesprayImage != newImageName {
+			manifest.Status.LocalAvailable.KubesprayImage = newImageName
+			_, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().UpdateStatus(context.Background(), &manifest, metav1.UpdateOptions{})
+			if err != nil {
+				klog.Warningf("ignoring error: %s", err.Error())
+				return
+			}
 		}
 	}
 }
@@ -212,22 +146,19 @@ func (c *Controller) IsOnlineENV() bool {
 }
 
 func (c *Controller) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) {
-	if req.Name == constants.InfoManifestGlobal {
-		return controllerruntime.Result{}, nil
-	}
-	klog.Infof("InfoManifest Controller receive event %s", req.Name)
-	latestInfoManifest, err := c.FetchLatestInfoManifest()
+	manifestItems, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().List(ctx, metav1.ListOptions{})
 	if err != nil {
-		klog.Warningf("%s ", err.Error())
+		if apierrors.IsNotFound(err) {
+			return controllerruntime.Result{}, nil
+		}
+		klog.Error(err)
 		return controllerruntime.Result{RequeueAfter: Loop}, nil
 	}
-	_, err = c.EnsureGlobalInfoManifestBeingLatest(latestInfoManifest)
-	if err != nil {
-		klog.Warningf("%s ", err.Error())
+
+	if requeue := c.UpdateLocalService(manifestItems.Items); requeue {
 		return controllerruntime.Result{RequeueAfter: Loop}, nil
 	}
-	c.UpdateGlobalLocalService()
-	c.UpdateLocalAvailableImage()
+	c.UpdateLocalAvailableImage(manifestItems.Items)
 	return controllerruntime.Result{RequeueAfter: Loop}, nil
 }
 
diff --git a/pkg/controllers/offlineversion/controller.go b/pkg/controllers/offlineversion/controller.go
index ce0333000..845bf12b6 100644
--- a/pkg/controllers/offlineversion/controller.go
+++ b/pkg/controllers/offlineversion/controller.go
@@ -5,6 +5,7 @@ package offlineversion
 
 import (
 	"context"
+	"fmt"
 	"time"
 
 	localartifactsetv1alpha1 "github.com/kubean-io/kubean-api/apis/localartifactset/v1alpha1"
@@ -37,49 +38,78 @@ func (c *Controller) Start(ctx context.Context) error {
 	return nil
 }
 
-func (c *Controller) FetchGlobalKubeanClusterConfig() (*manifestv1alpha1.Manifest, error) {
-	infoManifest, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().Get(context.Background(), constants.InfoManifestGlobal, metav1.GetOptions{})
+// SelectManifestsBySprayRelease select manifests by using the label sprayRelease.
+func (c *Controller) SelectManifestsBySprayRelease(sprayRelease string) ([]*manifestv1alpha1.Manifest, error) {
+	selectedManifests, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().List(context.Background(), metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s", constants.KeySprayRelease, sprayRelease)})
 	if err != nil {
 		return nil, err
 	}
-	return infoManifest, nil
+	if len(selectedManifests.Items) == 0 {
+		return nil, nil
+	}
+	manifests := make([]*manifestv1alpha1.Manifest, 0, len(selectedManifests.Items))
+	for _, manifest := range selectedManifests.Items {
+		manifestsTmp := manifest
+		manifests = append(manifests, &manifestsTmp)
+	}
+	return manifests, nil
 }
 
-func (c *Controller) MergeOfflineVersionStatus(offlineVersion *localartifactsetv1alpha1.LocalArtifactSet, clusterConfig *manifestv1alpha1.Manifest) (bool, *manifestv1alpha1.Manifest) {
-	updated := false
-	for _, dockerInfo := range offlineVersion.Spec.Docker {
-		if clusterConfig.Status.LocalAvailable.MergeDockerInfo(dockerInfo.OS, dockerInfo.VersionRange) {
-			updated = true
+// MergeManifestsStatus merge the status of manifests which has same sprayRelease label of the localartifactset.
+func (c *Controller) MergeManifestsStatus(localartifactset *localartifactsetv1alpha1.LocalArtifactSet, manifests []*manifestv1alpha1.Manifest) ([]*manifestv1alpha1.Manifest, error) {
+	for _, manifest := range manifests {
+		updated := false
+		for _, dockerInfo := range localartifactset.Spec.Docker {
+			if manifest.Status.LocalAvailable.MergeDockerInfo(dockerInfo.OS, dockerInfo.VersionRange) {
+				updated = true
+			}
 		}
-	}
-	for _, softItem := range offlineVersion.Spec.Items {
-		if clusterConfig.Status.LocalAvailable.MergeSoftwareInfo(softItem.Name, softItem.VersionRange) {
-			updated = true
+		for _, softItem := range localartifactset.Spec.Items {
+			if manifest.Status.LocalAvailable.MergeSoftwareInfo(softItem.Name, softItem.VersionRange) {
+				updated = true
+			}
+		}
+		if !updated {
+			continue
+		}
+		klog.Infof("Update manifest status for %s since %s", manifest.Name, localartifactset.Name)
+		if _, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().UpdateStatus(context.Background(), manifest, metav1.UpdateOptions{}); err != nil {
+			klog.Error(err)
+			return nil, err
 		}
 	}
-	return updated, clusterConfig
+	return manifests, nil
 }
 
 func (c *Controller) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) {
-	offlineVersion := &localartifactsetv1alpha1.LocalArtifactSet{}
-	if err := c.Client.Get(context.Background(), req.NamespacedName, offlineVersion); err != nil {
+	localartifactset := &localartifactsetv1alpha1.LocalArtifactSet{}
+	if err := c.Client.Get(context.Background(), req.NamespacedName, localartifactset); err != nil {
 		if apierrors.IsNotFound(err) {
 			return controllerruntime.Result{}, nil
 		}
 		klog.Error(err)
 		return controllerruntime.Result{RequeueAfter: Loop}, nil
 	}
-	globalInfoManifest, err := c.FetchGlobalKubeanClusterConfig()
+
+	sprayRelease, ok := localartifactset.ObjectMeta.Labels[constants.KeySprayRelease]
+	if !ok {
+		klog.Infof("No label %s found in %s", constants.KeySprayRelease, localartifactset.Name)
+		return controllerruntime.Result{}, nil
+	}
+
+	manifests, err := c.SelectManifestsBySprayRelease(sprayRelease)
 	if err != nil {
-		klog.Errorf("Fetch %s , ignoring %s", constants.InfoManifestGlobal, err)
+		klog.Error(err)
 		return controllerruntime.Result{RequeueAfter: Loop}, nil
 	}
-	if needUpdate, newGlobalInfoManifest := c.MergeOfflineVersionStatus(offlineVersion, globalInfoManifest); needUpdate {
-		klog.Info("Update componentsVersion")
-		if _, err := c.InfoManifestClientSet.KubeanV1alpha1().Manifests().UpdateStatus(context.Background(), newGlobalInfoManifest, metav1.UpdateOptions{}); err != nil {
-			klog.Error(err)
-		}
+	if manifests == nil {
+		return controllerruntime.Result{RequeueAfter: Loop}, nil
 	}
+	if _, err := c.MergeManifestsStatus(localartifactset, manifests); err != nil {
+		klog.Error(err)
+		return controllerruntime.Result{RequeueAfter: Loop}, nil
+	}
+
 	return controllerruntime.Result{RequeueAfter: Loop}, nil // endless loop
 }
 
diff --git a/vendor/github.com/kubean-io/kubean-api/constants/constants.go b/vendor/github.com/kubean-io/kubean-api/constants/constants.go
index 87444c9e0..b489ad3ab 100644
--- a/vendor/github.com/kubean-io/kubean-api/constants/constants.go
+++ b/vendor/github.com/kubean-io/kubean-api/constants/constants.go
@@ -11,3 +11,7 @@ const Group_vars_yml = "group_vars.yml"
 const SSH_privatekey = "ssh-privatekey"
 
 const KubeanClusterHasCompleted = "hasCompleted"
+
+const KeySprayRelease = "kubean.io/sprayRelease"
+
+const KeySprayCommit = "kubean.io/sprayCommit"


Test patch:
diff --git a/pkg/controllers/infomanifest/controller_test.go b/pkg/controllers/infomanifest/controller_test.go
index 2ab6d52ba..d71ee5793 100644
--- a/pkg/controllers/infomanifest/controller_test.go
+++ b/pkg/controllers/infomanifest/controller_test.go
@@ -7,9 +7,7 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"os"
 	"reflect"
-	"strings"
 	"testing"
 	"time"
 
@@ -73,119 +71,6 @@ func removeReactorFromTestingTake(obj interface{ RESTClient() rest.Interface },
 	}
 }
 
-func Test_FetchLatestInfoManifest(t *testing.T) {
-	tests := []struct {
-		name string
-		args func() bool
-		want bool
-	}{
-		{
-			name: "return empty",
-			args: func() bool {
-				controller := &Controller{
-					Client:                newFakeClient(),
-					ClientSet:             clientsetfake.NewSimpleClientset(),
-					InfoManifestClientSet: manifestv1alpha1fake.NewSimpleClientset(),
-				}
-				_, err := controller.FetchLatestInfoManifest()
-				return err != nil && strings.Contains(err.Error(), "not found")
-			},
-			want: true,
-		},
-		{
-			name: "return empty exclude the global-infomanifest",
-			args: func() bool {
-				controller := &Controller{
-					Client:                newFakeClient(),
-					ClientSet:             clientsetfake.NewSimpleClientset(),
-					InfoManifestClientSet: manifestv1alpha1fake.NewSimpleClientset(),
-				}
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:              constants.InfoManifestGlobal,
-						Labels:            map[string]string{OriginLabel: ""},
-						CreationTimestamp: metav1.NewTime(time.Now()),
-					},
-					Spec: manifestv1alpha1.Spec{},
-				}, metav1.CreateOptions{})
-				_, err := controller.FetchLatestInfoManifest()
-				return err != nil && strings.Contains(err.Error(), "not found")
-			},
-			want: true,
-		},
-		{
-			name: "return the latest infomanifest",
-			args: func() bool {
-				controller := &Controller{
-					Client:                newFakeClient(),
-					ClientSet:             clientsetfake.NewSimpleClientset(),
-					InfoManifestClientSet: manifestv1alpha1fake.NewSimpleClientset(),
-				}
-				now := time.Now()
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:              "v1",
-						CreationTimestamp: metav1.NewTime(now.Add(time.Second * 100)),
-					},
-					Spec: manifestv1alpha1.Spec{},
-				}, metav1.CreateOptions{})
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:              "v2",
-						CreationTimestamp: metav1.NewTime(now.Add(time.Second * 10000)),
-					},
-					Spec: manifestv1alpha1.Spec{},
-				}, metav1.CreateOptions{})
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:              constants.InfoManifestGlobal,
-						CreationTimestamp: metav1.NewTime(now.Add(time.Second * 10000000)),
-					},
-					Spec: manifestv1alpha1.Spec{},
-				}, metav1.CreateOptions{})
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:              "v3",
-						CreationTimestamp: metav1.NewTime(now.Add(time.Second * 1000)),
-					},
-					Spec: manifestv1alpha1.Spec{},
-				}, metav1.CreateOptions{})
-				result, err := controller.FetchLatestInfoManifest()
-				return err == nil && result.Name == "v2"
-			},
-			want: true,
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			if test.args() != test.want {
-				t.Fatal()
-			}
-		})
-	}
-}
-
 func Test_ParseConfigMapToLocalService(t *testing.T) {
 	controller := &Controller{
 		Client:                newFakeClient(),
@@ -270,385 +155,6 @@ func Test_ParseConfigMapToLocalService(t *testing.T) {
 	}
 }
 
-func Test_EnsureGlobalInfoManifestBeingLatest(t *testing.T) {
-	controller := &Controller{
-		Client:                newFakeClient(),
-		ClientSet:             clientsetfake.NewSimpleClientset(),
-		InfoManifestClientSet: manifestv1alpha1fake.NewSimpleClientset(),
-	}
-	tests := []struct {
-		name               string
-		latestInfoManifest func() *manifestv1alpha1.Manifest
-		want               *manifestv1alpha1.Manifest
-	}{
-		{
-			name: "not existing global InfoManifest",
-			latestInfoManifest: func() *manifestv1alpha1.Manifest {
-				return &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "v1",
-					},
-					Spec: manifestv1alpha1.Spec{
-						Components: []*manifestv1alpha1.SoftwareInfo{{Name: "etcd1", VersionRange: []string{"1"}}},
-					},
-				}
-			},
-			want: &manifestv1alpha1.Manifest{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "Manifest",
-					APIVersion: "kubean.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:   constants.InfoManifestGlobal,
-					Labels: map[string]string{OriginLabel: "v1"},
-				},
-				Spec: manifestv1alpha1.Spec{
-					Components: []*manifestv1alpha1.SoftwareInfo{{Name: "etcd1", VersionRange: []string{"1"}}},
-				},
-			},
-		},
-		{
-			name: "already existing global InfoManifest",
-			latestInfoManifest: func() *manifestv1alpha1.Manifest {
-				return &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "v2",
-					},
-					Spec: manifestv1alpha1.Spec{
-						Components: []*manifestv1alpha1.SoftwareInfo{{Name: "etcd2", VersionRange: []string{"2"}}},
-					},
-				}
-			},
-			want: &manifestv1alpha1.Manifest{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "Manifest",
-					APIVersion: "kubean.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:   constants.InfoManifestGlobal,
-					Labels: map[string]string{OriginLabel: "v2"},
-				},
-				Spec: manifestv1alpha1.Spec{
-					Components: []*manifestv1alpha1.SoftwareInfo{{Name: "etcd2", VersionRange: []string{"2"}}},
-				},
-			},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			global, err := controller.EnsureGlobalInfoManifestBeingLatest(test.latestInfoManifest())
-			if err != nil {
-				t.Fatal(err)
-			}
-			if !reflect.DeepEqual(global, test.want) {
-				t.Fatal()
-			}
-		})
-	}
-}
-
-func Test_UpdateGlobalLocalService1(t *testing.T) {
-	controller := &Controller{
-		Client:                    newFakeClient(),
-		ClientSet:                 clientsetfake.NewSimpleClientset(),
-		InfoManifestClientSet:     manifestv1alpha1fake.NewSimpleClientset(),
-		LocalArtifactSetClientSet: localartifactsetv1alpha1fake.NewSimpleClientset(),
-	}
-	tests := []struct {
-		name string
-		arg  func()
-		want manifestv1alpha1.LocalService
-	}{
-		{
-			name: "global not have localService before",
-			arg: func() {
-				global := &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:   constants.InfoManifestGlobal,
-						Labels: map[string]string{OriginLabel: "v2"},
-					},
-					Spec: manifestv1alpha1.Spec{
-						Components: []*manifestv1alpha1.SoftwareInfo{{Name: "etcd2", VersionRange: []string{"2"}}},
-					},
-				}
-				configMap := &corev1.ConfigMap{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      LocalServiceConfigMap,
-						Namespace: "default",
-					},
-					Data: map[string]string{"localService": `
-      imageRepo: 
-        kubeImageRepo: "temp-registry.daocloud.io:5000/registry.k8s.io"
-        gcrImageRepo: "temp-registry.daocloud.io:5000/gcr.io"
-        githubImageRepo: "a"
-        dockerImageRepo: "b"
-        quayImageRepo: "c"
-      filesRepo: 'http://temp-registry.daocloud.io:9000'
-      yumRepos:
-        aRepo: 
-          - 'aaa1'
-          - 'aaa2'
-        bRepo: 
-          - 'bbb1'
-          - 'bbb2'
-      hostsMap: 
-        - domain: temp-registry.daocloud.io
-          address: 'a.b.c.d'
-`},
-				}
-				addLocalArtifactSet(controller)
-				controller.ClientSet.CoreV1().ConfigMaps("default").Create(context.Background(), configMap, metav1.CreateOptions{})
-				controller.Client.Create(context.Background(), global)
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), global, metav1.CreateOptions{})
-				controller.UpdateGlobalLocalService()
-			},
-			want: manifestv1alpha1.LocalService{
-				ImageRepo: map[manifestv1alpha1.ImageRepoType]string{
-					"kubeImageRepo":   "temp-registry.daocloud.io:5000/registry.k8s.io",
-					"gcrImageRepo":    "temp-registry.daocloud.io:5000/gcr.io",
-					"githubImageRepo": "a",
-					"dockerImageRepo": "b",
-					"quayImageRepo":   "c",
-				},
-				FilesRepo: "http://temp-registry.daocloud.io:9000",
-				YumRepos: map[string][]string{
-					"aRepo": {"aaa1", "aaa2"},
-					"bRepo": {"bbb1", "bbb2"},
-				},
-				HostsMap: []*manifestv1alpha1.HostsMap{
-					{
-						Domain:  "temp-registry.daocloud.io",
-						Address: "a.b.c.d",
-					},
-				},
-			},
-		},
-		{
-			name: "global already have localService and update it",
-			arg: func() {
-				global := &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:   constants.InfoManifestGlobal,
-						Labels: map[string]string{OriginLabel: "v2"},
-					},
-					Spec: manifestv1alpha1.Spec{
-						Components: []*manifestv1alpha1.SoftwareInfo{{Name: "etcd2", VersionRange: []string{"2"}}},
-					},
-				}
-				configMap := &corev1.ConfigMap{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      LocalServiceConfigMap,
-						Namespace: "default",
-					},
-					Data: map[string]string{"localService": `
-      imageRepo: 
-        kubeImageRepo: "temp-registry.daocloud.io:5000/registry.k8s.io"
-        gcrImageRepo: "temp-registry.daocloud.io:5000/gcr.io"
-        githubImageRepo: "a"
-        dockerImageRepo: "b"
-        quayImageRepo: "c"
-      filesRepo: 'http://temp-registry.daocloud.io:9000'
-      yumRepos:
-        aRepo: 
-          - 'aaa1'
-          - 'aaa2'
-        bRepo: 
-          - 'bbb1'
-          - 'bbb2'
-      hostsMap: 
-        - domain: temp-registry.daocloud.io
-          address: 'a.b.c.d1'
-`},
-				}
-				addLocalArtifactSet(controller)
-				controller.ClientSet.CoreV1().ConfigMaps("default").Update(context.Background(), configMap, metav1.UpdateOptions{})
-				controller.Client.Create(context.Background(), global)
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), global, metav1.CreateOptions{})
-				controller.UpdateGlobalLocalService()
-			},
-			want: manifestv1alpha1.LocalService{
-				ImageRepo: map[manifestv1alpha1.ImageRepoType]string{
-					"kubeImageRepo":   "temp-registry.daocloud.io:5000/registry.k8s.io",
-					"gcrImageRepo":    "temp-registry.daocloud.io:5000/gcr.io",
-					"githubImageRepo": "a",
-					"dockerImageRepo": "b",
-					"quayImageRepo":   "c",
-				},
-				FilesRepo: "http://temp-registry.daocloud.io:9000",
-				YumRepos: map[string][]string{
-					"aRepo": {"aaa1", "aaa2"},
-					"bRepo": {"bbb1", "bbb2"},
-				},
-				HostsMap: []*manifestv1alpha1.HostsMap{
-					{
-						Domain:  "temp-registry.daocloud.io",
-						Address: "a.b.c.d1",
-					},
-				},
-			},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			test.arg()
-			global, err := controller.FetchGlobalInfoManifest()
-			if err != nil {
-				t.Fatal(err)
-			}
-			if !reflect.DeepEqual(&global.Spec.LocalService, &test.want) {
-				t.Fatal()
-			}
-		})
-	}
-}
-
-func Test_UpdateGlobalLocalService2(t *testing.T) {
-	controller := &Controller{
-		Client:                    newFakeClient(),
-		ClientSet:                 clientsetfake.NewSimpleClientset(),
-		InfoManifestClientSet:     manifestv1alpha1fake.NewSimpleClientset(),
-		LocalArtifactSetClientSet: localartifactsetv1alpha1fake.NewSimpleClientset(),
-	}
-	tests := []struct {
-		name string
-		args func() bool
-		want bool
-	}{
-		{
-			name: "online ENV",
-			args: func() bool {
-				controller.UpdateGlobalLocalService()
-				return controller.IsOnlineENV()
-			},
-			want: true,
-		},
-		{
-			name: "LocalService not found in default namespace",
-			args: func() bool {
-				os.Setenv("POD_NAMESPACE", "")
-				controller.LocalArtifactSetClientSet.KubeanV1alpha1().LocalArtifactSets().Create(
-					context.Background(),
-					&localartifactsetv1alpha1.LocalArtifactSet{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "localservice-1",
-						},
-						Spec: localartifactsetv1alpha1.Spec{
-							Items: []*localartifactsetv1alpha1.SoftwareInfo{
-								{
-									Name:         "etcd-1",
-									VersionRange: []string{"1.1", "1.2"},
-								},
-							},
-						},
-					},
-					metav1.CreateOptions{})
-				controller.UpdateGlobalLocalService()
-				return controller.IsOnlineENV()
-			},
-			want: false,
-		},
-		{
-			name: "fetch localService but this cm is wrong format",
-			args: func() bool {
-				os.Setenv("POD_NAMESPACE", "")
-				controller.LocalArtifactSetClientSet.KubeanV1alpha1().LocalArtifactSets().Create(
-					context.Background(),
-					&localartifactsetv1alpha1.LocalArtifactSet{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "localservice-1",
-						},
-						Spec: localartifactsetv1alpha1.Spec{
-							Items: []*localartifactsetv1alpha1.SoftwareInfo{
-								{
-									Name:         "etcd-1",
-									VersionRange: []string{"1.1", "1.2"},
-								},
-							},
-						},
-					},
-					metav1.CreateOptions{})
-				controller.ClientSet.CoreV1().ConfigMaps("default").Create(context.Background(), &corev1.ConfigMap{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      LocalServiceConfigMap,
-						Namespace: "default",
-					},
-					Data: map[string]string{"localService": "1==1"},
-				}, metav1.CreateOptions{})
-				controller.UpdateGlobalLocalService()
-				return controller.IsOnlineENV()
-			},
-			want: false,
-		},
-		{
-			name: "global-manifest not-found",
-			args: func() bool {
-				os.Setenv("POD_NAMESPACE", "")
-				controller.LocalArtifactSetClientSet.KubeanV1alpha1().LocalArtifactSets().Create(
-					context.Background(),
-					&localartifactsetv1alpha1.LocalArtifactSet{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "localservice-1",
-						},
-						Spec: localartifactsetv1alpha1.Spec{
-							Items: []*localartifactsetv1alpha1.SoftwareInfo{
-								{
-									Name:         "etcd-1",
-									VersionRange: []string{"1.1", "1.2"},
-								},
-							},
-						},
-					},
-					metav1.CreateOptions{})
-				controller.ClientSet.CoreV1().ConfigMaps("default").Delete(context.Background(), LocalServiceConfigMap, metav1.DeleteOptions{})
-				time.Sleep(time.Second)
-				localServiceData := `
-      imageRepo: 
-        kubeImageRepo: "temp-registry.daocloud.io:5000/registry.k8s.io"
-        gcrImageRepo: "temp-registry.daocloud.io:5000/gcr.io"
-        githubImageRepo: "a"
-        dockerImageRepo: "b"
-`
-				controller.ClientSet.CoreV1().ConfigMaps("default").Create(context.Background(), &corev1.ConfigMap{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      LocalServiceConfigMap,
-						Namespace: "default",
-					},
-					Data: map[string]string{"localService": localServiceData},
-				}, metav1.CreateOptions{})
-				controller.UpdateGlobalLocalService()
-				return controller.IsOnlineENV()
-			},
-			want: false,
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			if test.args() != test.want {
-				t.Fatal()
-			}
-		})
-	}
-}
-
 func Test_Test_UpdateLocalAvailableImage2(t *testing.T) {
 	controller := &Controller{
 		Client:                newFakeClient(),
@@ -656,21 +162,22 @@ func Test_Test_UpdateLocalAvailableImage2(t *testing.T) {
 		InfoManifestClientSet: manifestv1alpha1fake.NewSimpleClientset(),
 	}
 
-	global := &manifestv1alpha1.Manifest{
+	manifestName := "manifest"
+
+	manifest := &manifestv1alpha1.Manifest{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Manifest",
 			APIVersion: "kubean.io/v1alpha1",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name:   constants.InfoManifestGlobal,
-			Labels: map[string]string{OriginLabel: "v2"},
+			Name: manifestName,
 		},
 		Spec: manifestv1alpha1.Spec{
 			KubeanVersion: "123",
 		},
 	}
-	controller.Client.Create(context.Background(), global)
-	controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), global, metav1.CreateOptions{})
+	controller.Client.Create(context.Background(), manifest)
+	controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), manifest, metav1.CreateOptions{})
 
 	tests := []struct {
 		name string
@@ -683,11 +190,15 @@ func Test_Test_UpdateLocalAvailableImage2(t *testing.T) {
 				fetchTestingFake(controller.InfoManifestClientSet.KubeanV1alpha1()).PrependReactor("get", "manifests", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
 					return true, nil, fmt.Errorf("this is error")
 				})
-				controller.UpdateLocalAvailableImage()
+				controller.UpdateLocalAvailableImage([]manifestv1alpha1.Manifest{*manifest})
 				removeReactorFromTestingTake(controller.InfoManifestClientSet.KubeanV1alpha1(), "get", "manifests")
 
-				global, _ := controller.FetchGlobalInfoManifest()
-				return global.Status.LocalAvailable.KubesprayImage
+				manifest := &manifestv1alpha1.Manifest{}
+				err := controller.Client.Get(context.Background(), types.NamespacedName{Name: manifestName}, manifest)
+				if err != nil {
+					t.Error(err)
+				}
+				return manifest.Status.LocalAvailable.KubesprayImage
 			},
 			want: "",
 		},
@@ -697,10 +208,14 @@ func Test_Test_UpdateLocalAvailableImage2(t *testing.T) {
 				fetchTestingFake(controller.InfoManifestClientSet.KubeanV1alpha1()).PrependReactor("update", "manifests/status", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
 					return true, nil, fmt.Errorf("this is error when updateStatus")
 				})
-				controller.UpdateLocalAvailableImage()
+				controller.UpdateLocalAvailableImage([]manifestv1alpha1.Manifest{*manifest})
 				removeReactorFromTestingTake(controller.InfoManifestClientSet.KubeanV1alpha1(), "update", "manifests/status")
-				global, _ := controller.FetchGlobalInfoManifest()
-				return global.Status.LocalAvailable.KubesprayImage
+				manifest := &manifestv1alpha1.Manifest{}
+				err := controller.Client.Get(context.Background(), types.NamespacedName{Name: manifestName}, manifest)
+				if err != nil {
+					t.Error(err)
+				}
+				return manifest.Status.LocalAvailable.KubesprayImage
 			},
 			want: "",
 		},
@@ -721,6 +236,15 @@ func Test_UpdateLocalAvailableImage(t *testing.T) {
 		ClientSet:             clientsetfake.NewSimpleClientset(),
 		InfoManifestClientSet: manifestv1alpha1fake.NewSimpleClientset(),
 	}
+	manifestName := "manifest1"
+	manifest, err := controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: manifestName,
+		},
+	}, metav1.CreateOptions{})
+	if err != nil {
+		t.Error(err)
+	}
 	tests := []struct {
 		name string
 		arg  func()
@@ -729,48 +253,29 @@ func Test_UpdateLocalAvailableImage(t *testing.T) {
 		{
 			name: "update local kubespray image with ghcr.io",
 			arg: func() {
-				global := &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:   constants.InfoManifestGlobal,
-						Labels: map[string]string{OriginLabel: "v2"},
-					},
-					Spec: manifestv1alpha1.Spec{
-						KubeanVersion: "123",
-					},
+				manifest.Spec = manifestv1alpha1.Spec{
+					KubeanVersion: "123",
 				}
-				controller.Client.Create(context.Background(), global)
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), global, metav1.CreateOptions{})
-				controller.UpdateLocalAvailableImage()
+				manifest, _ := controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Update(context.Background(), manifest, metav1.UpdateOptions{})
+				controller.UpdateLocalAvailableImage([]manifestv1alpha1.Manifest{*manifest})
 			},
 			want: "ghcr.m.daocloud.io/kubean-io/spray-job:123",
 		},
 		{
 			name: "update local kubespray image with local registry",
 			arg: func() {
-				global := &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name:   constants.InfoManifestGlobal,
-						Labels: map[string]string{OriginLabel: "v2"},
-					},
-					Spec: manifestv1alpha1.Spec{
-						LocalService: manifestv1alpha1.LocalService{ImageRepo: map[manifestv1alpha1.ImageRepoType]string{
-							"dockerImageRepo": "abc.io",
-							"githubImageRepo": "ghcr.io",
-						}},
-						KubeanVersion: "123456",
-					},
+				manifest.Spec = manifestv1alpha1.Spec{
+					LocalService: manifestv1alpha1.LocalService{ImageRepo: map[manifestv1alpha1.ImageRepoType]string{
+						"dockerImageRepo": "abc.io",
+						"githubImageRepo": "ghcr.io",
+					}},
+					KubeanVersion: "123456",
+				}
+				manifest, err := controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Update(context.Background(), manifest, metav1.UpdateOptions{})
+				if err != nil {
+					t.Error(err)
 				}
-				controller.Client.Update(context.Background(), global)
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Update(context.Background(), global, metav1.UpdateOptions{})
-				controller.UpdateLocalAvailableImage()
+				controller.UpdateLocalAvailableImage([]manifestv1alpha1.Manifest{*manifest})
 			},
 			want: "ghcr.io/kubean-io/spray-job:123456",
 		},
@@ -779,77 +284,12 @@ func Test_UpdateLocalAvailableImage(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			test.arg()
-			global, err := controller.FetchGlobalInfoManifest()
+			manifest, err := controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Get(context.Background(), manifestName, metav1.GetOptions{})
 			if err != nil {
 				t.Fatal(err)
 			}
-			if global.Status.LocalAvailable.KubesprayImage != test.want {
-				t.Fatal()
-			}
-		})
-	}
-}
-
-func TestNewGlobalInfoManifest(t *testing.T) {
-	type args struct {
-		latestInfoManifest *manifestv1alpha1.Manifest
-	}
-	tests := []struct {
-		name string
-		args args
-		want *manifestv1alpha1.Manifest
-	}{
-		{
-			name: "test new global info manifest normal",
-			args: args{
-				latestInfoManifest: &manifestv1alpha1.Manifest{
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "test",
-					},
-					Spec: manifestv1alpha1.Spec{
-						LocalService: manifestv1alpha1.LocalService{
-							ImageRepo: map[manifestv1alpha1.ImageRepoType]string{
-								"dockerImageRepo": "abc.io",
-							},
-							FilesRepo: "abc.io",
-							YumRepos: map[string][]string{
-								"a": {"aa"},
-							},
-						},
-						KubesprayVersion: "v2.0.0",
-						KubeanVersion:    "v1.0.0",
-					},
-				},
-			},
-			want: &manifestv1alpha1.Manifest{
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "Manifest",
-					APIVersion: "kubean.io/v1alpha1",
-				},
-				ObjectMeta: metav1.ObjectMeta{
-					Name:   constants.InfoManifestGlobal,
-					Labels: map[string]string{OriginLabel: "test"},
-				},
-				Spec: manifestv1alpha1.Spec{
-					LocalService: manifestv1alpha1.LocalService{
-						ImageRepo: map[manifestv1alpha1.ImageRepoType]string{
-							"dockerImageRepo": "abc.io",
-						},
-						FilesRepo: "abc.io",
-						YumRepos: map[string][]string{
-							"a": {"aa"},
-						},
-					},
-					KubesprayVersion: "v2.0.0",
-					KubeanVersion:    "v1.0.0",
-				},
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := NewGlobalInfoManifest(tt.args.latestInfoManifest); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("NewGlobalInfoManifest() = %v, want %v", got, tt.want)
+			if manifest.Status.LocalAvailable.KubesprayImage != test.want {
+				t.Fatalf("got %s, want %s", manifest.Status.LocalAvailable.KubesprayImage, test.want)
 			}
 		})
 	}
@@ -1014,26 +454,6 @@ func TestReconcile(t *testing.T) {
 			},
 			want: true,
 		},
-		{
-			name: "fetch infomanifest successfully",
-			args: func() bool {
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "Manifest",
-						APIVersion: "kubean.io/v1alpha1",
-					},
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "abc-infomanifest",
-					},
-					Spec: manifestv1alpha1.Spec{},
-				}, metav1.CreateOptions{})
-				controller.Reconcile(context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: "abc-infomanifest"}})
-				result, err := controller.Reconcile(context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: "abc-infomanifest"}})
-				globalManifest, _ := controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Get(context.Background(), constants.InfoManifestGlobal, metav1.GetOptions{})
-				return err == nil && result.RequeueAfter == Loop && globalManifest != nil
-			},
-			want: true,
-		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
diff --git a/pkg/controllers/offlineversion/controller_test.go b/pkg/controllers/offlineversion/controller_test.go
index 438788d4a..db2ab99b8 100644
--- a/pkg/controllers/offlineversion/controller_test.go
+++ b/pkg/controllers/offlineversion/controller_test.go
@@ -71,45 +71,66 @@ func removeReactorFromTestingTake(obj interface{ RESTClient() rest.Interface },
 	}
 }
 
-func TestMergeOfflineVersion(t *testing.T) {
+func TestMergeManifestsStatus(t *testing.T) {
 	controller := &Controller{
 		Client:                    newFakeClient(),
 		ClientSet:                 clientsetfake.NewSimpleClientset(),
 		LocalArtifactSetClientSet: localartifactsetv1alpha1fake.NewSimpleClientset(),
 		InfoManifestClientSet:     manifestv1alpha1fake.NewSimpleClientset(),
 	}
+
+	name := "manifest1"
+	controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+	}, metav1.CreateOptions{})
+
+	type args struct {
+		localartifactset *localartifactsetv1alpha1.LocalArtifactSet
+		manifests        []*manifestv1alpha1.Manifest
+	}
+	type wants struct {
+		manifests []*manifestv1alpha1.Manifest
+		err       bool
+	}
 	tests := []struct {
-		name string
-		args struct {
-			OfflineVersion    localartifactsetv1alpha1.LocalArtifactSet
-			ComponentsVersion manifestv1alpha1.Manifest
-		}
-		updated bool
+		name   string
+		args   args
+		result wants
 	}{
 		{
 			name: "nothing update",
-			args: struct {
-				OfflineVersion    localartifactsetv1alpha1.LocalArtifactSet
-				ComponentsVersion manifestv1alpha1.Manifest
-			}{
-				OfflineVersion: localartifactsetv1alpha1.LocalArtifactSet{
+			args: args{
+				localartifactset: &localartifactsetv1alpha1.LocalArtifactSet{
 					Spec: localartifactsetv1alpha1.Spec{},
 				},
-				ComponentsVersion: manifestv1alpha1.Manifest{
-					Status: manifestv1alpha1.Status{
-						LocalAvailable: manifestv1alpha1.LocalAvailable{},
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: name,
+						},
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{},
+						},
+					},
+				},
+			},
+			result: wants{
+				err: false,
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{},
+						},
 					},
 				},
 			},
-			updated: false,
 		},
 		{
 			name: "update software info",
-			args: struct {
-				OfflineVersion    localartifactsetv1alpha1.LocalArtifactSet
-				ComponentsVersion manifestv1alpha1.Manifest
-			}{
-				OfflineVersion: localartifactsetv1alpha1.LocalArtifactSet{
+			args: args{
+				localartifactset: &localartifactsetv1alpha1.LocalArtifactSet{
 					Spec: localartifactsetv1alpha1.Spec{
 						Items: []*localartifactsetv1alpha1.SoftwareInfo{
 							{
@@ -119,21 +140,39 @@ func TestMergeOfflineVersion(t *testing.T) {
 						},
 					},
 				},
-				ComponentsVersion: manifestv1alpha1.Manifest{
-					Status: manifestv1alpha1.Status{
-						LocalAvailable: manifestv1alpha1.LocalAvailable{},
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: name,
+						},
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{},
+						},
+					},
+				},
+			},
+			result: wants{
+				err: false,
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.1", "1.2"},
+									},
+								},
+							},
+						},
 					},
 				},
 			},
-			updated: true,
 		},
 		{
 			name: "update software info",
-			args: struct {
-				OfflineVersion    localartifactsetv1alpha1.LocalArtifactSet
-				ComponentsVersion manifestv1alpha1.Manifest
-			}{
-				OfflineVersion: localartifactsetv1alpha1.LocalArtifactSet{
+			args: args{
+				localartifactset: &localartifactsetv1alpha1.LocalArtifactSet{
 					Spec: localartifactsetv1alpha1.Spec{
 						Items: []*localartifactsetv1alpha1.SoftwareInfo{
 							{
@@ -143,28 +182,46 @@ func TestMergeOfflineVersion(t *testing.T) {
 						},
 					},
 				},
-				ComponentsVersion: manifestv1alpha1.Manifest{
-					Status: manifestv1alpha1.Status{
-						LocalAvailable: manifestv1alpha1.LocalAvailable{
-							Components: []*manifestv1alpha1.SoftwareInfoStatus{
-								{
-									Name:         "etcd-1",
-									VersionRange: []string{"1.2", "1.3"},
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: name,
+						},
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.2", "1.3"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			result: wants{
+				err: false,
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.2", "1.3", "1.1"},
+									},
 								},
 							},
 						},
 					},
 				},
 			},
-			updated: true,
 		},
 		{
 			name: "add software info but nothing updated",
-			args: struct {
-				OfflineVersion    localartifactsetv1alpha1.LocalArtifactSet
-				ComponentsVersion manifestv1alpha1.Manifest
-			}{
-				OfflineVersion: localartifactsetv1alpha1.LocalArtifactSet{
+			args: args{
+				localartifactset: &localartifactsetv1alpha1.LocalArtifactSet{
 					Spec: localartifactsetv1alpha1.Spec{
 						Items: []*localartifactsetv1alpha1.SoftwareInfo{
 							{
@@ -174,28 +231,46 @@ func TestMergeOfflineVersion(t *testing.T) {
 						},
 					},
 				},
-				ComponentsVersion: manifestv1alpha1.Manifest{
-					Status: manifestv1alpha1.Status{
-						LocalAvailable: manifestv1alpha1.LocalAvailable{
-							Components: []*manifestv1alpha1.SoftwareInfoStatus{
-								{
-									Name:         "etcd-1",
-									VersionRange: []string{"1.1", "1.2", "1.3"},
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: name,
+						},
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.1", "1.2", "1.3"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			result: wants{
+				err: false,
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.1", "1.2", "1.3"},
+									},
 								},
 							},
 						},
 					},
 				},
 			},
-			updated: false,
 		},
 		{
 			name: "update docker-ce info",
-			args: struct {
-				OfflineVersion    localartifactsetv1alpha1.LocalArtifactSet
-				ComponentsVersion manifestv1alpha1.Manifest
-			}{
-				OfflineVersion: localartifactsetv1alpha1.LocalArtifactSet{
+			args: args{
+				localartifactset: &localartifactsetv1alpha1.LocalArtifactSet{
 					Spec: localartifactsetv1alpha1.Spec{
 						Docker: []*localartifactsetv1alpha1.DockerInfo{
 							{
@@ -211,34 +286,62 @@ func TestMergeOfflineVersion(t *testing.T) {
 						},
 					},
 				},
-				ComponentsVersion: manifestv1alpha1.Manifest{
-					Status: manifestv1alpha1.Status{
-						LocalAvailable: manifestv1alpha1.LocalAvailable{
-							Docker: []*manifestv1alpha1.DockerInfoStatus{
-								{
-									OS:           "redhat-8",
-									VersionRange: []string{},
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: name,
+						},
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Docker: []*manifestv1alpha1.DockerInfoStatus{
+									{
+										OS:           "redhat-8",
+										VersionRange: []string{},
+									},
+								},
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.1", "1.2"},
+									},
 								},
 							},
-							Components: []*manifestv1alpha1.SoftwareInfoStatus{
-								{
-									Name:         "etcd-1",
-									VersionRange: []string{"1.1", "1.2"},
+						},
+					},
+				},
+			},
+			result: wants{
+				err: false,
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Docker: []*manifestv1alpha1.DockerInfoStatus{
+									{
+										OS:           "redhat-7",
+										VersionRange: []string{"20.01", "20.02"},
+									},
+									{
+										OS:           "redhat-8",
+										VersionRange: []string{},
+									},
+								},
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.1", "1.2"},
+									},
 								},
 							},
 						},
 					},
 				},
 			},
-			updated: true,
 		},
 		{
 			name: "nothing updated",
-			args: struct {
-				OfflineVersion    localartifactsetv1alpha1.LocalArtifactSet
-				ComponentsVersion manifestv1alpha1.Manifest
-			}{
-				OfflineVersion: localartifactsetv1alpha1.LocalArtifactSet{
+			args: args{
+				localartifactset: &localartifactsetv1alpha1.LocalArtifactSet{
 					Spec: localartifactsetv1alpha1.Spec{
 						Docker: []*localartifactsetv1alpha1.DockerInfo{
 							{
@@ -254,97 +357,172 @@ func TestMergeOfflineVersion(t *testing.T) {
 						},
 					},
 				},
-				ComponentsVersion: manifestv1alpha1.Manifest{
-					Status: manifestv1alpha1.Status{
-						LocalAvailable: manifestv1alpha1.LocalAvailable{
-							Docker: []*manifestv1alpha1.DockerInfoStatus{
-								{
-									OS:           "redhat-7",
-									VersionRange: []string{"20.02", "20.01"},
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: name,
+						},
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Docker: []*manifestv1alpha1.DockerInfoStatus{
+									{
+										OS:           "redhat-7",
+										VersionRange: []string{"20.02", "20.01"},
+									},
+									{
+										OS:           "redhat-8",
+										VersionRange: []string{"21.02", "21.01"},
+									},
 								},
-								{
-									OS:           "redhat-8",
-									VersionRange: []string{"21.02", "21.01"},
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.2", "1.1"},
+									},
 								},
 							},
-							Components: []*manifestv1alpha1.SoftwareInfoStatus{
-								{
-									Name:         "etcd-1",
-									VersionRange: []string{"1.2", "1.1"},
+						},
+					},
+				},
+			},
+			result: wants{
+				err: false,
+				manifests: []*manifestv1alpha1.Manifest{
+					{
+						Status: manifestv1alpha1.Status{
+							LocalAvailable: manifestv1alpha1.LocalAvailable{
+								Docker: []*manifestv1alpha1.DockerInfoStatus{
+									{
+										OS:           "redhat-7",
+										VersionRange: []string{"20.02", "20.01"},
+									},
+									{
+										OS:           "redhat-8",
+										VersionRange: []string{"21.02", "21.01"},
+									},
+								},
+								Components: []*manifestv1alpha1.SoftwareInfoStatus{
+									{
+										Name:         "etcd-1",
+										VersionRange: []string{"1.2", "1.1"},
+									},
 								},
 							},
 						},
 					},
 				},
 			},
-			updated: false,
 		},
 	}
 
+	statusEqual := func(m1, m2 []*manifestv1alpha1.Manifest) bool {
+		for i, m := range m1 {
+			if !reflect.DeepEqual(m.Status.LocalAvailable, m2[i].Status.LocalAvailable) {
+				return false
+			}
+		}
+		return true
+	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			if updated, _ := controller.MergeOfflineVersionStatus(&test.args.OfflineVersion, &test.args.ComponentsVersion); updated != test.updated {
+			if manifests, err := controller.MergeManifestsStatus(test.args.localartifactset, test.args.manifests); (test.result.err != (err != nil)) && statusEqual(manifests, test.result.manifests) {
 				t.Fatal()
 			}
 		})
 	}
 }
 
-func TestReconcile(t *testing.T) {
+func TestSelectManifestsBySprayRelease(t *testing.T) {
+	controller := &Controller{
+		Client:                    newFakeClient(),
+		ClientSet:                 clientsetfake.NewSimpleClientset(),
+		LocalArtifactSetClientSet: localartifactsetv1alpha1fake.NewSimpleClientset(),
+		InfoManifestClientSet:     manifestv1alpha1fake.NewSimpleClientset(),
+	}
+
+	type wants struct {
+		err           bool
+		manifestCount int
+	}
 	tests := []struct {
-		name string
-		args func() bool
-		want bool
+		name         string
+		arg          string
+		prerequisite func()
+		result       wants
 	}{
 		{
-			name: "merge successfully",
-			args: func() bool {
-				controller := &Controller{
-					Client:                    newFakeClient(),
-					ClientSet:                 clientsetfake.NewSimpleClientset(),
-					LocalArtifactSetClientSet: localartifactsetv1alpha1fake.NewSimpleClientset(),
-					InfoManifestClientSet:     manifestv1alpha1fake.NewSimpleClientset(),
-				}
-				offlineVersionData := localartifactsetv1alpha1.LocalArtifactSet{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "LocalArtifactSet",
-						APIVersion: "kubean.io/v1alpha1",
-					},
+			name:         "no manifest exist",
+			arg:          "1.1",
+			prerequisite: func() {},
+			result: wants{
+				err:           false,
+				manifestCount: 0,
+			},
+		},
+		{
+			name:         "manifest exist but no label",
+			arg:          "1.1",
+			prerequisite: func() {},
+			result: wants{
+				err:           false,
+				manifestCount: 0,
+			},
+		},
+		{
+			name: "manifest exist but has different label",
+			arg:  "1.1",
+			prerequisite: func() {
+				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "offlineversion-1",
-					},
-					Spec: localartifactsetv1alpha1.Spec{
-						Docker: []*localartifactsetv1alpha1.DockerInfo{
-							{
-								OS:           "redhat-7",
-								VersionRange: []string{"20.1", "20.2"},
-							},
-						},
-					},
-				}
-
-				globalComponentsVersion := manifestv1alpha1.Manifest{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "kubeanclusterconfig",
-						APIVersion: "kubean.io/v1alpha1",
+						Name:   "manifest1",
+						Labels: map[string]string{constants.KeySprayRelease: "1.2"},
 					},
+				}, metav1.CreateOptions{})
+			},
+			result: wants{
+				err:           false,
+				manifestCount: 0,
+			},
+		},
+		{
+			name: "manifest exist and has identical label",
+			arg:  "1.1",
+			prerequisite: func() {
+				_, err := controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &manifestv1alpha1.Manifest{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: constants.InfoManifestGlobal,
+						Name:   "manifest2",
+						Labels: map[string]string{constants.KeySprayRelease: "1.1"},
 					},
+				}, metav1.CreateOptions{})
+				if err != nil {
+					panic(err)
 				}
-
-				controller.Client.Create(context.Background(), &offlineVersionData)
-				controller.LocalArtifactSetClientSet.KubeanV1alpha1().LocalArtifactSets().Create(context.Background(), &offlineVersionData, metav1.CreateOptions{})
-				controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Create(context.Background(), &globalComponentsVersion, metav1.CreateOptions{})
-
-				result, err := controller.Reconcile(context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: offlineVersionData.Name}})
-				newGlobalComponentsVersion, _ := controller.InfoManifestClientSet.KubeanV1alpha1().Manifests().Get(context.Background(), constants.InfoManifestGlobal, metav1.GetOptions{})
-				return err == nil && result.RequeueAfter == Loop && len(newGlobalComponentsVersion.Status.LocalAvailable.Docker) == 1 && len(newGlobalComponentsVersion.Status.LocalAvailable.Docker[0].VersionRange) == 2
 			},
-			want: true,
+			result: wants{
+				err:           false,
+				manifestCount: 1,
+			},
 		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			test.prerequisite()
+			if manifests, err := controller.SelectManifestsBySprayRelease(test.arg); (test.result.err != (err != nil)) || !(len(manifests) == test.result.manifestCount) {
+				t.Fatal()
+			}
+		})
+	}
+}
+
+func TestReconcile(t *testing.T) {
+	tests := []struct {
+		name string
+		args func() bool
+		want bool
+	}{
 		{
-			name: "update global-manifest unsuccessfully",
+			name: "update manifest unsuccessfully",
 			args: func() bool {
 				controller := &Controller{
 					Client:                    newFakeClient(),
@@ -402,10 +580,6 @@ func TestReconcile(t *testing.T) {
 					InfoManifestClientSet:     manifestv1alpha1fake.NewSimpleClientset(),
 				}
 				offlineVersionData := localartifactsetv1alpha1.LocalArtifactSet{
-					TypeMeta: metav1.TypeMeta{
-						Kind:       "LocalArtifactSet",
-						APIVersion: "kubean.io/v1alpha1",
-					},
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "offlineversion-1",
 					},
@@ -423,12 +597,12 @@ func TestReconcile(t *testing.T) {
 				controller.LocalArtifactSetClientSet.KubeanV1alpha1().LocalArtifactSets().Create(context.Background(), &offlineVersionData, metav1.CreateOptions{})
 
 				result, _ := controller.Reconcile(context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: offlineVersionData.Name}})
-				return result.RequeueAfter == Loop
+				return result.RequeueAfter == 0 && result.Requeue == false
 			},
 			want: true,
 		},
 		{
-			name: "offlineVersion not found",
+			name: "localartifact not found",
 			args: func() bool {
 				controller := &Controller{
 					Client:                    newFakeClient(),
@@ -436,7 +610,7 @@ func TestReconcile(t *testing.T) {
 					LocalArtifactSetClientSet: localartifactsetv1alpha1fake.NewSimpleClientset(),
 					InfoManifestClientSet:     manifestv1alpha1fake.NewSimpleClientset(),
 				}
-				result, _ := controller.Reconcile(context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: "offlineversion-1"}})
+				result, _ := controller.Reconcile(context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: "localartifact-1"}})
 				return result.Requeue == false && result.RequeueAfter == 0
 			},
 			want: true,