mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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."
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: core.oam.dev/v1beta1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: example
|
||||||
|
spec:
|
||||||
|
components:
|
||||||
|
- name: blog
|
||||||
|
type: webservice
|
||||||
|
properties:
|
||||||
|
image: wordpress
|
||||||
+33
@@ -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"]
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user