refactor: parse trait & scope (#6017)

Signed-off-by: yyzxw <1020938856@qq.com>
This commit is contained in:
yyzxw 2023-05-22 19:21:01 +08:00 committed by GitHub
parent 338703baf5
commit 3cb0f7b330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 431 additions and 28 deletions

View File

@ -96,7 +96,7 @@ type Workload struct {
SkipApplyWorkload bool
}
// EvalContext eval workload template and set result to context
// EvalContext eval workload template and set the result to context
func (wl *Workload) EvalContext(ctx process.Context) error {
return wl.engine.Complete(ctx, wl.FullTemplate.TemplateStr, wl.Params)
}
@ -125,7 +125,7 @@ func (wl *Workload) EvalStatus(templateContext map[string]interface{}) (string,
// EvalHealth eval workload health check
func (wl *Workload) EvalHealth(templateContext map[string]interface{}) (bool, error) {
// if health of template is not set or standard workload is managed by trait always return true
// if the health of template is not set or standard workload is managed by trait always return true
if wl.SkipApplyWorkload {
return true, nil
}

View File

@ -582,22 +582,38 @@ func (p *Parser) parseWorkload(ctx context.Context, comp common.ApplicationCompo
}
workload.ExternalRevision = comp.ExternalRevision
if err = p.parseTraits(ctx, workload, comp); err != nil {
return nil, err
}
if err = p.parseScopes(ctx, workload, comp); err != nil {
return nil, err
}
return workload, nil
}
func (p *Parser) parseTraits(ctx context.Context, workload *Workload, comp common.ApplicationComponent) error {
for _, traitValue := range comp.Traits {
properties, err := util.RawExtension2Map(traitValue.Properties)
if err != nil {
return nil, errors.Errorf("fail to parse properties of %s for %s", traitValue.Type, comp.Name)
return errors.Errorf("fail to parse properties of %s for %s", traitValue.Type, comp.Name)
}
trait, err := p.parseTrait(ctx, traitValue.Type, properties)
if err != nil {
return nil, errors.WithMessagef(err, "component(%s) parse trait(%s)", comp.Name, traitValue.Type)
return errors.WithMessagef(err, "component(%s) parse trait(%s)", comp.Name, traitValue.Type)
}
workload.Traits = append(workload.Traits, trait)
}
return nil
}
func (p *Parser) parseScopes(ctx context.Context, workload *Workload, comp common.ApplicationComponent) error {
for scopeType, instanceName := range comp.Scopes {
sd, gvk, err := GetScopeDefAndGVK(ctx, p.client, p.dm, scopeType)
if err != nil {
return nil, err
return err
}
workload.Scopes = append(workload.Scopes, Scope{
Name: instanceName,
@ -606,7 +622,7 @@ func (p *Parser) parseWorkload(ctx context.Context, comp common.ApplicationCompo
})
workload.ScopeDefinition = append(workload.ScopeDefinition, sd)
}
return workload, nil
return nil
}
// ParseWorkloadFromRevision resolve an ApplicationComponent and generate a Workload
@ -618,22 +634,38 @@ func (p *Parser) ParseWorkloadFromRevision(comp common.ApplicationComponent, app
}
workload.ExternalRevision = comp.ExternalRevision
if err = p.parseTraitsFromRevision(comp, appRev, workload); err != nil {
return nil, err
}
if err = p.parseScopesFromRevision(comp, appRev, workload); err != nil {
return nil, err
}
return workload, nil
}
func (p *Parser) parseTraitsFromRevision(comp common.ApplicationComponent, appRev *v1beta1.ApplicationRevision, workload *Workload) error {
for _, traitValue := range comp.Traits {
properties, err := util.RawExtension2Map(traitValue.Properties)
if err != nil {
return nil, errors.Errorf("fail to parse properties of %s for %s", traitValue.Type, comp.Name)
return errors.Errorf("fail to parse properties of %s for %s", traitValue.Type, comp.Name)
}
trait, err := p.parseTraitFromRevision(traitValue.Type, properties, appRev)
if err != nil {
return nil, errors.WithMessagef(err, "component(%s) parse trait(%s)", comp.Name, traitValue.Type)
return errors.WithMessagef(err, "component(%s) parse trait(%s)", comp.Name, traitValue.Type)
}
workload.Traits = append(workload.Traits, trait)
}
return nil
}
func (p *Parser) parseScopesFromRevision(comp common.ApplicationComponent, appRev *v1beta1.ApplicationRevision, workload *Workload) error {
for scopeType, instanceName := range comp.Scopes {
sd, gvk, err := GetScopeDefAndGVKFromRevision(scopeType, appRev)
if err != nil {
return nil, err
return err
}
workload.Scopes = append(workload.Scopes, Scope{
Name: instanceName,
@ -642,7 +674,7 @@ func (p *Parser) ParseWorkloadFromRevision(comp common.ApplicationComponent, app
})
workload.ScopeDefinition = append(workload.ScopeDefinition, sd)
}
return workload, nil
return nil
}
// ParseWorkloadFromRevisionAndClient resolve an ApplicationComponent and generate a Workload
@ -728,7 +760,7 @@ func (p *Parser) convertTemplate2Trait(name string, properties map[string]interf
}, nil
}
// ValidateComponentNames validate all component name whether repeat in cluster and template
// ValidateComponentNames validate all component names whether repeat in cluster and template
func (p *Parser) ValidateComponentNames(app *v1beta1.Application) (int, error) {
compNames := map[string]struct{}{}
for idx, comp := range app.Spec.Components {
@ -740,7 +772,7 @@ func (p *Parser) ValidateComponentNames(app *v1beta1.Application) (int, error) {
return 0, nil
}
// GetScopeDefAndGVK get grouped API version of the given scope
// GetScopeDefAndGVK get grouped an API version of the given scope
func GetScopeDefAndGVK(ctx context.Context, cli client.Reader, dm discoverymapper.DiscoveryMapper,
name string) (*v1beta1.ScopeDefinition, metav1.GroupVersionKind, error) {
var gvk metav1.GroupVersionKind

View File

@ -21,17 +21,24 @@ import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/crossplane/crossplane-runtime/pkg/test"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
errors2 "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam/util"
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
@ -234,7 +241,7 @@ var _ = Describe("Test application parser", func() {
err := yaml.Unmarshal([]byte(appfileYaml), &o)
Expect(err).ShouldNot(HaveOccurred())
// Create mock client
// Create a mock client
tclient := test.MockClient{
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
if strings.Contains(key.Name, "notexist") {
@ -499,3 +506,362 @@ patch: spec: replicas: parameter.replicas
})
})
})
func TestParser_parseTraits(t *testing.T) {
type args struct {
workload *Workload
comp common.ApplicationComponent
}
tests := []struct {
name string
args args
wantErr assert.ErrorAssertionFunc
mockTemplateLoaderFn TemplateLoaderFn
validateFunc func(w *Workload) bool
}{
{
name: "test empty traits",
args: args{
comp: common.ApplicationComponent{},
},
wantErr: assert.NoError,
},
{
name: "test parse trait properties error",
args: args{
comp: common.ApplicationComponent{
Traits: []common.ApplicationTrait{
{
Type: "expose",
Properties: &runtime.RawExtension{
Raw: []byte("invalid properties"),
},
},
},
},
},
wantErr: assert.Error,
},
{
name: "test parse trait error",
args: args{
comp: common.ApplicationComponent{
Traits: []common.ApplicationTrait{
{
Type: "expose",
Properties: &runtime.RawExtension{
Raw: []byte(`{"unsupported": "{\"key\":\"value\"}"}`),
},
},
},
},
},
mockTemplateLoaderFn: func(context.Context, discoverymapper.DiscoveryMapper, client.Reader, string, types.CapType) (*Template, error) {
return nil, fmt.Errorf("unsupported key not found")
},
wantErr: assert.Error,
},
{
name: "test parse trait success",
args: args{
comp: common.ApplicationComponent{
Traits: []common.ApplicationTrait{
{
Type: "expose",
Properties: &runtime.RawExtension{
Raw: []byte(`{"annotation": "{\"key\":\"value\"}"}`),
},
},
},
},
workload: &Workload{},
},
wantErr: assert.NoError,
mockTemplateLoaderFn: func(ctx context.Context, mapper discoverymapper.DiscoveryMapper, reader client.Reader, s string, capType types.CapType) (*Template, error) {
return &Template{
TemplateStr: "template",
CapabilityCategory: "network",
Health: "true",
CustomStatus: "healthy",
}, nil
},
validateFunc: func(w *Workload) bool {
return w != nil && len(w.Traits) != 0 && w.Traits[0].Name == "expose" && w.Traits[0].Template == "template"
},
},
}
p := NewApplicationParser(nil, dm, pd)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p.tmplLoader = tt.mockTemplateLoaderFn
err := p.parseTraits(context.Background(), tt.args.workload, tt.args.comp)
tt.wantErr(t, err, fmt.Sprintf("parseTraits(%v, %v)", tt.args.workload, tt.args.comp))
if tt.validateFunc != nil {
assert.True(t, tt.validateFunc(tt.args.workload))
}
})
}
}
func TestParser_parseScopes(t *testing.T) {
type args struct {
workload *Workload
comp common.ApplicationComponent
}
tests := []struct {
name string
args args
mockTemplateLoaderFn TemplateLoaderFn
mockGetFunc test.MockGetFn
wantErr assert.ErrorAssertionFunc
validateFunc func(w *Workload) bool
}{
{
name: "test empty scope",
args: args{
comp: common.ApplicationComponent{},
workload: &Workload{},
},
wantErr: assert.NoError,
validateFunc: func(w *Workload) bool {
return w != nil && len(w.Scopes) == 0
},
},
{
name: "test get gvk error",
args: args{
comp: common.ApplicationComponent{
Scopes: map[string]string{
"cluster1": "namespace1",
"cluster2": "namespace2",
},
},
workload: &Workload{},
},
mockGetFunc: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
return fmt.Errorf("not exist")
},
wantErr: assert.Error,
},
{
name: "test parse scopes success",
args: args{
comp: common.ApplicationComponent{
Scopes: map[string]string{
"cluster1": "namespace1",
"cluster2": "namespace2",
},
},
workload: &Workload{},
},
mockGetFunc: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
return nil
},
wantErr: assert.NoError,
validateFunc: func(w *Workload) bool {
return w != nil && len(w.Scopes) == 2 && w.Scopes[0].Name == "namespace1" && w.Scopes[1].Name == "namespace2"
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewApplicationParser(&test.MockClient{MockGet: tt.mockGetFunc}, dm, pd)
p.tmplLoader = tt.mockTemplateLoaderFn
err := p.parseScopes(context.Background(), tt.args.workload, tt.args.comp)
if !tt.wantErr(t, err, fmt.Sprintf("parseScopes(%v, %v)", tt.args.workload, tt.args.comp)) {
return
}
if tt.validateFunc != nil {
assert.True(t, tt.validateFunc(tt.args.workload))
}
})
}
}
func TestParser_parseTraitsFromRevision(t *testing.T) {
type args struct {
comp common.ApplicationComponent
appRev *v1beta1.ApplicationRevision
workload *Workload
}
tests := []struct {
name string
args args
validateFunc func(w *Workload) bool
wantErr assert.ErrorAssertionFunc
}{
{
name: "test empty traits",
args: args{
comp: common.ApplicationComponent{},
},
wantErr: assert.NoError,
},
{
name: "test parse traits properties error",
args: args{
comp: common.ApplicationComponent{
Traits: []common.ApplicationTrait{
{
Type: "expose",
Properties: &runtime.RawExtension{Raw: []byte("invalid")},
},
},
},
workload: &Workload{},
},
wantErr: assert.Error,
},
{
name: "test parse traits from revision failed",
args: args{
comp: common.ApplicationComponent{
Traits: []common.ApplicationTrait{
{
Type: "expose",
Properties: &runtime.RawExtension{Raw: []byte(`{"appRevisionName": "appRevName"}`)},
},
},
},
appRev: &v1beta1.ApplicationRevision{
Spec: v1beta1.ApplicationRevisionSpec{
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
TraitDefinitions: map[string]*v1beta1.TraitDefinition{},
},
},
},
workload: &Workload{},
},
wantErr: assert.Error,
},
{
name: "test parse traits from revision success",
args: args{
comp: common.ApplicationComponent{
Traits: []common.ApplicationTrait{
{
Type: "expose",
Properties: &runtime.RawExtension{Raw: []byte(`{"appRevisionName": "appRevName"}`)},
},
},
},
appRev: &v1beta1.ApplicationRevision{
Spec: v1beta1.ApplicationRevisionSpec{
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
TraitDefinitions: map[string]*v1beta1.TraitDefinition{
"expose": {
Spec: v1beta1.TraitDefinitionSpec{
RevisionEnabled: true,
AppliesToWorkloads: []string{"*"},
},
},
},
},
},
},
workload: &Workload{},
},
wantErr: assert.NoError,
validateFunc: func(w *Workload) bool {
return w != nil && len(w.Traits) == 1 && w.Traits[0].Name == "expose"
},
},
}
p := NewApplicationParser(nil, dm, pd)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.wantErr(t, p.parseTraitsFromRevision(tt.args.comp, tt.args.appRev, tt.args.workload), fmt.Sprintf("parseTraitsFromRevision(%v, %v, %v)", tt.args.comp, tt.args.appRev, tt.args.workload))
if tt.validateFunc != nil {
assert.True(t, tt.validateFunc(tt.args.workload))
}
})
}
}
func TestParser_parseScopesFromRevision(t *testing.T) {
type args struct {
comp common.ApplicationComponent
appRev *v1beta1.ApplicationRevision
workload *Workload
}
tests := []struct {
name string
args args
wantErr assert.ErrorAssertionFunc
validateFunc func(w *Workload) bool
}{
{
name: "test empty scopes",
args: args{
comp: common.ApplicationComponent{},
},
wantErr: assert.NoError,
},
{
name: "test get scope definition from revision failed",
args: args{
comp: common.ApplicationComponent{
Scopes: map[string]string{
"cluster": "namespace",
},
},
appRev: &v1beta1.ApplicationRevision{
Spec: v1beta1.ApplicationRevisionSpec{
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
ScopeDefinitions: map[string]v1beta1.ScopeDefinition{},
},
},
},
},
wantErr: assert.Error,
},
{
name: "test parse scopes from revision success",
args: args{
comp: common.ApplicationComponent{
Scopes: map[string]string{
"cluster": "namespace",
},
},
appRev: &v1beta1.ApplicationRevision{
Spec: v1beta1.ApplicationRevisionSpec{
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
ScopeDefinitions: map[string]v1beta1.ScopeDefinition{
"cluster": {
Spec: v1beta1.ScopeDefinitionSpec{
AllowComponentOverlap: true,
Reference: common.DefinitionReference{
Name: "cluster",
Version: "v1alpha2",
},
},
},
},
ScopeGVK: map[string]metav1.GroupVersionKind{
"cluster/v1alpha2": {
Group: "core.oam.dev",
Version: "v1alpha2",
},
},
},
},
},
workload: &Workload{},
},
wantErr: assert.NoError,
validateFunc: func(w *Workload) bool {
return w != nil && len(w.Scopes) == 1 && w.Scopes[0].ResourceVersion == "cluster/v1alpha2"
},
},
}
p := NewApplicationParser(nil, dm, pd)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.wantErr(t, p.parseScopesFromRevision(tt.args.comp, tt.args.appRev, tt.args.workload), fmt.Sprintf("parseScopesFromRevision(%v, %v, %v)", tt.args.comp, tt.args.appRev, tt.args.workload))
if tt.validateFunc != nil {
assert.True(t, tt.validateFunc(tt.args.workload))
}
})
}
}

