Feat: add readOnly for velaux application which is synced from CR (#3479)

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
This commit is contained in:
Jianbo Sun 2022-03-21 16:33:29 +08:00 committed by GitHub
parent 9671e3b232
commit 9a3ad7ef84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 445 additions and 130 deletions

View File

@ -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": {

View File

@ -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"`

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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})
}

View File

@ -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))
})
})

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"
)