From 9a3ad7ef84fae9fa1ab40407b457044b7756281c Mon Sep 17 00:00:00 2001 From: Jianbo Sun Date: Mon, 21 Mar 2022 16:33:29 +0800 Subject: [PATCH] Feat: add readOnly for velaux application which is synced from CR (#3479) Signed-off-by: Jianbo Sun --- docs/apidoc/swagger.json | 134 +++++++++++------- pkg/apiserver/model/application.go | 12 ++ pkg/apiserver/model/whole.go | 24 +++- pkg/apiserver/rest/apis/v1/types.go | 1 + pkg/apiserver/rest/usecase/application.go | 22 +-- pkg/apiserver/sync/cache.go | 23 ++-- pkg/apiserver/sync/cache_test.go | 92 +++++++++++++ pkg/apiserver/sync/convert.go | 15 +- pkg/apiserver/sync/cr2ux.go | 83 +++++------ pkg/apiserver/sync/cr2ux_test.go | 160 ++++++++++++++++++++++ pkg/apiserver/sync/store.go | 4 +- pkg/apiserver/sync/worker.go | 2 +- pkg/oam/labels.go | 3 - 13 files changed, 445 insertions(+), 130 deletions(-) create mode 100644 pkg/apiserver/sync/cache_test.go create mode 100644 pkg/apiserver/sync/cr2ux_test.go diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 86ee922dc..199f8fb5b 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -3233,9 +3233,9 @@ "parameters": [ { "enum": [ - "workflowstep", "component", - "trait" + "trait", + "workflowstep" ], "type": "string", "description": "query the definition type", @@ -4153,7 +4153,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.DetailUserResponse" + "$ref": "#/definitions/v1.UserBase" } }, "400": { @@ -4213,7 +4213,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.DetailUserResponse" + "$ref": "#/definitions/v1.UserBase" } }, "400": { @@ -5969,6 +5969,27 @@ } } }, + "utils.Condition": { + "required": [ + "jsonKey", + "value" + ], + "properties": { + "action": { + "type": "string" + }, + "jsonKey": { + "type": "string" + }, + "op": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/utils.Condition.value" + } + } + }, + "utils.Condition.value": {}, "utils.GroupOption": { "required": [ "label", @@ -6027,6 +6048,12 @@ "additionalParameter": { "$ref": "#/definitions/utils.UIParameter" }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.Condition" + } + }, "description": { "type": "string" }, @@ -6293,6 +6320,9 @@ "project": { "$ref": "#/definitions/v1.ProjectBase" }, + "readOnly": { + "type": "boolean" + }, "updateTime": { "type": "string", "format": "date-time" @@ -6329,11 +6359,11 @@ }, "v1.ApplicationDeployResponse": { "required": [ - "version", - "envName", "createTime", - "status", "note", + "version", + "status", + "envName", "triggerType" ], "properties": { @@ -7390,11 +7420,11 @@ }, "v1.DetailAddonResponse": { "required": [ - "description", - "name", - "icon", "invisible", + "name", + "description", "version", + "icon", "schema", "uiSchema", "definitions" @@ -7467,13 +7497,13 @@ }, "v1.DetailApplicationResponse": { "required": [ - "createTime", - "updateTime", "icon", "name", "alias", "project", "description", + "createTime", + "updateTime", "policies", "envBindings", "status", @@ -7521,6 +7551,9 @@ "project": { "$ref": "#/definitions/v1.ProjectBase" }, + "readOnly": { + "type": "boolean" + }, "resourceInfo": { "$ref": "#/definitions/v1.ApplicationResourceInfo" }, @@ -7535,20 +7568,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "status", - "reason", - "apiServerURL", - "dashboardURL", - "updateTime", "name", - "icon", - "labels", - "createTime", - "provider", - "kubeConfig", - "kubeConfigSecret", "alias", + "labels", + "dashboardURL", + "kubeConfigSecret", + "createTime", + "updateTime", + "reason", + "icon", + "provider", "description", + "status", + "apiServerURL", + "kubeConfig", "resourceInfo" ], "properties": { @@ -7606,14 +7639,14 @@ }, "v1.DetailComponentResponse": { "required": [ - "alias", - "appPrimaryKey", - "creator", - "createTime", "updateTime", - "main", + "creator", "name", + "alias", "type", + "main", + "createTime", + "appPrimaryKey", "definition" ], "properties": { @@ -7715,13 +7748,13 @@ }, "v1.DetailPolicyResponse": { "required": [ - "createTime", - "updateTime", - "name", "type", "description", "creator", - "properties" + "properties", + "createTime", + "updateTime", + "name" ], "properties": { "createTime": { @@ -7753,15 +7786,15 @@ "required": [ "note", "workflowName", - "appPrimaryKey", - "version", - "reason", - "triggerType", - "envName", "createTime", - "status", + "triggerType", "updateTime", - "deployUser" + "appPrimaryKey", + "deployUser", + "envName", + "version", + "status", + "reason" ], "properties": { "appPrimaryKey": { @@ -7815,8 +7848,8 @@ }, "v1.DetailTargetResponse": { "required": [ - "createTime", "updateTime", + "createTime", "name" ], "properties": { @@ -7854,11 +7887,11 @@ }, "v1.DetailUserResponse": { "required": [ + "disabled", + "createTime", "lastLoginTime", "name", "email", - "disabled", - "createTime", "projects" ], "properties": { @@ -7892,12 +7925,12 @@ }, "v1.DetailWorkflowRecordResponse": { "required": [ + "status", "name", "namespace", "workflowName", "workflowAlias", "applicationRevision", - "status", "deployTime", "deployUser", "note", @@ -7949,12 +7982,12 @@ }, "v1.DetailWorkflowResponse": { "required": [ - "alias", "description", "default", "envName", "createTime", "name", + "alias", "enable", "updateTime" ], @@ -8589,7 +8622,7 @@ "required": [ "name", "alias", - "userRole" + "userRoles" ], "properties": { "alias": { @@ -8598,8 +8631,11 @@ "name": { "type": "string" }, - "userRole": { - "type": "string" + "userRoles": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -8626,11 +8662,11 @@ }, "v1.SystemInfoResponse": { "required": [ + "updateTime", + "installID", "enableCollection", "loginType", "createTime", - "updateTime", - "installID", "systemVersion" ], "properties": { diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 340513e6a..67812a47d 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -81,6 +81,18 @@ func (a *Application) GetAppNameForSynced() string { return strings.TrimSuffix(a.Name, "-"+namespace) } +// IsSynced answer if the app is synced one +func (a *Application) IsSynced() bool { + if a.Labels == nil { + return false + } + sot := a.Labels[LabelSourceOfTruth] + if sot == FromCR || sot == FromInner { + return true + } + return false +} + // ClusterSelector cluster selector type ClusterSelector struct { Name string `json:"name"` diff --git a/pkg/apiserver/model/whole.go b/pkg/apiserver/model/whole.go index 03044673f..985c5c9f9 100644 --- a/pkg/apiserver/model/whole.go +++ b/pkg/apiserver/model/whole.go @@ -24,17 +24,17 @@ const ( AutoGenProj = "Automatically generated by sync mechanism." // AutoGenEnvNamePrefix describes the common prefix for auto-generated env - AutoGenEnvNamePrefix = "synced-" + AutoGenEnvNamePrefix = "syc-" // AutoGenComp describes the creator of component that is auto-generated - AutoGenComp = "synced-comp" + AutoGenComp = "syc-comp" // AutoGenPolicy describes the creator of policy that is auto-generated - AutoGenPolicy = "synced-policy" + AutoGenPolicy = "syc-policy" // AutoGenRefPolicy describes the creator of policy that is auto-generated, this differs from AutoGenPolicy as the policy is referenced ones - AutoGenRefPolicy = "synced-ref-policy" + AutoGenRefPolicy = "syc-ref-policy" // AutoGenWorkflowNamePrefix describes the common prefix for auto-generated workflow - AutoGenWorkflowNamePrefix = "synced-" + AutoGenWorkflowNamePrefix = "syc-" // AutoGenTargetNamePrefix describes the common prefix for auto-generated target - AutoGenTargetNamePrefix = "synced-" + AutoGenTargetNamePrefix = "syc-" // LabelSyncGeneration describes the generation synced from LabelSyncGeneration = "ux.oam.dev/synced-generation" @@ -42,6 +42,18 @@ const ( LabelSyncNamespace = "ux.oam.dev/from-namespace" ) +const ( + // LabelSourceOfTruth describes the source of this app + LabelSourceOfTruth = "app.oam.dev/source-of-truth" + + // FromCR means the data source of truth is from k8s CR + FromCR = "from-k8s-resource" + // FromUX means the data source of truth is from velaux data store + FromUX = "from-velaux" + // FromInner means the data source of truth is from KubeVela inner usage, such as addon or configuration that don't want to be synced + FromInner = "from-inner-system" +) + // DataStoreApp is a memory struct that describes the model of an application in datastore type DataStoreApp struct { AppMeta *Application diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index f9cf3ff93..04986baaa 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -311,6 +311,7 @@ type ApplicationBase struct { UpdateTime time.Time `json:"updateTime"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` } // AppCompareResponse application compare result diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index a14105464..dc2b6ba2e 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -208,7 +208,7 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptio } var list []*apisv1.ApplicationBase for _, app := range apps { - appBase := c.converAppModelToBase(ctx, app) + appBase := c.convertAppModelToBase(ctx, app) list = append(list, appBase) } sort.Slice(list, func(i, j int) bool { @@ -233,7 +233,7 @@ func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName str // DetailApplication detail application info func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) { - base := c.converAppModelToBase(ctx, app) + base := c.convertAppModelToBase(ctx, app) policys, err := c.queryApplicationPolicies(ctx, app) if err != nil { return nil, err @@ -375,7 +375,7 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, err } // render app base info. - base := c.converAppModelToBase(ctx, &application) + base := c.convertAppModelToBase(ctx, &application) return base, nil } @@ -503,7 +503,7 @@ func (c *applicationUsecaseImpl) UpdateApplication(ctx context.Context, app *mod if err := c.ds.Put(ctx, app); err != nil { return nil, err } - return c.converAppModelToBase(ctx, app), nil + return c.convertAppModelToBase(ctx, app), nil } // ListRecords list application record @@ -736,7 +736,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat } return &apisv1.ApplicationDeployResponse{ - ApplicationRevisionBase: c.converRevisionModelToBase(appRevision), + ApplicationRevisionBase: c.convertRevisionModelToBase(appRevision), }, nil } @@ -883,7 +883,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return app, nil } -func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app *model.Application) *apisv1.ApplicationBase { +func (c *applicationUsecaseImpl) convertAppModelToBase(ctx context.Context, app *model.Application) *apisv1.ApplicationBase { appBase := &apisv1.ApplicationBase{ Name: app.Name, Alias: app.Alias, @@ -893,6 +893,10 @@ func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app * Icon: app.Icon, Labels: app.Labels, } + if app.IsSynced() { + appBase.ReadOnly = true + } + project, err := c.projectUsecase.GetProject(ctx, app.Project) if err != nil { log.Logger.Errorf("query project info failure %s", err.Error()) @@ -903,7 +907,7 @@ func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app * return appBase } -func (c *applicationUsecaseImpl) converRevisionModelToBase(revision *model.ApplicationRevision) apisv1.ApplicationRevisionBase { +func (c *applicationUsecaseImpl) convertRevisionModelToBase(revision *model.ApplicationRevision) apisv1.ApplicationRevisionBase { return apisv1.ApplicationRevisionBase{ Version: revision.Version, Status: revision.Status, @@ -925,7 +929,7 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod if err != nil { return err } - if len(crs.Items) > 0 { + if len(crs.Items) > 0 || app.IsSynced() { return bcode.ErrApplicationRefusedDelete } // query all components to deleted @@ -1337,7 +1341,7 @@ func (c *applicationUsecaseImpl) ListRevisions(ctx context.Context, appName, env for _, raw := range revisions { r, ok := raw.(*model.ApplicationRevision) if ok { - resp.Revisions = append(resp.Revisions, c.converRevisionModelToBase(r)) + resp.Revisions = append(resp.Revisions, c.convertRevisionModelToBase(r)) } } count, err := c.ds.Count(ctx, &revision, nil) diff --git a/pkg/apiserver/sync/cache.go b/pkg/apiserver/sync/cache.go index 115605029..3e440c844 100644 --- a/pkg/apiserver/sync/cache.go +++ b/pkg/apiserver/sync/cache.go @@ -34,8 +34,8 @@ type cached struct { targets int64 } -// InitCache will initialize the cache -func (c *CR2UX) InitCache(ctx context.Context) error { +// initCache will initialize the cache +func (c *CR2UX) initCache(ctx context.Context) error { appsRaw, err := c.ds.List(ctx, &model.Application{}, &datastore.ListOptions{}) if err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { @@ -60,7 +60,7 @@ func (c *CR2UX) InitCache(ctx context.Context) error { generation, _ := strconv.ParseInt(gen, 10, 64) // we should check targets if we synced from app status - c.updateCache(key, generation, 0) + c.syncCache(key, generation, 0) } return nil } @@ -72,31 +72,30 @@ func (c *CR2UX) shouldSync(ctx context.Context, targetApp *v1beta1.Application, cd := cachedData.(*cached) // TODO(wonderflow): we should check targets if we sync that, it can avoid missing the status changed for targets updated in multi-cluster deploy, e.g. resumed suspend case. - + if del { + c.cache.Delete(key) + return false + } if cd.generation == targetApp.Generation && !del { logrus.Infof("app %s/%s with generation(%v) hasn't updated, ignore the sync event..", targetApp.Name, targetApp.Namespace, targetApp.Generation) return false } - if del { - c.cache.Delete(key) - } } - sot := CheckSoTFromCR(targetApp) - // This is a double check to make sure the app not be converted and un-deployed - sot = CheckSoTFromAppMeta(ctx, c.ds, targetApp.Name, targetApp.Namespace, sot) + sot := c.CheckSoTFromAppMeta(ctx, targetApp.Name, targetApp.Namespace, CheckSoTFromCR(targetApp)) switch sot { - case FromUX, FromInner: + case model.FromUX, model.FromInner: // we don't sync if the application is not created from CR return false + case model.FromCR: default: } return true } -func (c *CR2UX) updateCache(key string, generation, targets int64) { +func (c *CR2UX) syncCache(key string, generation, targets int64) { // update cache c.cache.Store(key, &cached{generation: generation, targets: targets}) } diff --git a/pkg/apiserver/sync/cache_test.go b/pkg/apiserver/sync/cache_test.go new file mode 100644 index 000000000..12f7dac21 --- /dev/null +++ b/pkg/apiserver/sync/cache_test.go @@ -0,0 +1,92 @@ +/* + Copyright 2022 The KubeVela 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 sync + +import ( + "context" + "sync" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +var _ = Describe("Test Cache", func() { + BeforeEach(func() { + }) + + It("Test cache update and delete", func() { + + By("Preparing database") + dbNamespace := "cache-db-ns1-test" + + ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: dbNamespace}) + Expect(ds).ToNot(BeNil()) + Expect(err).Should(BeNil()) + var ns = corev1.Namespace{} + ns.Name = dbNamespace + err = k8sClient.Create(context.TODO(), &ns) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + cr2ux := CR2UX{ds: ds, cli: k8sClient, cache: sync.Map{}} + + ctx := context.Background() + Expect(ds.Add(ctx, &model.Application{Name: "app1"})).Should(BeNil()) + Expect(ds.Add(ctx, &model.Application{Name: "app2", Labels: map[string]string{ + model.LabelSyncGeneration: "1", + model.LabelSyncNamespace: "app2-ns", + }})).Should(BeNil()) + + Expect(ds.Add(ctx, &model.Application{Name: "app3", Labels: map[string]string{ + model.LabelSyncGeneration: "1", + model.LabelSyncNamespace: "app3-ns", + model.LabelSourceOfTruth: model.FromUX, + }})).Should(BeNil()) + + Expect(cr2ux.initCache(ctx)).Should(BeNil()) + app1 := &v1beta1.Application{} + app1.Name = "app1" + app1.Namespace = "app1-ns" + app1.Generation = 1 + Expect(cr2ux.shouldSync(ctx, app1, false)).Should(BeEquivalentTo(true)) + + app2 := &v1beta1.Application{} + app2.Name = "app2" + app2.Namespace = "app2-ns" + app2.Generation = 1 + + Expect(cr2ux.shouldSync(ctx, app2, false)).Should(BeEquivalentTo(false)) + + app3 := &v1beta1.Application{} + app3.Name = "app3" + app3.Namespace = "app3-ns" + app3.Generation = 3 + + Expect(cr2ux.shouldSync(ctx, app3, false)).Should(BeEquivalentTo(false)) + + cr2ux.syncCache(formatAppComposedName(app1.Name, app1.Namespace), 1, 0) + Expect(cr2ux.shouldSync(ctx, app1, false)).Should(BeEquivalentTo(false)) + + }) + +}) diff --git a/pkg/apiserver/sync/convert.go b/pkg/apiserver/sync/convert.go index 98c7bda18..101835c15 100644 --- a/pkg/apiserver/sync/convert.go +++ b/pkg/apiserver/sync/convert.go @@ -89,8 +89,9 @@ func ConvertFromCRWorkflow(ctx context.Context, cli client.Client, appPrimaryKey // every namespace has a synced env EnvName: model.AutoGenEnvNamePrefix + app.Namespace, // every application has a synced workflow - Name: model.AutoGenWorkflowNamePrefix + app.Name, - Alias: "Synced", + Name: model.AutoGenWorkflowNamePrefix + appPrimaryKey, + Alias: model.AutoGenWorkflowNamePrefix + app.Name, + Description: model.AutoGenDesc, } if app.Spec.Workflow == nil { return dataWf, nil, nil @@ -169,6 +170,7 @@ func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1. Labels: map[string]string{ model.LabelSyncNamespace: targetApp.Namespace, model.LabelSyncGeneration: strconv.FormatInt(targetApp.Generation, 10), + model.LabelSourceOfTruth: model.FromCR, }, } @@ -180,7 +182,7 @@ func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1. Namespace: targetApp.Namespace, Description: model.AutoGenDesc, Project: project, - Alias: "Synced", + Alias: model.AutoGenEnvNamePrefix + targetApp.Namespace, }, Eb: &model.EnvBinding{ AppPrimaryKey: appMeta.PrimaryKey(), @@ -206,12 +208,19 @@ func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1. dsApp.Workflow = &wf // 4. convert policy, some policies are references in workflow step, we need to sync all the outside policy to make that work + var innerPlc = make(map[string]struct{}) + for _, plc := range targetApp.Spec.Policies { + innerPlc[plc.Name] = struct{}{} + } outsidePLC, err := step.LoadExternalPoliciesForWorkflow(ctx, cli, targetApp.Namespace, steps, targetApp.Spec.Policies) if err != nil { return nil, err } for _, plc := range outsidePLC { plcModel, err := ConvertFromCRPolicy(appMeta.PrimaryKey(), plc, model.AutoGenRefPolicy) + if _, ok := innerPlc[plc.Name]; ok { + plcModel.Creator = model.AutoGenPolicy + } if err != nil { return nil, err } diff --git a/pkg/apiserver/sync/cr2ux.go b/pkg/apiserver/sync/cr2ux.go index d50be1244..cb014fba3 100644 --- a/pkg/apiserver/sync/cr2ux.go +++ b/pkg/apiserver/sync/cr2ux.go @@ -29,46 +29,52 @@ import ( "github.com/oam-dev/kubevela/pkg/oam" ) -const ( - // FromCR means the data source of truth is from k8s CR - FromCR = "from-CR" - // FromUX means the data source of truth is from velaux data store - FromUX = "from-UX" - // FromInner means the data source of truth is from KubeVela inner usage, such as addon or configuration that don't want to be synced - FromInner = "from-inner" - - // SoT means the source of Truth from - SoT = "SourceOfTruth" -) - // CheckSoTFromCR will check the source of truth of the application func CheckSoTFromCR(targetApp *v1beta1.Application) string { - - if _, innerUse := targetApp.Annotations[oam.AnnotationSOTFromInner]; innerUse { - return FromInner + if sot := targetApp.Annotations[model.LabelSourceOfTruth]; sot != "" { + return sot } + // if no LabelSourceOfTruth label, it means the app is existing ones, check the existing labels and annotations if _, appName := targetApp.Annotations[oam.AnnotationAppName]; appName { - return FromUX + return model.FromUX } - return FromCR + // no labels mean it's created by K8s resources. + return model.FromCR } // CheckSoTFromAppMeta will check the source of truth marked in datastore -func CheckSoTFromAppMeta(ctx context.Context, ds datastore.DataStore, appName, namespace string, sotFromCR string) string { +func (c *CR2UX) CheckSoTFromAppMeta(ctx context.Context, appName, namespace string, sotFromCR string) string { - app := &model.Application{Name: formatAppComposedName(appName, namespace)} - err := ds.Get(ctx, app) + app, _, err := c.getApp(ctx, appName, namespace) if err != nil { - app = &model.Application{Name: appName} - err = ds.Get(ctx, app) - if err != nil { - return sotFromCR - } - } - if app.Labels == nil || app.Labels[SoT] == "" { return sotFromCR } - return app.Labels[SoT] + if app.Labels == nil || app.Labels[model.LabelSourceOfTruth] == "" { + return sotFromCR + } + return app.Labels[model.LabelSourceOfTruth] +} + +// getApp will return the app and appname if exists +func (c *CR2UX) getApp(ctx context.Context, name, namespace string) (*model.Application, string, error) { + alreadyCreated := &model.Application{Name: formatAppComposedName(name, namespace)} + err1 := c.ds.Get(ctx, alreadyCreated) + if err1 == nil { + return alreadyCreated, alreadyCreated.Name, nil + } + + // check if it's created the first in database + existApp := &model.Application{Name: name} + err2 := c.ds.Get(ctx, existApp) + if err2 == nil { + en := existApp.Labels[model.LabelSyncNamespace] + // it means the namespace/app is not created yet, the appname is occupied by app from other namespace + if en != namespace { + return nil, formatAppComposedName(name, namespace), err1 + } + return existApp, name, nil + } + return nil, name, err2 } // CR2UX provides the Add/Update/Delete method @@ -84,28 +90,13 @@ func formatAppComposedName(name, namespace string) string { // we need to prevent the case that one app is deleted ant it's name is pure appName, then other app with namespace suffix will be mixed func (c *CR2UX) getAppMetaName(ctx context.Context, name, namespace string) string { - alreadyCreated := &model.Application{Name: formatAppComposedName(name, namespace)} - err := c.ds.Get(ctx, alreadyCreated) - if err == nil { - return formatAppComposedName(name, namespace) - } - - // check if it's created the first in database - existApp := &model.Application{Name: name} - err = c.ds.Get(ctx, existApp) - if err == nil { - en := existApp.Labels[model.LabelSyncNamespace] - if en != namespace { - return formatAppComposedName(name, namespace) - } - } - return name + _, appName, _ := c.getApp(ctx, name, namespace) + return appName } // AddOrUpdate will sync application CR to storage of VelaUX automatically func (c *CR2UX) AddOrUpdate(ctx context.Context, targetApp *v1beta1.Application) error { ds := c.ds - if !c.shouldSync(ctx, targetApp, false) { return nil } @@ -155,7 +146,7 @@ func (c *CR2UX) AddOrUpdate(ctx context.Context, targetApp *v1beta1.Application) } // update cache - c.updateCache(dsApp.AppMeta.PrimaryKey(), targetApp.Generation, int64(len(dsApp.Targets))) + c.syncCache(dsApp.AppMeta.PrimaryKey(), targetApp.Generation, int64(len(dsApp.Targets))) return nil } diff --git a/pkg/apiserver/sync/cr2ux_test.go b/pkg/apiserver/sync/cr2ux_test.go new file mode 100644 index 000000000..7f14e54e7 --- /dev/null +++ b/pkg/apiserver/sync/cr2ux_test.go @@ -0,0 +1,160 @@ +/* + Copyright 2022 The KubeVela 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 sync + +import ( + "context" + "fmt" + "sync" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + "github.com/oam-dev/kubevela/pkg/oam/util" + common2 "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var _ = Describe("Test CR convert to ux", func() { + BeforeEach(func() { + }) + + It("Test get app with occupied app", func() { + + By("Preparing database") + dbNamespace := "get-app-db-ns1-test" + + apName1 := "example" + appNS1 := "get-app-test-ns1" + appNS2 := "get-app-test-ns2" + ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: dbNamespace}) + Expect(ds).ToNot(BeNil()) + Expect(err).Should(BeNil()) + var ns = corev1.Namespace{} + ns.Name = dbNamespace + err = k8sClient.Create(context.TODO(), &ns) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + ns.Name = appNS1 + ns.ResourceVersion = "" + err = k8sClient.Create(context.TODO(), &ns) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + By("no app created, test the name") + + cr2ux := CR2UX{ds: ds, cli: k8sClient, cache: sync.Map{}} + gotApp, gotAppName, err := cr2ux.getApp(context.Background(), apName1, appNS1) + Expect(gotAppName).Should(BeEquivalentTo(apName1)) + Expect(gotApp).Should(BeNil()) + Expect(err).ShouldNot(BeNil()) + + By("create test app2 and check the syncing results") + app2 := &v1beta1.Application{} + Expect(common2.ReadYamlToObject("testdata/test-app2.yaml", app2)).Should(BeNil()) + app2.Namespace = appNS2 + Expect(cr2ux.AddOrUpdate(context.Background(), app2)).Should(BeNil()) + comp1 := model.ApplicationComponent{AppPrimaryKey: apName1, Name: "blog"} + Expect(ds.Get(context.Background(), &comp1)).Should(BeNil()) + Expect(comp1.Properties).Should(BeEquivalentTo(&model.JSONStruct{"image": "wordpress"})) + + By("app not created, but the name is occupied by the same name app from other namespace") + gotApp, gotAppName, err = cr2ux.getApp(context.Background(), apName1, appNS1) + Expect(gotAppName).Should(BeEquivalentTo(formatAppComposedName(apName1, appNS1))) + Expect(gotApp).Should(BeNil()) + Expect(err).ShouldNot(BeNil()) + + By("app get the created app") + gotApp, gotAppName, err = cr2ux.getApp(context.Background(), apName1, appNS2) + Expect(gotAppName).Should(BeEquivalentTo(apName1)) + Expect(gotApp.Labels[model.LabelSourceOfTruth]).Should(BeEquivalentTo(model.FromCR)) + Expect(err).Should(BeNil()) + Expect(gotApp.IsSynced()).Should(BeEquivalentTo(true)) + + }) + It("Test app updated and delete app", func() { + ctx := context.Background() + By("Preparing database") + dbNamespace := "update-app-db-ns1-test" + + apName1 := "example" + appNS1 := "update-app-test-ns1" + ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: dbNamespace}) + Expect(ds).ToNot(BeNil()) + Expect(err).Should(BeNil()) + var ns = corev1.Namespace{} + ns.Name = dbNamespace + err = k8sClient.Create(context.TODO(), &ns) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + ns.Name = appNS1 + ns.ResourceVersion = "" + err = k8sClient.Create(context.TODO(), &ns) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + cr2ux := CR2UX{ds: ds, cli: k8sClient, cache: sync.Map{}} + + By("create test app1 and check the syncing results") + app1 := &v1beta1.Application{} + Expect(common2.ReadYamlToObject("testdata/test-app1.yaml", app1)).Should(BeNil()) + app1.Namespace = appNS1 + Expect(cr2ux.AddOrUpdate(context.Background(), app1)).Should(BeNil()) + comp1 := model.ApplicationComponent{AppPrimaryKey: apName1, Name: "nginx"} + Expect(ds.Get(context.Background(), &comp1)).Should(BeNil()) + Expect(comp1.Properties).Should(BeEquivalentTo(&model.JSONStruct{"image": "nginx"})) + + comp2 := model.ApplicationComponent{AppPrimaryKey: app1.Name, Name: "nginx2"} + Expect(ds.Get(ctx, &comp2)).Should(BeNil()) + Expect(comp2.Properties).Should(BeEquivalentTo(&model.JSONStruct{"image": "nginx2"})) + + appPlc1 := model.ApplicationPolicy{AppPrimaryKey: app1.Name, Name: "topology-beijing-demo"} + Expect(ds.Get(ctx, &appPlc1)).Should(BeNil()) + Expect(appPlc1.Properties).Should(BeEquivalentTo(&model.JSONStruct{"namespace": "demo", "clusterLabelSelector": map[string]interface{}{"region": "beijing"}})) + + appPlc2 := model.ApplicationPolicy{AppPrimaryKey: app1.Name, Name: "topology-local"} + Expect(ds.Get(ctx, &appPlc2)).Should(BeNil()) + Expect(appPlc2.Properties).Should(BeEquivalentTo(&model.JSONStruct{"targets": []interface{}{"local/demo", "local/ackone-demo"}})) + + appwf1 := model.Workflow{AppPrimaryKey: app1.Name, Name: model.AutoGenWorkflowNamePrefix + app1.Name} + Expect(ds.Get(ctx, &appwf1)).Should(BeNil()) + Expect(len(appwf1.Steps)).Should(BeEquivalentTo(1)) + + app2 := &v1beta1.Application{} + Expect(common2.ReadYamlToObject("testdata/test-app2.yaml", app2)).Should(BeNil()) + app1.Namespace = appNS1 + app1.Generation = 2 + app1.Spec = app2.Spec + Expect(cr2ux.AddOrUpdate(context.Background(), app1)).Should(BeNil()) + comp3 := model.ApplicationComponent{AppPrimaryKey: apName1, Name: "blog"} + Expect(ds.Get(context.Background(), &comp3)).Should(BeNil()) + Expect(comp3.Properties).Should(BeEquivalentTo(&model.JSONStruct{"image": "wordpress"})) + + Expect(ds.Get(ctx, &comp1)).Should(BeEquivalentTo(datastore.ErrRecordNotExist)) + Expect(ds.Get(ctx, &comp2)).Should(BeEquivalentTo(datastore.ErrRecordNotExist)) + Expect(ds.Get(ctx, &appPlc1)).Should(BeEquivalentTo(datastore.ErrRecordNotExist), fmt.Sprintf("plc name %s, creator %s", appPlc1.Name, appPlc1.Creator)) + Expect(ds.Get(ctx, &appPlc2)).Should(BeEquivalentTo(datastore.ErrRecordNotExist), fmt.Sprintf("plc name %s, creator %s", appPlc2.Name, appPlc2.Creator)) + appwf2 := &model.Workflow{AppPrimaryKey: apName1, Name: appwf1.Name} + Expect(ds.Get(ctx, appwf2)).Should(BeNil()) + + Expect(len(appwf2.Steps)).Should(BeEquivalentTo(0)) + + Expect(cr2ux.DeleteApp(ctx, app1)).Should(BeNil()) + Expect(ds.Get(context.Background(), &comp3)).Should(BeEquivalentTo(datastore.ErrRecordNotExist)) + }) + +}) diff --git a/pkg/apiserver/sync/store.go b/pkg/apiserver/sync/store.go index 99972824f..628b1d93b 100644 --- a/pkg/apiserver/sync/store.go +++ b/pkg/apiserver/sync/store.go @@ -167,7 +167,9 @@ func StorePolicy(ctx context.Context, appPrimaryKey string, expPolicies []*model // delete the components that not belongs to the new app for _, entity := range originPolicies { plc := entity.(*model.ApplicationPolicy) - // we only compare for policies that automatically generated by sync process, and the policy should not be ref ones. + // we only compare for policies that automatically generated by sync process + // and the policy should not be ref ones. + if plc.Creator != model.AutoGenPolicy { continue } diff --git a/pkg/apiserver/sync/worker.go b/pkg/apiserver/sync/worker.go index a0d1c54f7..bc5708650 100644 --- a/pkg/apiserver/sync/worker.go +++ b/pkg/apiserver/sync/worker.go @@ -67,7 +67,7 @@ func startAppSyncing(ctx context.Context, factory dynamicinformer.DynamicSharedI cli: cli, cache: sync.Map{}, } - if err = cu.InitCache(ctx); err != nil { + if err = cu.initCache(ctx); err != nil { klog.Fatal("sync app init err", err) } diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index 8dba9f204..5d65afb24 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -190,7 +190,4 @@ const ( // AnnotationServiceAccountName indicates the name of the ServiceAccount to use to apply Components and run Workflow. // ServiceAccount will be used in the local cluster only. AnnotationServiceAccountName = "app.oam.dev/service-account-name" - - // AnnotationSOTFromInner indicates the application source of truth is from inner and should not be synced - AnnotationSOTFromInner = "sot.oam.dev/from-inner" )