Feat: sync application from CR to data store (#3428)

* Feat: sync application from CR to data store

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

* Feature: address comments

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

* Feat: add migrate database feature to avoid max 63 charactor in kubeapi storage

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

* update the sync data

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
This commit is contained in:
Jianbo Sun
2022-03-18 09:55:15 +08:00
committed by GitHub
parent 5209be6da9
commit 6354912bba
37 changed files with 1637 additions and 147 deletions
+2 -1
View File
@@ -73,6 +73,7 @@ type Entity interface {
SetUpdateTime(time time.Time) SetUpdateTime(time time.Time)
PrimaryKey() string PrimaryKey() string
TableName() string TableName() string
ShortTableName() string
Index() map[string]string Index() map[string]string
} }
@@ -141,7 +142,7 @@ type DataStore interface {
// Get entity from database, Name() and TableName() can't return zero value. // Get entity from database, Name() and TableName() can't return zero value.
Get(ctx context.Context, entity Entity) error Get(ctx context.Context, entity Entity) error
// List entities from database, TableName() can't return zero value. // List entities from database, TableName() can't return zero value, if no matches, it will return a zero list without error.
List(ctx context.Context, query Entity, options *ListOptions) ([]Entity, error) List(ctx context.Context, query Entity, options *ListOptions) ([]Entity, error)
// Count entities from database, TableName() can't return zero value. // Count entities from database, TableName() can't return zero value.
+4 -1
View File
@@ -64,6 +64,7 @@ func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error)
return nil, fmt.Errorf("create namespace failure %w", err) return nil, fmt.Errorf("create namespace failure %w", err)
} }
} }
migrate(cfg.Database)
return &kubeapi{ return &kubeapi{
kubeclient: kubeClient, kubeclient: kubeClient,
namespace: cfg.Database, namespace: cfg.Database,
@@ -71,7 +72,9 @@ func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error)
} }
func generateName(entity datastore.Entity) string { func generateName(entity datastore.Entity) string {
name := fmt.Sprintf("veladatabase-%s-%s", entity.TableName(), entity.PrimaryKey()) // record the old ways here, it'll be migrated
// name := fmt.Sprintf("veladatabase-%s-%s", entity.TableName(), entity.PrimaryKey())
name := fmt.Sprintf("%s-%s", entity.ShortTableName(), entity.PrimaryKey())
return strings.ReplaceAll(name, "_", "-") return strings.ReplaceAll(name, "_", "-")
} }
@@ -0,0 +1,85 @@
/*
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 kubeapi
import (
"context"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/klog"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
)
// migrate will migrate the configmap to new short table name, it won't delete the configmaps:
// users can delete by the following commands:
// kubectl -n kubevela delete cm -l migrated=ok
func migrate(dbns string) {
kubeClient, err := clients.GetKubeClient()
if err != nil {
panic(err)
}
models := model.GetRegisterModels()
for _, k := range models {
var configMaps corev1.ConfigMapList
table := k.TableName()
selector, _ := labels.Parse(fmt.Sprintf("table=%s", table))
if err = kubeClient.List(context.Background(), &configMaps, &client.ListOptions{Namespace: dbns, LabelSelector: selector}); err != nil {
err = client.IgnoreNotFound(err)
if err != nil {
klog.Errorf("migrate db for kubeapi storage err: %v", err)
continue
}
}
var migrated bool
for _, cm := range configMaps.Items {
if strings.HasPrefix(cm.Name, strings.ReplaceAll(k.ShortTableName()+"-", "_", "-")) {
migrated = true
break
}
}
if migrated || len(configMaps.Items) == 0 {
continue
}
klog.Infof("migrating data for table %v", k.TableName())
for _, cm := range configMaps.Items {
cm := cm
checkprefix := strings.ReplaceAll(fmt.Sprintf("veladatabase-%s", k.TableName()), "_", "-")
if !strings.HasPrefix(cm.Name, checkprefix) {
continue
}
cm.Labels["migrated"] = "ok"
err = kubeClient.Update(context.Background(), &cm)
if err != nil {
klog.Errorf("update migrated record %s for kubeapi storage err: %v", cm.Name, err)
}
cm.Name = strings.ReplaceAll(k.ShortTableName()+strings.TrimPrefix(cm.Name, checkprefix), "_", "-")
cm.ResourceVersion = ""
delete(cm.Labels, "migrated")
err = kubeClient.Create(context.Background(), &cm)
if err != nil {
klog.Errorf("migrate record %s for kubeapi storage err: %v", cm.Name, err)
}
}
}
}
+42 -1
View File
@@ -18,13 +18,14 @@ package model
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
) )
func init() { func init() {
RegistModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &ApplicationRevision{}, &ApplicationTrigger{}) RegisterModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &ApplicationRevision{}, &ApplicationTrigger{})
} }
// Application application delivery model // Application application delivery model
@@ -43,7 +44,15 @@ func (a *Application) TableName() string {
return tableNamePrefix + "application" return tableNamePrefix + "application"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (a *Application) ShortTableName() string {
return "app"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
// the app primary key is the app name, so the app name is globally unique in every namespace
// when the app is synced from CR, the first synced one be same with app name,
// if there's any conflicts, the name will be composed by <appname>-<namespace>
func (a *Application) PrimaryKey() string { func (a *Application) PrimaryKey() string {
return a.Name return a.Name
} }
@@ -60,6 +69,18 @@ func (a *Application) Index() map[string]string {
return index return index
} }
// GetAppNameForSynced will trim namespace suffix for synced CR
func (a *Application) GetAppNameForSynced() string {
if a.Labels == nil {
return a.Name
}
namespace := a.Labels[LabelSyncNamespace]
if namespace == "" {
return a.Name
}
return strings.TrimSuffix(a.Name, "-"+namespace)
}
// ClusterSelector cluster selector // ClusterSelector cluster selector
type ClusterSelector struct { type ClusterSelector struct {
Name string `json:"name"` Name string `json:"name"`
@@ -102,6 +123,11 @@ func (a *ApplicationComponent) TableName() string {
return tableNamePrefix + "application_component" return tableNamePrefix + "application_component"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (a *ApplicationComponent) ShortTableName() string {
return "app_cmp"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (a *ApplicationComponent) PrimaryKey() string { func (a *ApplicationComponent) PrimaryKey() string {
return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name) return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name)
@@ -138,6 +164,11 @@ func (a *ApplicationPolicy) TableName() string {
return tableNamePrefix + "application_policy" return tableNamePrefix + "application_policy"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (a *ApplicationPolicy) ShortTableName() string {
return "app_plc"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (a *ApplicationPolicy) PrimaryKey() string { func (a *ApplicationPolicy) PrimaryKey() string {
return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name) return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name)
@@ -270,6 +301,11 @@ func (a *ApplicationRevision) TableName() string {
return tableNamePrefix + "application_revision" return tableNamePrefix + "application_revision"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (a *ApplicationRevision) ShortTableName() string {
return "app_rev"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (a *ApplicationRevision) PrimaryKey() string { func (a *ApplicationRevision) PrimaryKey() string {
return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Version) return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Version)
@@ -349,6 +385,11 @@ func (w *ApplicationTrigger) TableName() string {
return tableNamePrefix + "trigger" return tableNamePrefix + "trigger"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (w *ApplicationTrigger) ShortTableName() string {
return "app_tg"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (w *ApplicationTrigger) PrimaryKey() string { func (w *ApplicationTrigger) PrimaryKey() string {
return w.Token return w.Token
+6 -1
View File
@@ -23,7 +23,7 @@ import (
) )
func init() { func init() {
RegistModel(&Cluster{}) RegisterModel(&Cluster{})
} }
// ProviderInfo describes the information from provider API // ProviderInfo describes the information from provider API
@@ -82,6 +82,11 @@ func (c *Cluster) TableName() string {
return tableNamePrefix + "cluster" return tableNamePrefix + "cluster"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (c *Cluster) ShortTableName() string {
return "cls"
}
// PrimaryKey primary key for datastore // PrimaryKey primary key for datastore
func (c *Cluster) PrimaryKey() string { func (c *Cluster) PrimaryKey() string {
return c.Name return c.Name
+6 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
package model package model
func init() { func init() {
RegistModel(&Env{}) RegisterModel(&Env{})
} }
// Env models the data of env in database // Env models the data of env in database
@@ -42,6 +42,11 @@ func (p *Env) TableName() string {
return tableNamePrefix + "env" return tableNamePrefix + "env"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (p *Env) ShortTableName() string {
return "ev"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (p *Env) PrimaryKey() string { func (p *Env) PrimaryKey() string {
return p.Name return p.Name
+7 -1
View File
@@ -19,13 +19,14 @@ package model
import "fmt" import "fmt"
func init() { func init() {
RegistModel(&EnvBinding{}) RegisterModel(&EnvBinding{})
} }
// EnvBinding application env binding // EnvBinding application env binding
type EnvBinding struct { type EnvBinding struct {
BaseModel BaseModel
AppPrimaryKey string `json:"appPrimaryKey"` AppPrimaryKey string `json:"appPrimaryKey"`
AppDeployName string `json:"appDeployName"`
Name string `json:"name"` Name string `json:"name"`
ComponentsPatch []ComponentPatch `json:"componentsPatchs"` ComponentsPatch []ComponentPatch `json:"componentsPatchs"`
} }
@@ -50,6 +51,11 @@ func (e *EnvBinding) TableName() string {
return tableNamePrefix + "envbinding" return tableNamePrefix + "envbinding"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (e *EnvBinding) ShortTableName() string {
return "evb"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (e *EnvBinding) PrimaryKey() string { func (e *EnvBinding) PrimaryKey() string {
return fmt.Sprintf("%s-%s", e.AppPrimaryKey, e.Name) return fmt.Sprintf("%s-%s", e.AppPrimaryKey, e.Name)
+17 -8
View File
@@ -30,28 +30,37 @@ import (
var tableNamePrefix = "vela_" var tableNamePrefix = "vela_"
var registedModels = map[string]Interface{} var registeredModels = map[string]Interface{}
// Interface model interface // Interface model interface
type Interface interface { type Interface interface {
TableName() string TableName() string
ShortTableName() string
} }
// RegistModel regist model // RegisterModel register model
func RegistModel(models ...Interface) { func RegisterModel(models ...Interface) {
for _, model := range models { for _, model := range models {
if _, exist := registedModels[model.TableName()]; exist { if _, exist := registeredModels[model.TableName()]; exist {
panic(fmt.Errorf("model table name %s conflict", model.TableName())) panic(fmt.Errorf("model table name %s conflict", model.TableName()))
} }
registedModels[model.TableName()] = model registeredModels[model.TableName()] = model
} }
} }
// GetRegisterModels will return the register models
func GetRegisterModels() map[string]Interface {
return registeredModels
}
// JSONStruct json struct, same with runtime.RawExtension // JSONStruct json struct, same with runtime.RawExtension
type JSONStruct map[string]interface{} type JSONStruct map[string]interface{}
// NewJSONStruct new jsonstruct from runtime.RawExtension // NewJSONStruct new json struct from runtime.RawExtension
func NewJSONStruct(raw *runtime.RawExtension) (*JSONStruct, error) { func NewJSONStruct(raw *runtime.RawExtension) (*JSONStruct, error) {
if raw == nil || raw.Raw == nil {
return nil, nil
}
var data JSONStruct var data JSONStruct
err := json.Unmarshal(raw.Raw, &data) err := json.Unmarshal(raw.Raw, &data)
if err != nil { if err != nil {
@@ -60,7 +69,7 @@ func NewJSONStruct(raw *runtime.RawExtension) (*JSONStruct, error) {
return &data, nil return &data, nil
} }
// NewJSONStructByString new jsonstruct from string // NewJSONStructByString new json struct from string
func NewJSONStructByString(source string) (*JSONStruct, error) { func NewJSONStructByString(source string) (*JSONStruct, error) {
if source == "" { if source == "" {
return nil, nil return nil, nil
@@ -73,7 +82,7 @@ func NewJSONStructByString(source string) (*JSONStruct, error) {
return &data, nil return &data, nil
} }
// NewJSONStructByStruct new jsonstruct from strcut object // NewJSONStructByStruct new json struct from struct object
func NewJSONStructByStruct(object interface{}) (*JSONStruct, error) { func NewJSONStructByStruct(object interface{}) (*JSONStruct, error) {
if object == nil { if object == nil {
return nil, nil return nil, nil
+7 -2
View File
@@ -17,10 +17,10 @@ limitations under the License.
package model package model
func init() { func init() {
RegistModel(&Project{}) RegisterModel(&Project{})
} }
// Project project model // Project basic model
type Project struct { type Project struct {
BaseModel BaseModel
Name string `json:"name"` Name string `json:"name"`
@@ -33,6 +33,11 @@ func (p *Project) TableName() string {
return tableNamePrefix + "project" return tableNamePrefix + "project"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (p *Project) ShortTableName() string {
return "pj"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (p *Project) PrimaryKey() string { func (p *Project) PrimaryKey() string {
return p.Name return p.Name
+6 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
package model package model
func init() { func init() {
RegistModel(&SystemInfo{}) RegisterModel(&SystemInfo{})
} }
// LoginType is the type of login // LoginType is the type of login
@@ -43,6 +43,11 @@ func (u *SystemInfo) TableName() string {
return tableNamePrefix + "system_info" return tableNamePrefix + "system_info"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (u *SystemInfo) ShortTableName() string {
return "sysi"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (u *SystemInfo) PrimaryKey() string { func (u *SystemInfo) PrimaryKey() string {
return u.InstallID return u.InstallID
+6 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
package model package model
func init() { func init() {
RegistModel(&Target{}) RegisterModel(&Target{})
} }
// Target defines the delivery target information for the application // Target defines the delivery target information for the application
@@ -36,6 +36,11 @@ func (d *Target) TableName() string {
return tableNamePrefix + "target" return tableNamePrefix + "target"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (d *Target) ShortTableName() string {
return "tg"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (d *Target) PrimaryKey() string { func (d *Target) PrimaryKey() string {
return d.Name return d.Name
+6 -1
View File
@@ -19,7 +19,7 @@ package model
import "strings" import "strings"
func init() { func init() {
RegistModel(&User{}) RegisterModel(&User{})
} }
// User is the model of user // User is the model of user
@@ -37,6 +37,11 @@ func (u *User) TableName() string {
return tableNamePrefix + "user" return tableNamePrefix + "user"
} }
// ShortTableName return custom table name
func (u *User) ShortTableName() string {
return "usr"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (u *User) PrimaryKey() string { func (u *User) PrimaryKey() string {
return verifyUserValue(u.Name) return verifyUserValue(u.Name)
+73
View File
@@ -0,0 +1,73 @@
/*
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 model
const (
// AutoGenDesc describes the metadata in datastore that's automatically generated
AutoGenDesc = "Automatically converted from KubeVela Application in Kubernetes."
// AutoGenProj describes the automatically created project
AutoGenProj = "Automatically generated by sync mechanism."
// AutoGenEnvNamePrefix describes the common prefix for auto-generated env
AutoGenEnvNamePrefix = "synced-"
// AutoGenComp describes the creator of component that is auto-generated
AutoGenComp = "synced-comp"
// AutoGenPolicy describes the creator of policy that is auto-generated
AutoGenPolicy = "synced-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"
// AutoGenWorkflowNamePrefix describes the common prefix for auto-generated workflow
AutoGenWorkflowNamePrefix = "synced-"
// AutoGenTargetNamePrefix describes the common prefix for auto-generated target
AutoGenTargetNamePrefix = "synced-"
// LabelSyncGeneration describes the generation synced from
LabelSyncGeneration = "ux.oam.dev/synced-generation"
// LabelSyncNamespace describes the namespace synced from
LabelSyncNamespace = "ux.oam.dev/from-namespace"
)
// DataStoreApp is a memory struct that describes the model of an application in datastore
type DataStoreApp struct {
AppMeta *Application
Env *Env
Eb *EnvBinding
Comps []*ApplicationComponent
Policies []*ApplicationPolicy
Workflow *Workflow
Targets []*Target
}
const (
// DefaultInitName is default object name for initialization
DefaultInitName = "default"
// DefaultAddonProject is default addon projects
DefaultAddonProject = "addons"
// DefaultInitNamespace is default namespace name for initialization
DefaultInitNamespace = "default"
// DefaultTargetDescription describes default target created
DefaultTargetDescription = "Default target is created by velaux system automatically."
// DefaultEnvDescription describes default env created
DefaultEnvDescription = "Default environment is created by velaux system automatically."
// DefaultProjectDescription describes the default project created
DefaultProjectDescription = "Default project is created by velaux system automatically."
)
+12 -2
View File
@@ -25,8 +25,8 @@ import (
) )
func init() { func init() {
RegistModel(&Workflow{}) RegisterModel(&Workflow{})
RegistModel(&WorkflowRecord{}) RegisterModel(&WorkflowRecord{})
} }
// Workflow application delivery database model // Workflow application delivery database model
@@ -61,6 +61,11 @@ func (w *Workflow) TableName() string {
return tableNamePrefix + "workflow" return tableNamePrefix + "workflow"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (w *Workflow) ShortTableName() string {
return "wf"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (w *Workflow) PrimaryKey() string { func (w *Workflow) PrimaryKey() string {
return fmt.Sprintf("%s-%s", w.AppPrimaryKey, w.Name) return fmt.Sprintf("%s-%s", w.AppPrimaryKey, w.Name)
@@ -118,6 +123,11 @@ func (w *WorkflowRecord) TableName() string {
return tableNamePrefix + "workflow_record" return tableNamePrefix + "workflow_record"
} }
// ShortTableName is the compressed version of table name for kubeapi storage and others
func (w *WorkflowRecord) ShortTableName() string {
return "wfr"
}
// PrimaryKey return custom primary key // PrimaryKey return custom primary key
func (w *WorkflowRecord) PrimaryKey() string { func (w *WorkflowRecord) PrimaryKey() string {
return w.Name return w.Name
+7 -3
View File
@@ -39,6 +39,7 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/webservice" "github.com/oam-dev/kubevela/pkg/apiserver/rest/webservice"
velasync "github.com/oam-dev/kubevela/pkg/apiserver/sync"
utils2 "github.com/oam-dev/kubevela/pkg/utils" utils2 "github.com/oam-dev/kubevela/pkg/utils"
) )
@@ -116,7 +117,6 @@ func (s *restServer) Run(ctx context.Context) error {
go func() { go func() {
leaderelection.RunOrDie(ctx, *l) leaderelection.RunOrDie(ctx, *l)
}() }()
return s.startHTTP(ctx) return s.startHTTP(ctx)
} }
@@ -138,10 +138,13 @@ func (s *restServer) setupLeaderElection() (*leaderelection.LeaderElectionConfig
RetryPeriod: time.Second * 2, RetryPeriod: time.Second * 2,
Callbacks: leaderelection.LeaderCallbacks{ Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) { OnStartedLeading: func(ctx context.Context) {
s.runLeader(ctx, s.cfg.LeaderConfig.Duration) go velasync.Start(ctx, s.dataStore, restCfg)
s.runWorkflowRecordSync(ctx, s.cfg.LeaderConfig.Duration)
}, },
OnStoppedLeading: func() { OnStoppedLeading: func() {
klog.Infof("leader lost: %s", s.cfg.LeaderConfig.ID) klog.Infof("leader lost: %s", s.cfg.LeaderConfig.ID)
// Currently, the started goroutine will all closed by the context, so there seems no need to call os.Exit here.
// But it can be safe to stop the process as leader lost.
os.Exit(0) os.Exit(0)
}, },
OnNewLeader: func(identity string) { OnNewLeader: func(identity string) {
@@ -155,7 +158,8 @@ func (s *restServer) setupLeaderElection() (*leaderelection.LeaderElectionConfig
}, nil }, nil
} }
func (s restServer) runLeader(ctx context.Context, duration time.Duration) { func (s restServer) runWorkflowRecordSync(ctx context.Context, duration time.Duration) {
klog.Infof("start to syncing workflow record")
w := usecase.NewWorkflowUsecase(s.dataStore, usecase.NewEnvUsecase(s.dataStore)) w := usecase.NewWorkflowUsecase(s.dataStore, usecase.NewEnvUsecase(s.dataStore))
t := time.NewTicker(duration) t := time.NewTicker(duration)
+2 -4
View File
@@ -26,14 +26,12 @@ import (
"sync" "sync"
"time" "time"
"k8s.io/client-go/discovery"
k8stypes "k8s.io/apimachinery/pkg/types"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
errors2 "k8s.io/apimachinery/pkg/api/errors" errors2 "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
+12 -39
View File
@@ -26,11 +26,10 @@ import (
"strings" "strings"
"time" "time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
@@ -49,6 +48,7 @@ import (
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
"github.com/oam-dev/kubevela/pkg/apiserver/sync"
"github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
utils2 "github.com/oam-dev/kubevela/pkg/utils" utils2 "github.com/oam-dev/kubevela/pkg/utils"
@@ -279,7 +279,7 @@ func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmo
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = c.kubeClient.Get(ctx, types.NamespacedName{Namespace: env.Namespace, Name: appmodel.Name}, &app) err = c.kubeClient.Get(ctx, types.NamespacedName{Namespace: env.Namespace, Name: appmodel.GetAppNameForSynced()}, &app)
if err != nil { if err != nil {
if apierrors.IsNotFound(err) { if apierrors.IsNotFound(err) {
return nil, nil return nil, nil
@@ -294,9 +294,10 @@ func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmo
// GetApplicationCR get application CR in cluster // GetApplicationCR get application CR in cluster
func (c *applicationUsecaseImpl) GetApplicationCR(ctx context.Context, appModel *model.Application) (*v1beta1.ApplicationList, error) { func (c *applicationUsecaseImpl) GetApplicationCR(ctx context.Context, appModel *model.Application) (*v1beta1.ApplicationList, error) {
var apps v1beta1.ApplicationList var apps v1beta1.ApplicationList
selector := labels.NewSelector() selector := labels.NewSelector()
re, err := labels.NewRequirement(oam.AnnotationAppName, selection.Equals, []string{appModel.Name}) re, err := labels.NewRequirement(oam.AnnotationAppName, selection.Equals, []string{appModel.GetAppNameForSynced()})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -998,7 +999,7 @@ func (c *applicationUsecaseImpl) GetApplicationComponent(ctx context.Context, ap
err := c.ds.Get(ctx, &component) err := c.ds.Get(ctx, &component)
if err != nil { if err != nil {
if errors.Is(err, datastore.ErrRecordNotExist) { if errors.Is(err, datastore.ErrRecordNotExist) {
return nil, bcode.ErrApplicationComponetNotExist return nil, bcode.ErrApplicationComponentNotExist
} }
return nil, err return nil, err
} }
@@ -1083,7 +1084,7 @@ func (c *applicationUsecaseImpl) createComponent(ctx context.Context, app *model
if err := c.ds.Add(ctx, &componentModel); err != nil { if err := c.ds.Add(ctx, &componentModel); err != nil {
if errors.Is(err, datastore.ErrRecordExist) { if errors.Is(err, datastore.ErrRecordExist) {
return nil, bcode.ErrApplicationComponetExist return nil, bcode.ErrApplicationComponentExist
} }
log.Logger.Warnf("add component for app %s failure %s", utils2.Sanitize(app.PrimaryKey()), err.Error()) log.Logger.Warnf("add component for app %s failure %s", utils2.Sanitize(app.PrimaryKey()), err.Error())
return nil, err return nil, err
@@ -1149,11 +1150,11 @@ func convertComponentModelToBase(componentModel *model.ApplicationComponent) *ap
func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.Application, component *model.ApplicationComponent) error { func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.Application, component *model.ApplicationComponent) error {
if component.Main { if component.Main {
return bcode.ErrApplicationComponetNotAllowDelete return bcode.ErrApplicationComponentNotAllowDelete
} }
if err := c.ds.Delete(ctx, component); err != nil { if err := c.ds.Delete(ctx, component); err != nil {
if errors.Is(err, datastore.ErrRecordNotExist) { if errors.Is(err, datastore.ErrRecordNotExist) {
return bcode.ErrApplicationComponetNotExist return bcode.ErrApplicationComponentNotExist
} }
log.Logger.Warnf("delete app component %s failure %s", app.PrimaryKey(), err.Error()) log.Logger.Warnf("delete app component %s failure %s", app.PrimaryKey(), err.Error())
return err return err
@@ -1528,34 +1529,6 @@ func genWebhookToken() string {
return string(b) return string(b)
} }
func convertToModelComponent(appPrimaryKey string, component common.ApplicationComponent) (model.ApplicationComponent, error) {
bc := model.ApplicationComponent{
AppPrimaryKey: appPrimaryKey,
Name: component.Name,
Type: component.Type,
ExternalRevision: component.ExternalRevision,
DependsOn: component.DependsOn,
Inputs: component.Inputs,
Outputs: component.Outputs,
Scopes: component.Scopes,
}
if component.Properties != nil {
properties, err := model.NewJSONStruct(component.Properties)
if err != nil {
return bc, err
}
bc.Properties = properties
}
for _, trait := range component.Traits {
properties, err := model.NewJSONStruct(trait.Properties)
if err != nil {
return bc, err
}
bc.Traits = append(bc.Traits, model.ApplicationTrait{CreateTime: time.Now(), UpdateTime: time.Now(), Properties: properties, Type: trait.Type, Alias: trait.Type, Description: "auto gen"})
}
return bc, nil
}
func (c *applicationUsecaseImpl) getAppFromLatestRevision(ctx context.Context, appName string, envName string, version string) (*v1beta1.Application, error) { func (c *applicationUsecaseImpl) getAppFromLatestRevision(ctx context.Context, appName string, envName string, version string) (*v1beta1.Application, error) {
ar := &model.ApplicationRevision{AppPrimaryKey: appName} ar := &model.ApplicationRevision{AppPrimaryKey: appName}
@@ -1590,7 +1563,7 @@ func (c *applicationUsecaseImpl) resetApp(ctx context.Context, targetApp *v1beta
originComps, err := c.ds.List(ctx, &model.ApplicationComponent{AppPrimaryKey: appPrimaryKey}, &datastore.ListOptions{}) originComps, err := c.ds.List(ctx, &model.ApplicationComponent{AppPrimaryKey: appPrimaryKey}, &datastore.ListOptions{})
if err != nil { if err != nil {
return nil, bcode.ErrApplicationComponetNotExist return nil, bcode.ErrApplicationComponentNotExist
} }
var originCompNames []string var originCompNames []string
@@ -1605,7 +1578,7 @@ func (c *applicationUsecaseImpl) resetApp(ctx context.Context, targetApp *v1beta
targetCompNames = append(targetCompNames, comp.Name) targetCompNames = append(targetCompNames, comp.Name)
} }
readyToUpdate, readyToDelete, readyToAdd := compareSlices(originCompNames, targetCompNames) readyToUpdate, readyToDelete, readyToAdd := utils2.ThreeWaySliceCompare(originCompNames, targetCompNames)
// delete new app's components // delete new app's components
for _, compName := range readyToDelete { for _, compName := range readyToDelete {
@@ -1624,7 +1597,7 @@ func (c *applicationUsecaseImpl) resetApp(ctx context.Context, targetApp *v1beta
for _, comp := range targetComps { for _, comp := range targetComps {
// add or update new app's components from old app // add or update new app's components from old app
if utils.StringsContain(readyToAdd, comp.Name) || utils.StringsContain(readyToUpdate, comp.Name) { if utils.StringsContain(readyToAdd, comp.Name) || utils.StringsContain(readyToUpdate, comp.Name) {
compModel, err := convertToModelComponent(appPrimaryKey, comp) compModel, err := sync.ConvertFromCRComponent(appPrimaryKey, comp)
if err != nil { if err != nil {
return &apisv1.AppResetResponse{}, bcode.ErrInvalidProperties return &apisv1.AppResetResponse{}, bcode.ErrInvalidProperties
} }
+6 -29
View File
@@ -114,7 +114,7 @@ func listFullEnvBinding(ctx context.Context, ds datastore.DataStore, option envL
log.Logger.Errorf("envbinding invalid %s", err.Error()) log.Logger.Errorf("envbinding invalid %s", err.Error())
continue continue
} }
list = append(list, convertEnvbindingModelToBase(eb, env, targets)) list = append(list, convertEnvBindingModelToBase(eb, env, targets))
} }
return list, nil return list, nil
} }
@@ -315,7 +315,7 @@ func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, app *model
return nil, err return nil, err
} }
return &apisv1.DetailEnvBindingResponse{ return &apisv1.DetailEnvBindingResponse{
EnvBindingBase: *convertEnvbindingModelToBase(envBinding, env, targets), EnvBindingBase: *convertEnvBindingModelToBase(envBinding, env, targets),
}, nil }, nil
} }
@@ -346,11 +346,12 @@ func convertCreateReqToEnvBindingModel(app *model.Application, req apisv1.Create
envBinding := model.EnvBinding{ envBinding := model.EnvBinding{
AppPrimaryKey: app.Name, AppPrimaryKey: app.Name,
Name: req.Name, Name: req.Name,
AppDeployName: app.GetAppNameForSynced(),
} }
return envBinding return envBinding
} }
func convertEnvbindingModelToBase(envBinding *model.EnvBinding, env *model.Env, targets []*model.Target) *apisv1.EnvBindingBase { func convertEnvBindingModelToBase(envBinding *model.EnvBinding, env *model.Env, targets []*model.Target) *apisv1.EnvBindingBase {
var dtMap = make(map[string]*model.Target, len(targets)) var dtMap = make(map[string]*model.Target, len(targets))
for _, dte := range targets { for _, dte := range targets {
dtMap[dte.Name] = dte dtMap[dte.Name] = dte
@@ -379,7 +380,7 @@ func convertEnvbindingModelToBase(envBinding *model.EnvBinding, env *model.Env,
Targets: envBindingTargets, Targets: envBindingTargets,
CreateTime: envBinding.CreateTime, CreateTime: envBinding.CreateTime,
UpdateTime: envBinding.UpdateTime, UpdateTime: envBinding.UpdateTime,
AppDeployName: envBinding.AppPrimaryKey, AppDeployName: envBinding.AppDeployName,
AppDeployNamespace: env.Namespace, AppDeployNamespace: env.Namespace,
} }
return ebb return ebb
@@ -389,6 +390,7 @@ func convertToEnvBindingModel(app *model.Application, envBind apisv1.EnvBinding)
re := model.EnvBinding{ re := model.EnvBinding{
AppPrimaryKey: app.Name, AppPrimaryKey: app.Name,
Name: envBind.Name, Name: envBind.Name,
AppDeployName: app.GetAppNameForSynced(),
} }
return &re return &re
} }
@@ -397,31 +399,6 @@ func convertWorkflowName(envName string) string {
return fmt.Sprintf("workflow-%s", envName) return fmt.Sprintf("workflow-%s", envName)
} }
func compareSlices(a []string, b []string) ([]string, []string, []string) {
m := make(map[string]uint8)
for _, k := range a {
m[k] |= 1 << 0
}
for _, k := range b {
m[k] |= 1 << 1
}
var inAAndB, inAButNotB, inBButNotA []string
for k, v := range m {
a := v&(1<<0) != 0
b := v&(1<<1) != 0
switch {
case a && b:
inAAndB = append(inAAndB, k)
case a && !b:
inAButNotB = append(inAButNotB, k)
case !a && b:
inBButNotA = append(inBButNotA, k)
}
}
return inAAndB, inAButNotB, inBButNotA
}
func isEnvStepType(stepType string) bool { func isEnvStepType(stepType string) bool {
return stepType == Deploy2Env || stepType == DeployCloudResource return stepType == Deploy2Env || stepType == DeployCloudResource
} }
+11 -25
View File
@@ -31,21 +31,6 @@ import (
"github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/multicluster"
) )
const (
// DefaultInitName is default object name for initialization
DefaultInitName = "default"
// DefaultInitNamespace is default namespace name for initialization
DefaultInitNamespace = "default"
// DefaultTargetDescription describes default target created
DefaultTargetDescription = "Default target is created by velaux system automatically."
// DefaultEnvDescription describes default env created
DefaultEnvDescription = "Default environment is created by velaux system automatically."
// DefaultProjectDescription describes the default project created
DefaultProjectDescription = "Default project is created by velaux system automatically."
)
// ProjectUsecase project manage usecase. // ProjectUsecase project manage usecase.
type ProjectUsecase interface { type ProjectUsecase interface {
GetProject(ctx context.Context, projectName string) (*model.Project, error) GetProject(ctx context.Context, projectName string) (*model.Project, error)
@@ -65,7 +50,7 @@ func NewProjectUsecase(ds datastore.DataStore) ProjectUsecase {
log.Logger.Fatalf("get k8sClient failure: %s", err.Error()) log.Logger.Fatalf("get k8sClient failure: %s", err.Error())
} }
p := &projectUsecaseImpl{ds: ds, k8sClient: k8sClient} p := &projectUsecaseImpl{ds: ds, k8sClient: k8sClient}
p.initDefaultProjectEnvTarget(DefaultInitNamespace) p.initDefaultProjectEnvTarget(model.DefaultInitNamespace)
return p return p
} }
@@ -84,15 +69,15 @@ func (p *projectUsecaseImpl) initDefaultProjectEnvTarget(defaultNamespace string
} }
log.Logger.Info("no default project found, adding a default project with default env and target") log.Logger.Info("no default project found, adding a default project with default env and target")
if err := createTargetNamespace(ctx, p.k8sClient, multicluster.ClusterLocalName, defaultNamespace, DefaultInitName); err != nil { if err := createTargetNamespace(ctx, p.k8sClient, multicluster.ClusterLocalName, defaultNamespace, model.DefaultInitName); err != nil {
log.Logger.Errorf("initialize default target namespace failed %v", err) log.Logger.Errorf("initialize default target namespace failed %v", err)
return return
} }
// initialize default target first // initialize default target first
err = createTarget(ctx, p.ds, &model.Target{ err = createTarget(ctx, p.ds, &model.Target{
Name: DefaultInitName, Name: model.DefaultInitName,
Alias: "Default", Alias: "Default",
Description: DefaultTargetDescription, Description: model.DefaultTargetDescription,
Cluster: &model.ClusterTarget{ Cluster: &model.ClusterTarget{
ClusterName: multicluster.ClusterLocalName, ClusterName: multicluster.ClusterLocalName,
Namespace: defaultNamespace, Namespace: defaultNamespace,
@@ -106,12 +91,12 @@ func (p *projectUsecaseImpl) initDefaultProjectEnvTarget(defaultNamespace string
// initialize default target first // initialize default target first
err = createEnv(ctx, p.k8sClient, p.ds, &model.Env{ err = createEnv(ctx, p.k8sClient, p.ds, &model.Env{
Name: DefaultInitName, Name: model.DefaultInitName,
Alias: "Default", Alias: "Default",
Description: DefaultEnvDescription, Description: model.DefaultEnvDescription,
Project: DefaultInitName, Project: model.DefaultInitName,
Namespace: defaultNamespace, Namespace: defaultNamespace,
Targets: []string{DefaultInitName}, Targets: []string{model.DefaultInitName},
}) })
// for idempotence, ignore default env already exist error // for idempotence, ignore default env already exist error
if err != nil && errors.Is(err, bcode.ErrEnvAlreadyExists) { if err != nil && errors.Is(err, bcode.ErrEnvAlreadyExists) {
@@ -120,14 +105,15 @@ func (p *projectUsecaseImpl) initDefaultProjectEnvTarget(defaultNamespace string
} }
_, err = p.CreateProject(ctx, apisv1.CreateProjectRequest{ _, err = p.CreateProject(ctx, apisv1.CreateProjectRequest{
Name: DefaultInitName, Name: model.DefaultInitName,
Alias: "Default", Alias: "Default",
Description: DefaultProjectDescription, Description: model.DefaultProjectDescription,
}) })
if err != nil { if err != nil {
log.Logger.Errorf("initialize project failed %v", err) log.Logger.Errorf("initialize project failed %v", err)
return return
} }
} }
// GetProject get project // GetProject get project
+13 -13
View File
@@ -81,43 +81,43 @@ var _ = Describe("Test project usecase functions", func() {
return k8sClient.Get(context.TODO(), types.NamespacedName{Name: defaultNamespace}, &namespace) return k8sClient.Get(context.TODO(), types.NamespacedName{Name: defaultNamespace}, &namespace)
}, time.Second*3, time.Microsecond*300).Should(BeNil()) }, time.Second*3, time.Microsecond*300).Should(BeNil())
Expect(cmp.Diff(namespace.Labels[oam.LabelNamespaceOfEnvName], DefaultInitName)).Should(BeEmpty()) Expect(cmp.Diff(namespace.Labels[oam.LabelNamespaceOfEnvName], model.DefaultInitName)).Should(BeEmpty())
Expect(cmp.Diff(namespace.Labels[oam.LabelNamespaceOfTargetName], DefaultInitName)).Should(BeEmpty()) Expect(cmp.Diff(namespace.Labels[oam.LabelNamespaceOfTargetName], model.DefaultInitName)).Should(BeEmpty())
Expect(cmp.Diff(namespace.Labels[oam.LabelControlPlaneNamespaceUsage], oam.VelaNamespaceUsageEnv)).Should(BeEmpty()) Expect(cmp.Diff(namespace.Labels[oam.LabelControlPlaneNamespaceUsage], oam.VelaNamespaceUsageEnv)).Should(BeEmpty())
Expect(cmp.Diff(namespace.Labels[oam.LabelRuntimeNamespaceUsage], oam.VelaNamespaceUsageTarget)).Should(BeEmpty()) Expect(cmp.Diff(namespace.Labels[oam.LabelRuntimeNamespaceUsage], oam.VelaNamespaceUsageTarget)).Should(BeEmpty())
By("check project created") By("check project created")
dp, err := projectUsecase.GetProject(context.TODO(), DefaultInitName) dp, err := projectUsecase.GetProject(context.TODO(), model.DefaultInitName)
Expect(err).Should(BeNil()) Expect(err).Should(BeNil())
Expect(dp.Alias).Should(BeEquivalentTo("Default")) Expect(dp.Alias).Should(BeEquivalentTo("Default"))
Expect(dp.Description).Should(BeEquivalentTo(DefaultProjectDescription)) Expect(dp.Description).Should(BeEquivalentTo(model.DefaultProjectDescription))
By("check env created") By("check env created")
env, err := envImpl.GetEnv(context.TODO(), DefaultInitName) env, err := envImpl.GetEnv(context.TODO(), model.DefaultInitName)
Expect(err).Should(BeNil()) Expect(err).Should(BeNil())
Expect(env.Alias).Should(BeEquivalentTo("Default")) Expect(env.Alias).Should(BeEquivalentTo("Default"))
Expect(env.Description).Should(BeEquivalentTo(DefaultEnvDescription)) Expect(env.Description).Should(BeEquivalentTo(model.DefaultEnvDescription))
Expect(env.Project).Should(BeEquivalentTo(DefaultInitName)) Expect(env.Project).Should(BeEquivalentTo(model.DefaultInitName))
Expect(env.Targets).Should(BeEquivalentTo([]string{DefaultInitName})) Expect(env.Targets).Should(BeEquivalentTo([]string{model.DefaultInitName}))
Expect(env.Namespace).Should(BeEquivalentTo(defaultNamespace)) Expect(env.Namespace).Should(BeEquivalentTo(defaultNamespace))
By("check target created") By("check target created")
tg, err := targetImpl.GetTarget(context.TODO(), DefaultInitName) tg, err := targetImpl.GetTarget(context.TODO(), model.DefaultInitName)
Expect(err).Should(BeNil()) Expect(err).Should(BeNil())
Expect(tg.Alias).Should(BeEquivalentTo("Default")) Expect(tg.Alias).Should(BeEquivalentTo("Default"))
Expect(tg.Description).Should(BeEquivalentTo(DefaultTargetDescription)) Expect(tg.Description).Should(BeEquivalentTo(model.DefaultTargetDescription))
Expect(tg.Cluster).Should(BeEquivalentTo(&model.ClusterTarget{ Expect(tg.Cluster).Should(BeEquivalentTo(&model.ClusterTarget{
ClusterName: multicluster.ClusterLocalName, ClusterName: multicluster.ClusterLocalName,
Namespace: defaultNamespace, Namespace: defaultNamespace,
})) }))
Expect(env.Targets).Should(BeEquivalentTo([]string{DefaultInitName})) Expect(env.Targets).Should(BeEquivalentTo([]string{model.DefaultInitName}))
Expect(env.Namespace).Should(BeEquivalentTo(defaultNamespace)) Expect(env.Namespace).Should(BeEquivalentTo(defaultNamespace))
err = targetImpl.DeleteTarget(context.TODO(), DefaultInitName) err = targetImpl.DeleteTarget(context.TODO(), model.DefaultInitName)
Expect(err).Should(BeNil()) Expect(err).Should(BeNil())
err = envImpl.DeleteEnv(context.TODO(), DefaultInitName) err = envImpl.DeleteEnv(context.TODO(), model.DefaultInitName)
Expect(err).Should(BeNil()) Expect(err).Should(BeNil())
}) })
+5 -5
View File
@@ -197,7 +197,7 @@ func (c *customHandlerImpl) handle(ctx context.Context, webhookTrigger *model.Ap
} }
if err := c.w.ds.Get(ctx, component); err != nil { if err := c.w.ds.Get(ctx, component); err != nil {
if errors.Is(err, datastore.ErrRecordNotExist) { if errors.Is(err, datastore.ErrRecordNotExist) {
return nil, bcode.ErrApplicationComponetNotExist return nil, bcode.ErrApplicationComponentNotExist
} }
return nil, err return nil, err
} }
@@ -227,7 +227,7 @@ func (c *acrHandlerImpl) handle(ctx context.Context, webhookTrigger *model.Appli
return nil, err return nil, err
} }
if len(comps) == 0 { if len(comps) == 0 {
return nil, bcode.ErrApplicationComponetNotExist return nil, bcode.ErrApplicationComponentNotExist
} }
// use the first component as the target component // use the first component as the target component
@@ -287,7 +287,7 @@ func (c dockerHubHandlerImpl) handle(ctx context.Context, trigger *model.Applica
return nil, err return nil, err
} }
if len(comps) == 0 { if len(comps) == 0 {
return nil, bcode.ErrApplicationComponetNotExist return nil, bcode.ErrApplicationComponentNotExist
} }
// use the first component as the target component // use the first component as the target component
@@ -395,7 +395,7 @@ func (c *harborHandlerImpl) handle(ctx context.Context, webhookTrigger *model.Ap
return nil, err return nil, err
} }
if len(comps) == 0 { if len(comps) == 0 {
return nil, bcode.ErrApplicationComponetNotExist return nil, bcode.ErrApplicationComponentNotExist
} }
// use the first component as the target component // use the first component as the target component
@@ -461,7 +461,7 @@ func (j *jfrogHandlerImpl) handle(ctx context.Context, webhookTrigger *model.App
return nil, err return nil, err
} }
if len(comps) == 0 { if len(comps) == 0 {
return nil, bcode.ErrApplicationComponetNotExist return nil, bcode.ErrApplicationComponentNotExist
} }
// use the first component as the target component // use the first component as the target component
+1 -1
View File
@@ -58,7 +58,7 @@ func UpdateEnvWorkflow(ctx context.Context, kubeClient client.Client, ds datasto
} }
var filteredSteps []apisv1.WorkflowStep var filteredSteps []apisv1.WorkflowStep
_, readyToDeleteSteps, readyToAddSteps := compareSlices(workflowStepNames, envStepNames) _, readyToDeleteSteps, readyToAddSteps := utils2.ThreeWaySliceCompare(workflowStepNames, envStepNames)
for _, step := range workflow.Steps { for _, step := range workflow.Steps {
if isEnvStepType(step.Type) && utils.StringsContain(readyToDeleteSteps, step.Name) { if isEnvStepType(step.Type) && utils.StringsContain(readyToDeleteSteps, step.Name) {
@@ -37,11 +37,11 @@ var ErrDeployApplyFail = NewBcode(500, 10005, "application deploy apply failure"
// ErrNoComponent no component // ErrNoComponent no component
var ErrNoComponent = NewBcode(200, 10006, "application not have components, can not deploy") var ErrNoComponent = NewBcode(200, 10006, "application not have components, can not deploy")
// ErrApplicationComponetExist application component is exist // ErrApplicationComponentExist application component is exist
var ErrApplicationComponetExist = NewBcode(400, 10007, "application component is exist") var ErrApplicationComponentExist = NewBcode(400, 10007, "application component is exist")
// ErrApplicationComponetNotExist application component is not exist // ErrApplicationComponentNotExist application component is not exist
var ErrApplicationComponetNotExist = NewBcode(404, 10008, "application component is not exist") var ErrApplicationComponentNotExist = NewBcode(404, 10008, "application component is not exist")
// ErrApplicationPolicyExist application policy is exist // ErrApplicationPolicyExist application policy is exist
var ErrApplicationPolicyExist = NewBcode(400, 10009, "application policy is exist") var ErrApplicationPolicyExist = NewBcode(400, 10009, "application policy is exist")
@@ -76,7 +76,7 @@ var ErrApplicationRevisionNotExist = NewBcode(404, 10018, "application revision
// ErrApplicationRefusedDelete means the application cannot be deleted because it has been deployed // ErrApplicationRefusedDelete means the application cannot be deleted because it has been deployed
var ErrApplicationRefusedDelete = NewBcode(400, 10019, "The application cannot be deleted because it has been deployed") var ErrApplicationRefusedDelete = NewBcode(400, 10019, "The application cannot be deleted because it has been deployed")
// ErrApplicationEnvRefusedDelete means he application env cannot be deleted because it has been deployed // ErrApplicationEnvRefusedDelete means the application env cannot be deleted because it has been deployed
var ErrApplicationEnvRefusedDelete = NewBcode(400, 10020, "The application envbinding cannot be deleted because it has been deployed") var ErrApplicationEnvRefusedDelete = NewBcode(400, 10020, "The application envbinding cannot be deleted because it has been deployed")
// ErrInvalidWebhookToken means the webhook token is invalid // ErrInvalidWebhookToken means the webhook token is invalid
@@ -91,5 +91,5 @@ var ErrInvalidWebhookPayloadBody = NewBcode(400, 10023, "Invalid webhook payload
// ErrApplicationTriggerNotExist means application trigger is not exist // ErrApplicationTriggerNotExist means application trigger is not exist
var ErrApplicationTriggerNotExist = NewBcode(404, 10024, "application trigger is not exist") var ErrApplicationTriggerNotExist = NewBcode(404, 10024, "application trigger is not exist")
// ErrApplicationComponetNotAllowDelete means the component is main in one application, and it must be deleted before delete app. // ErrApplicationComponentNotAllowDelete means the component is main in one application, and it must be deleted before delete app.
var ErrApplicationComponetNotAllowDelete = NewBcode(400, 10025, "main component in application can not be deleted") var ErrApplicationComponentNotAllowDelete = NewBcode(400, 10025, "main component in application can not be deleted")
+102
View File
@@ -0,0 +1,102 @@
/*
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"
"errors"
"strconv"
"strings"
"github.com/sirupsen/logrus"
"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"
)
type cached struct {
generation int64
targets int64
}
// 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) {
return nil
}
return err
}
for _, appR := range appsRaw {
app, ok := appR.(*model.Application)
if !ok {
continue
}
gen, ok := app.Labels[model.LabelSyncGeneration]
if !ok || gen == "" {
continue
}
namespace := app.Labels[model.LabelSyncNamespace]
var key = formatAppComposedName(app.Name, namespace)
if strings.HasSuffix(app.Name, namespace) {
key = app.Name
}
generation, _ := strconv.ParseInt(gen, 10, 64)
// we should check targets if we synced from app status
c.updateCache(key, generation, 0)
}
return nil
}
func (c *CR2UX) shouldSync(ctx context.Context, targetApp *v1beta1.Application, del bool) bool {
key := formatAppComposedName(targetApp.Name, targetApp.Namespace)
cachedData, ok := c.cache.Load(key)
if ok {
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 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)
switch sot {
case FromUX, FromInner:
// we don't sync if the application is not created from CR
return false
default:
}
return true
}
func (c *CR2UX) updateCache(key string, generation, targets int64) {
// update cache
c.cache.Store(key, &cached{generation: generation, targets: targets})
}
+230
View File
@@ -0,0 +1,230 @@
/*
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"
"strconv"
"strings"
"time"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/workflow/step"
)
// ConvertFromCRComponent concerts Application CR Component object into velaux data store component
func ConvertFromCRComponent(appPrimaryKey string, component common.ApplicationComponent) (model.ApplicationComponent, error) {
bc := model.ApplicationComponent{
AppPrimaryKey: appPrimaryKey,
Name: component.Name,
Type: component.Type,
ExternalRevision: component.ExternalRevision,
DependsOn: component.DependsOn,
Inputs: component.Inputs,
Outputs: component.Outputs,
Scopes: component.Scopes,
Creator: model.AutoGenComp,
}
if component.Properties != nil {
properties, err := model.NewJSONStruct(component.Properties)
if err != nil {
return bc, err
}
bc.Properties = properties
}
for _, trait := range component.Traits {
properties, err := model.NewJSONStruct(trait.Properties)
if err != nil {
return bc, err
}
bc.Traits = append(bc.Traits, model.ApplicationTrait{CreateTime: time.Now(), UpdateTime: time.Now(), Properties: properties, Type: trait.Type, Alias: trait.Type, Description: "auto gen"})
}
return bc, nil
}
// ConvertFromCRPolicy converts Application CR Policy object into velaux data store policy
func ConvertFromCRPolicy(appPrimaryKey string, policyCR v1beta1.AppPolicy, creator string) (model.ApplicationPolicy, error) {
plc := model.ApplicationPolicy{
AppPrimaryKey: appPrimaryKey,
Name: policyCR.Name,
Type: policyCR.Type,
Creator: creator,
}
if policyCR.Properties != nil {
properties, err := model.NewJSONStruct(policyCR.Properties)
if err != nil {
return plc, err
}
plc.Properties = properties
}
return plc, nil
}
// ConvertFromCRWorkflow converts Application CR Workflow section into velaux data store workflow
func ConvertFromCRWorkflow(ctx context.Context, cli client.Client, appPrimaryKey string, app *v1beta1.Application) (model.Workflow, []v1beta1.WorkflowStep, error) {
dataWf := model.Workflow{
AppPrimaryKey: 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",
}
if app.Spec.Workflow == nil {
return dataWf, nil, nil
}
var steps []v1beta1.WorkflowStep
if app.Spec.Workflow.Ref != "" {
dataWf.Name = app.Spec.Workflow.Ref
wf := &v1alpha1.Workflow{}
if err := cli.Get(ctx, types.NamespacedName{Namespace: app.GetNamespace(), Name: app.Spec.Workflow.Ref}, wf); err != nil {
return dataWf, nil, err
}
steps = wf.Steps
} else {
steps = app.Spec.Workflow.Steps
}
for _, s := range steps {
if s.Properties == nil {
continue
}
properties, err := model.NewJSONStruct(s.Properties)
if err != nil {
return dataWf, nil, err
}
dataWf.Steps = append(dataWf.Steps, model.WorkflowStep{
Name: s.Name,
Type: s.Type,
Inputs: s.Inputs,
Outputs: s.Outputs,
DependsOn: s.DependsOn,
Properties: properties,
})
}
return dataWf, steps, nil
}
// ConvertFromCRTargets converts deployed Cluster/Namespace from Application CR Status into velaux data store
func ConvertFromCRTargets(targetApp *v1beta1.Application) []*model.Target {
var targets []*model.Target
nc := make(map[string]struct{})
for _, v := range targetApp.Status.AppliedResources {
var cluster = v.Cluster
if cluster == "" {
cluster = multicluster.ClusterLocalName
}
name := model.AutoGenTargetNamePrefix + cluster + "-" + v.Namespace
if _, ok := nc[name]; ok {
continue
}
nc[name] = struct{}{}
targets = append(targets, &model.Target{
Name: name,
Cluster: &model.ClusterTarget{
ClusterName: cluster,
Namespace: v.Namespace,
},
})
}
return targets
}
// ConvertApp2DatastoreApp will convert Application CR to datastore application related resources
func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1.Application) (*model.DataStoreApp, error) {
cli := c.cli
appName := c.getAppMetaName(ctx, targetApp.Name, targetApp.Namespace)
project := model.DefaultInitName
if _, ok := targetApp.Labels[oam.LabelAddonName]; ok && strings.HasPrefix(targetApp.Name, "addon-") {
project = model.DefaultAddonProject
}
appMeta := &model.Application{
Name: appName,
Description: model.AutoGenDesc,
Alias: targetApp.Name,
Project: project,
Labels: map[string]string{
model.LabelSyncNamespace: targetApp.Namespace,
model.LabelSyncGeneration: strconv.FormatInt(targetApp.Generation, 10),
},
}
// 1. convert app meta and env
dsApp := &model.DataStoreApp{
AppMeta: appMeta,
Env: &model.Env{
Name: model.AutoGenEnvNamePrefix + targetApp.Namespace,
Namespace: targetApp.Namespace,
Description: model.AutoGenDesc,
Project: project,
Alias: "Synced",
},
Eb: &model.EnvBinding{
AppPrimaryKey: appMeta.PrimaryKey(),
Name: model.AutoGenEnvNamePrefix + targetApp.Namespace,
AppDeployName: appMeta.GetAppNameForSynced(),
},
}
// 2. convert component and trait
for _, cmp := range targetApp.Spec.Components {
compModel, err := ConvertFromCRComponent(appMeta.PrimaryKey(), cmp)
if err != nil {
return nil, err
}
dsApp.Comps = append(dsApp.Comps, &compModel)
}
// 3. convert workflow
wf, steps, err := ConvertFromCRWorkflow(ctx, cli, appMeta.PrimaryKey(), targetApp)
if err != nil {
return nil, err
}
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
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 err != nil {
return nil, err
}
dsApp.Policies = append(dsApp.Policies, &plcModel)
}
// TODO(wonderflow): we don't sync targets as it can't be judged well in velaux env
// if we want to sync, we can extract targets from status, like below:
/*
dsApp.Targets = ConvertFromCRTargets(targetApp)
for _, t := range dsApp.Targets {
dsApp.Env.Targets = append(dsApp.Env.Targets, t.Name)
}
*/
return dsApp, nil
}
+198
View File
@@ -0,0 +1,198 @@
/*
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"
"sigs.k8s.io/controller-runtime/pkg/client"
"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/log"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
"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 _, appName := targetApp.Annotations[oam.AnnotationAppName]; appName {
return FromUX
}
return 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 {
app := &model.Application{Name: formatAppComposedName(appName, namespace)}
err := ds.Get(ctx, app)
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]
}
// CR2UX provides the Add/Update/Delete method
type CR2UX struct {
ds datastore.DataStore
cli client.Client
cache sync.Map
}
func formatAppComposedName(name, namespace string) string {
return name + "-" + namespace
}
// 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
}
// 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
}
dsApp, err := c.ConvertApp2DatastoreApp(ctx, targetApp)
if err != nil {
log.Logger.Errorf("Convert App to data store err %v", err)
return err
}
if err = StoreProject(ctx, dsApp.AppMeta.Project, ds); err != nil {
log.Logger.Errorf("get or create project for sync process err %v", err)
return err
}
if err = StoreEnv(ctx, dsApp, ds); err != nil {
log.Logger.Errorf("Store Env Metadata to data store err %v", err)
return err
}
if err = StoreEnvBinding(ctx, dsApp.Eb, ds); err != nil {
log.Logger.Errorf("Store EnvBinding Metadata to data store err %v", err)
return err
}
if err = StoreComponents(ctx, dsApp.AppMeta.Name, dsApp.Comps, ds); err != nil {
log.Logger.Errorf("Store Components Metadata to data store err %v", err)
return err
}
if err = StorePolicy(ctx, dsApp.AppMeta.Name, dsApp.Policies, ds); err != nil {
log.Logger.Errorf("Store Policy Metadata to data store err %v", err)
return err
}
if err = StoreWorkflow(ctx, dsApp, ds); err != nil {
log.Logger.Errorf("Store Workflow Metadata to data store err %v", err)
return err
}
/*
if err = StoreTargets(ctx, dsApp, ds); err != nil {
log.Logger.Errorf("Store targets to data store err %v", err)
return err
}
*/
if err = StoreAppMeta(ctx, dsApp, ds); err != nil {
log.Logger.Errorf("Store App Metadata to data store err %v", err)
return err
}
// update cache
c.updateCache(dsApp.AppMeta.PrimaryKey(), targetApp.Generation, int64(len(dsApp.Targets)))
return nil
}
// DeleteApp will delete the application as the CR was deleted
func (c *CR2UX) DeleteApp(ctx context.Context, targetApp *v1beta1.Application) error {
ds := c.ds
if !c.shouldSync(ctx, targetApp, true) {
return nil
}
appName := c.getAppMetaName(ctx, targetApp.Name, targetApp.Namespace)
_ = ds.Delete(ctx, &model.Application{Name: appName})
cmps, err := ds.List(ctx, &model.ApplicationComponent{AppPrimaryKey: appName}, &datastore.ListOptions{})
if err != nil {
return err
}
for _, entity := range cmps {
comp := entity.(*model.ApplicationComponent)
if comp.Creator == model.AutoGenComp {
_ = ds.Delete(ctx, comp)
}
}
plcs, err := ds.List(ctx, &model.ApplicationPolicy{AppPrimaryKey: appName}, &datastore.ListOptions{})
if err != nil {
return err
}
for _, entity := range plcs {
comp := entity.(*model.ApplicationPolicy)
if comp.Creator == model.AutoGenPolicy {
_ = ds.Delete(ctx, comp)
}
}
_ = ds.Delete(ctx, &model.Workflow{Name: model.AutoGenWorkflowNamePrefix + appName})
return nil
}
+230
View File
@@ -0,0 +1,230 @@
/*
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"
"errors"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
"github.com/oam-dev/kubevela/pkg/utils"
)
// StoreProject will create project for synced application
func StoreProject(ctx context.Context, name string, ds datastore.DataStore) error {
err := ds.Get(ctx, &model.Project{Name: name})
if err == nil {
// it means the record already exists, don't need to add anything
return nil
}
if !errors.Is(err, datastore.ErrRecordNotExist) {
// other database error, return it
return err
}
proj := &model.Project{
Name: name,
Description: model.AutoGenProj,
}
return ds.Add(ctx, proj)
}
// StoreAppMeta will sync application metadata from CR to datastore
func StoreAppMeta(ctx context.Context, app *model.DataStoreApp, ds datastore.DataStore) error {
err := ds.Get(ctx, &model.Application{Name: app.AppMeta.Name})
if err == nil {
// it means the record already exists
return ds.Put(ctx, app.AppMeta)
}
if !errors.Is(err, datastore.ErrRecordNotExist) {
// other database error, return it
return err
}
return ds.Add(ctx, app.AppMeta)
}
// StoreEnv will sync application namespace from CR to datastore env, one namespace belongs to one env
func StoreEnv(ctx context.Context, app *model.DataStoreApp, ds datastore.DataStore) error {
curEnv := &model.Env{Name: app.Env.Name}
err := ds.Get(ctx, curEnv)
if err == nil {
// it means the record already exists, compare the targets
if utils.EqualSlice(curEnv.Targets, app.Env.Targets) {
return nil
}
return ds.Put(ctx, app.Env)
}
if !errors.Is(err, datastore.ErrRecordNotExist) {
// other database error, return it
return err
}
return ds.Add(ctx, app.Env)
}
// StoreEnvBinding will add envbinding for application CR one application one envbinding
func StoreEnvBinding(ctx context.Context, eb *model.EnvBinding, ds datastore.DataStore) error {
err := ds.Get(ctx, eb)
if err == nil {
// it means the record already exists, don't need to add anything
return nil
}
if !errors.Is(err, datastore.ErrRecordNotExist) {
// other database error, return it
return err
}
return ds.Add(ctx, eb)
}
// StoreComponents will sync application components from CR to datastore
func StoreComponents(ctx context.Context, appPrimaryKey string, expComps []*model.ApplicationComponent, ds datastore.DataStore) error {
// list the existing components in datastore
originComps, err := ds.List(ctx, &model.ApplicationComponent{AppPrimaryKey: appPrimaryKey}, &datastore.ListOptions{})
if err != nil {
return err
}
var originCompNames []string
for _, entity := range originComps {
comp := entity.(*model.ApplicationComponent)
originCompNames = append(originCompNames, comp.Name)
}
var targetCompNames []string
for _, comp := range expComps {
targetCompNames = append(targetCompNames, comp.Name)
}
_, readyToDelete, readyToAdd := utils.ThreeWaySliceCompare(originCompNames, targetCompNames)
// delete the components that not belongs to the new app
for _, entity := range originComps {
comp := entity.(*model.ApplicationComponent)
// we only compare for components that automatically generated by sync process.
if comp.Creator != model.AutoGenComp {
continue
}
if !utils.StringsContain(readyToDelete, comp.Name) {
continue
}
if err := ds.Delete(ctx, comp); err != nil {
if errors.Is(err, datastore.ErrRecordNotExist) {
continue
}
log.Logger.Warnf("delete comp %s for app %s failure %s", comp.Name, appPrimaryKey, err.Error())
}
}
// add or update new app's components for datastore
for _, comp := range expComps {
if utils.StringsContain(readyToAdd, comp.Name) {
err = ds.Add(ctx, comp)
} else {
err = ds.Put(ctx, comp)
}
if err != nil {
log.Logger.Warnf("convert comp %s for app %s into datastore failure %s", comp.Name, utils.Sanitize(appPrimaryKey), err.Error())
return err
}
}
return nil
}
// StorePolicy will add/update/delete policies, we don't delete ref policy
func StorePolicy(ctx context.Context, appPrimaryKey string, expPolicies []*model.ApplicationPolicy, ds datastore.DataStore) error {
// list the existing policies for this app in datastore
originPolicies, err := ds.List(ctx, &model.ApplicationPolicy{AppPrimaryKey: appPrimaryKey}, &datastore.ListOptions{})
if err != nil {
return err
}
var originPolicyNames []string
for _, entity := range originPolicies {
plc := entity.(*model.ApplicationPolicy)
originPolicyNames = append(originPolicyNames, plc.Name)
}
var targetPLCNames []string
for _, plc := range expPolicies {
targetPLCNames = append(targetPLCNames, plc.Name)
}
_, readyToDelete, readyToAdd := utils.ThreeWaySliceCompare(originPolicyNames, targetPLCNames)
// 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.
if plc.Creator != model.AutoGenPolicy {
continue
}
if !utils.StringsContain(readyToDelete, plc.Name) {
continue
}
if err := ds.Delete(ctx, plc); err != nil {
if errors.Is(err, datastore.ErrRecordNotExist) {
continue
}
log.Logger.Warnf("delete policy %s for app %s failure %s", plc.Name, appPrimaryKey, err.Error())
}
}
// add or update new app's policies for datastore
for _, plc := range expPolicies {
if utils.StringsContain(readyToAdd, plc.Name) {
err = ds.Add(ctx, plc)
} else {
err = ds.Put(ctx, plc)
}
if err != nil {
log.Logger.Warnf("convert policy %s for app %s into datastore failure %s", plc.Name, utils.Sanitize(appPrimaryKey), err.Error())
return err
}
}
return nil
}
// StoreWorkflow will sync workflow application CR to datastore, it updates the only one workflow from the application with specified name
func StoreWorkflow(ctx context.Context, dsApp *model.DataStoreApp, ds datastore.DataStore) error {
err := ds.Get(ctx, &model.Workflow{AppPrimaryKey: dsApp.AppMeta.Name, Name: dsApp.Workflow.Name})
if err == nil {
// it means the record already exists, update it
return ds.Put(ctx, dsApp.Workflow)
}
if !errors.Is(err, datastore.ErrRecordNotExist) {
// other database error, return it
return err
}
return ds.Add(ctx, dsApp.Workflow)
}
// StoreTargets will sync targets from application CR to datastore
func StoreTargets(ctx context.Context, dsApp *model.DataStoreApp, ds datastore.DataStore) error {
for _, t := range dsApp.Targets {
err := ds.Get(ctx, t)
if err == nil {
continue
}
if !errors.Is(err, datastore.ErrRecordNotExist) {
// other database error, return it
return err
}
if err = ds.Add(ctx, t); err != nil {
return err
}
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
/*
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"
"math/rand"
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore/kubeapi"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore/mongodb"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
func TestSync(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Sync Suite Test")
}
var _ = BeforeSuite(func(done Done) {
rand.Seed(time.Now().UnixNano())
By("bootstrapping Sync test environment")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
CRDDirectoryPaths: []string{"../../../charts/vela-core/crds"},
}
By("start kube test env")
var err error
cfg, err = testEnv.Start()
Expect(err).ShouldNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
By("new kube client")
cfg.Timeout = time.Minute * 2
k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme})
Expect(err).Should(BeNil())
Expect(k8sClient).ToNot(BeNil())
clients.SetKubeClient(k8sClient)
By("new kube client success")
clients.SetKubeClient(k8sClient)
Expect(err).Should(BeNil())
close(done)
}, 240)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
func NewDatastore(cfg datastore.Config) (ds datastore.DataStore, err error) {
switch cfg.Type {
case "mongodb":
ds, err = mongodb.New(context.Background(), cfg)
if err != nil {
return nil, fmt.Errorf("create mongodb datastore instance failure %w", err)
}
case "kubeapi":
ds, err = kubeapi.New(context.Background(), cfg)
if err != nil {
return nil, fmt.Errorf("create mongodb datastore instance failure %w", err)
}
default:
return nil, fmt.Errorf("not support datastore type %s", cfg.Type)
}
return ds, nil
}
+37
View File
@@ -0,0 +1,37 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: example
spec:
components:
- name: nginx
type: webservice
properties:
image: nginx
traits:
- type: gateway
properties:
domain: testsvc.example.com
http:
"/": 8000
- name: nginx2
type: webservice
properties:
image: nginx2
policies:
- name: topology-beijing-demo
type: topology
properties:
clusterLabelSelector:
region: beijing
namespace: demo
- name: topology-local
type: topology
properties:
targets: ["local/demo", "local/ackone-demo"]
workflow:
steps:
- type: deploy
name: deploy-local
properties:
policies: ["topology-local", "topology-beijing-demo"]
+10
View File
@@ -0,0 +1,10 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: example
spec:
components:
- name: blog
type: webservice
properties:
image: wordpress
+33
View File
@@ -0,0 +1,33 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: example
spec:
components:
- name: blog
type: webservice
properties:
image: wordpress
traits:
- type: gateway
properties:
domain: testsvc.example.com
http:
"/": 8000
- name: nginx2
type: webservice
properties:
image: nginx
policies:
- name: topology-beijing-demo
type: topology
properties:
clusterLabelSelector:
region: beijing
namespace: demo
workflow:
steps:
- type: deploy
name: deploy-local
properties:
policies: ["topology-beijing-demo"]
+103
View File
@@ -0,0 +1,103 @@
/*
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"
"encoding/json"
"sync"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
)
// Start prepares watchers and run their controllers, then waits for process termination signals
func Start(ctx context.Context, ds datastore.DataStore, cfg *rest.Config) {
k8sClient, err := clients.GetKubeClient()
if err != nil {
logrus.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(cfg)
if err != nil {
logrus.Fatal(err)
}
f := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, 0, v1.NamespaceAll, nil)
startAppSyncing(ctx, f, ds, k8sClient)
}
func startAppSyncing(ctx context.Context, factory dynamicinformer.DynamicSharedInformerFactory, ds datastore.DataStore, cli client.Client) {
var err error
informer := factory.ForResource(v1beta1.SchemeGroupVersion.WithResource("applications")).Informer()
getApp := func(obj interface{}) *v1beta1.Application {
app := &v1beta1.Application{}
bs, _ := json.Marshal(obj)
_ = json.Unmarshal(bs, app)
return app
}
cu := &CR2UX{
ds: ds,
cli: cli,
cache: sync.Map{},
}
if err = cu.InitCache(ctx); err != nil {
klog.Fatal("sync app init err", err)
}
handlers := cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
app := getApp(obj)
klog.Infof("watched add app event, namespace: %s, name: %s", app.Namespace, app.Name)
err = cu.AddOrUpdate(ctx, app)
if err != nil {
logrus.Errorf("Application %-30s Create Sync to db err %v", color.WhiteString(app.Namespace+"/"+app.Name), err)
}
},
UpdateFunc: func(oldObj, obj interface{}) {
app := getApp(obj)
klog.Infof("watched update app event, namespace: %s, name: %s", app.Namespace, app.Name)
err = cu.AddOrUpdate(ctx, app)
if err != nil {
klog.Errorf("Application %-30s Update Sync to db err %v", color.WhiteString(app.Namespace+"/"+app.Name), err)
}
},
DeleteFunc: func(obj interface{}) {
app := getApp(obj)
klog.Infof("watched delete app event, namespace: %s, name: %s", app.Namespace, app.Name)
err = cu.DeleteApp(ctx, app)
if err != nil {
klog.Errorf("Application %-30s Deleted Sync to db err %v", color.WhiteString(app.Namespace+"/"+app.Name), err)
}
},
}
informer.AddEventHandler(handlers)
klog.Info("app syncing started")
informer.Run(ctx.Done())
}
+127
View File
@@ -0,0 +1,127 @@
/*
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"
"time"
. "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 Worker CR sync to datastore", func() {
BeforeEach(func() {
})
It("Test app sync test-app1", func() {
By("Preparing database")
dbNamespace := "sync-db-ns1-test"
appNS1 := "sync-worker-test-ns1"
appNS2 := "sync-worker-test-ns2"
ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: "sync-test-db1"})
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{}))
ns.Name = appNS2
ns.ResourceVersion = ""
err = k8sClient.Create(context.TODO(), &ns)
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
By("Start syncing")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go Start(ctx, ds, cfg)
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(k8sClient.Create(context.TODO(), app1)).Should(BeNil())
Eventually(func() error {
appm := model.Application{Name: app1.Name}
return ds.Get(ctx, &appm)
}, 10*time.Second, 100*time.Millisecond).Should(BeNil())
comp1 := model.ApplicationComponent{AppPrimaryKey: app1.Name, Name: "nginx"}
Expect(ds.Get(ctx, &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())
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(k8sClient.Create(context.TODO(), app2)).Should(BeNil())
Eventually(func() error {
appm := model.Application{Name: formatAppComposedName(app2.Name, app2.Namespace)}
return ds.Get(ctx, &appm)
}, 10*time.Second, 100*time.Millisecond).Should(BeNil())
By("delete test app1 and check the syncing results")
Expect(k8sClient.Delete(context.TODO(), app1)).Should(BeNil())
Eventually(func() error {
appm := model.Application{Name: app1.Name}
return ds.Get(ctx, &appm)
}, 10*time.Second, 100*time.Millisecond).Should(BeEquivalentTo(datastore.ErrRecordNotExist))
By("update test app2 and check the syncing results")
newapp2 := &v1beta1.Application{}
Expect(common2.ReadYamlToObject("testdata/test-app3.yaml", newapp2)).Should(BeNil())
app2.Spec = newapp2.Spec
Expect(k8sClient.Update(context.TODO(), app2)).Should(BeNil())
Eventually(func() error {
appm := model.ApplicationComponent{AppPrimaryKey: formatAppComposedName(app2.Name, app2.Namespace), Name: "nginx2"}
return ds.Get(ctx, &appm)
}, 10*time.Second, 100*time.Millisecond).Should(BeNil())
})
})
+4
View File
@@ -172,6 +172,7 @@ const (
AnnotationWorkflowName = "app.oam.dev/workflowName" AnnotationWorkflowName = "app.oam.dev/workflowName"
// AnnotationAppName specifies the name for application in db. // AnnotationAppName specifies the name for application in db.
// Note: the annotation is only created by velaUX, please don't use it in other Source of Truth.
AnnotationAppName = "app.oam.dev/appName" AnnotationAppName = "app.oam.dev/appName"
// AnnotationAppAlias specifies the alias for application in db. // AnnotationAppAlias specifies the alias for application in db.
@@ -189,4 +190,7 @@ const (
// AnnotationServiceAccountName indicates the name of the ServiceAccount to use to apply Components and run Workflow. // AnnotationServiceAccountName indicates the name of the ServiceAccount to use to apply Components and run Workflow.
// ServiceAccount will be used in the local cluster only. // ServiceAccount will be used in the local cluster only.
AnnotationServiceAccountName = "app.oam.dev/service-account-name" 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"
) )
+38
View File
@@ -16,6 +16,11 @@ limitations under the License.
package utils package utils
import (
"reflect"
"sort"
)
// StringsContain strings contain // StringsContain strings contain
func StringsContain(items []string, source string) bool { func StringsContain(items []string, source string) bool {
for _, item := range items { for _, item := range items {
@@ -25,3 +30,36 @@ func StringsContain(items []string, source string) bool {
} }
return false return false
} }
// ThreeWaySliceCompare will compare two string slice, with the three return values: [both in A and B], [only in A], [only in B]
func ThreeWaySliceCompare(a []string, b []string) ([]string, []string, []string) {
m := make(map[string]struct{})
for _, k := range b {
m[k] = struct{}{}
}
var AB, AO, BO []string
for _, k := range a {
_, ok := m[k]
if !ok {
AO = append(AO, k)
continue
}
AB = append(AB, k)
delete(m, k)
}
for k := range m {
BO = append(BO, k)
}
sort.Strings(AB)
sort.Strings(AO)
sort.Strings(BO)
return AB, AO, BO
}
// EqualSlice checks if two slice are equal
func EqualSlice(a, b []string) bool {
sort.Strings(a)
sort.Strings(b)
return reflect.DeepEqual(a, b)
}
+79
View File
@@ -0,0 +1,79 @@
/*
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 utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCompareSlice(t *testing.T) {
caseA := []string{"c", "b", "a"}
caseB := []string{"c", "b", "a"}
gotab, gotao, gotbo := ThreeWaySliceCompare(caseA, caseB)
assert.Equal(t, gotab, []string{"a", "b", "c"})
assert.Equal(t, gotao, []string(nil))
assert.Equal(t, gotbo, []string(nil))
caseA = []string{"c", "b"}
caseB = []string{"c", "a"}
gotab, gotao, gotbo = ThreeWaySliceCompare(caseA, caseB)
assert.Equal(t, gotab, []string{"c"})
assert.Equal(t, gotao, []string{"b"})
assert.Equal(t, gotbo, []string{"a"})
caseA = []string{"c", "b"}
caseB = nil
gotab, gotao, gotbo = ThreeWaySliceCompare(caseA, caseB)
assert.Equal(t, gotab, []string(nil))
assert.Equal(t, gotao, []string{"b", "c"})
assert.Equal(t, gotbo, []string(nil))
caseA = nil
caseB = []string{"c", "b"}
gotab, gotao, gotbo = ThreeWaySliceCompare(caseA, caseB)
assert.Equal(t, gotab, []string(nil))
assert.Equal(t, gotao, []string(nil))
assert.Equal(t, gotbo, []string{"b", "c"})
}
func TestEqual(t *testing.T) {
caseA := []string{"c", "b", "a"}
caseB := []string{"c", "b", "a"}
assert.Equal(t, EqualSlice(caseA, caseB), true)
caseA = []string{"c", "a", "b"}
caseB = []string{"c", "b", "a"}
assert.Equal(t, EqualSlice(caseA, caseB), true)
caseA = []string{"c", "a", "b"}
caseB = []string{"b", "a", "c"}
assert.Equal(t, EqualSlice(caseA, caseB), true)
caseA = []string{"c", "a", "b"}
caseB = []string{"b", "a", "c", "d"}
assert.Equal(t, EqualSlice(caseA, caseB), false)
caseA = []string{}
caseB = []string{}
assert.Equal(t, EqualSlice(caseA, caseB), true)
caseA = []string{}
caseB = []string{"b", "a", "c"}
assert.Equal(t, EqualSlice(caseA, caseB), false)
}
+3
View File
@@ -144,6 +144,9 @@ func (c *AppCollector) FindResourceFromResourceTrackerSpec(app *v1beta1.Applicat
} }
return nil, err return nil, err
} }
if objRef.Cluster == "" {
objRef.Cluster = multicluster.ClusterLocalName
}
resources = append(resources, Resource{ resources = append(resources, Resource{
Cluster: objRef.Cluster, Cluster: objRef.Cluster,
Revision: obj.GetLabels()[oam.LabelAppRevision], Revision: obj.GetLabels()[oam.LabelAppRevision],