View File

@ -72,7 +72,7 @@ type Template struct {
// processing.
func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli client.Reader, capName string, capType types.CapType) (*Template, error) {
ctx = multicluster.WithCluster(ctx, multicluster.Local)
// Application Controller only load template from ComponentDefinition and TraitDefinition
// Application Controller only loads template from ComponentDefinition and TraitDefinition
switch capType {
case types.TypeComponentDefinition, types.TypeWorkload:
cd := new(v1beta1.ComponentDefinition)

View File

@ -286,7 +286,7 @@ func GetDefinitionNamespaceWithCtx(ctx context.Context) string {
}
// SetNamespaceInCtx set app namespace in context,
// Sometimes webhook handler may receive request that appNs is empty string, and will cause error when search definition
// Sometimes webhook handler may receive a request that appNs is empty string, and will cause error when search definition
// So if namespace is empty, it will use `default` namespace by default.
func SetNamespaceInCtx(ctx context.Context, namespace string) context.Context {
if namespace == "" {
@ -302,18 +302,23 @@ func GetDefinition(ctx context.Context, cli client.Reader, definition client.Obj
appNs := GetDefinitionNamespaceWithCtx(ctx)
if err := cli.Get(ctx, types.NamespacedName{Name: definitionName, Namespace: appNs}, definition); err != nil {
if apierrors.IsNotFound(err) {
if err = cli.Get(ctx, types.NamespacedName{Name: definitionName, Namespace: oam.SystemDefinitionNamespace}, definition); err != nil {
if apierrors.IsNotFound(err) {
// compatibility code for old clusters those definition crd is cluster scope
var newErr error
if newErr = cli.Get(ctx, types.NamespacedName{Name: definitionName}, definition); checkRequestNamespaceError(newErr) {
return err
}
return newErr
}
return GetDefinitionFromNamespace(ctx, cli, definition, definitionName, oam.SystemDefinitionNamespace)
}
return err
}
return nil
}
// GetDefinitionFromNamespace get definition from namespace.
func GetDefinitionFromNamespace(ctx context.Context, cli client.Reader, definition client.Object, definitionName, namespace string) error {
if err := cli.Get(ctx, types.NamespacedName{Name: definitionName, Namespace: namespace}, definition); err != nil {
if apierrors.IsNotFound(err) {
// compatibility code for old clusters those definition crd is cluster scope
var newErr error
if newErr = cli.Get(ctx, types.NamespacedName{Name: definitionName}, definition); checkRequestNamespaceError(newErr) {
return err
}
return err
return newErr
}
return err
}
@ -383,7 +388,7 @@ func ConvertDefinitionRevName(definitionName string) (string, error) {
return defRevName, nil
}
// when get a namespaced scope object without namespace, would get an error request namespace
// when get a namespaced scope object without namespace, would get an error request namespace
func checkRequestNamespaceError(err error) bool {
return err != nil && err.Error() == "an empty namespace may not be set when a resource name is provided"
}
@ -461,7 +466,7 @@ func EndReconcileWithNegativeCondition(ctx context.Context, r client.StatusClien
return errors.Errorf(ErrReconcileErrInCondition, condition[0].Type, condition[0].Message)
}
// PatchCondition will patch status with condition and return, it generally used by cases which don't want reconcile after patch
// PatchCondition will patch status with condition and return, it generally used by cases which don't want to reconcile after patch
func PatchCondition(ctx context.Context, r client.StatusClient, workload ConditionedObject,
condition ...condition.Condition) error {
if len(condition) == 0 {
@ -472,7 +477,7 @@ func PatchCondition(ctx context.Context, r client.StatusClient, workload Conditi
return r.Status().Patch(ctx, workload, workloadPatch, client.FieldOwner(workload.GetUID()))
}
// IsConditionChanged will check if conditions in workload is changed compare to newCondition
// IsConditionChanged will check if conditions in workload are changed compare to newCondition
func IsConditionChanged(newCondition []condition.Condition, workload ConditionedObject) bool {
var conditionIsChanged bool
for _, newCond := range newCondition {