mirror of https://github.com/kubevela/kubevela.git
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:
parent
9671e3b232
commit
9a3ad7ef84
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue