diff --git a/charts/vela-core/templates/defwithtemplate/deploy.yaml b/charts/vela-core/templates/defwithtemplate/deploy.yaml index 1da9cc32b..abf90485d 100644 --- a/charts/vela-core/templates/defwithtemplate/deploy.yaml +++ b/charts/vela-core/templates/defwithtemplate/deploy.yaml @@ -26,7 +26,7 @@ spec: //+usage=If set to false, the workflow will suspend automatically before this step, default to be true. auto: *true | bool //+usage=Declare the policies that used for this deployment. If not specified, the components will be deployed to the hub cluster. - policies?: [...string] + policies: *[] | [...string] //+usage=Maximum number of concurrent delivered components. parallelism: *5 | int //+usage=If set false, this step will apply the components with the terraform workload. diff --git a/charts/vela-minimal/templates/defwithtemplate/deploy.yaml b/charts/vela-minimal/templates/defwithtemplate/deploy.yaml index 1da9cc32b..abf90485d 100644 --- a/charts/vela-minimal/templates/defwithtemplate/deploy.yaml +++ b/charts/vela-minimal/templates/defwithtemplate/deploy.yaml @@ -26,7 +26,7 @@ spec: //+usage=If set to false, the workflow will suspend automatically before this step, default to be true. auto: *true | bool //+usage=Declare the policies that used for this deployment. If not specified, the components will be deployed to the hub cluster. - policies?: [...string] + policies: *[] | [...string] //+usage=Maximum number of concurrent delivered components. parallelism: *5 | int //+usage=If set false, this step will apply the components with the terraform workload. diff --git a/pkg/appfile/appfile.go b/pkg/appfile/appfile.go index 8aa803374..6dcbb3724 100644 --- a/pkg/appfile/appfile.go +++ b/pkg/appfile/appfile.go @@ -353,7 +353,7 @@ func (af *Appfile) SetOAMContract(comp *types.ComponentManifest) error { } for _, trait := range comp.Traits { af.assembleTrait(trait, compName, commonLabels) - if err := af.setWorkloadRefToTrait(workloadRef, trait); err != nil { + if err := af.setWorkloadRefToTrait(workloadRef, trait); err != nil && !IsNotFoundInAppFile(err) { return errors.WithMessagef(err, "cannot set workload reference to trait %q", trait.GetName()) } } @@ -507,6 +507,11 @@ func (af *Appfile) setWorkloadRefToTrait(wlRef corev1.ObjectReference, trait *un return nil } +// IsNotFoundInAppFile check if the target error is `not found in appfile` +func IsNotFoundInAppFile(err error) bool { + return err != nil && strings.Contains(err.Error(), "not found in appfile") +} + // PrepareProcessContext prepares a DSL process Context func PrepareProcessContext(wl *Workload, ctxData velaprocess.ContextData) (process.Context, error) { if wl.Ctx == nil { diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go index faa9736aa..6a1e8122d 100644 --- a/pkg/appfile/parser.go +++ b/pkg/appfile/parser.go @@ -638,6 +638,53 @@ func (p *Parser) ParseWorkloadFromRevision(comp common.ApplicationComponent, app return workload, nil } +// ParseWorkloadFromRevisionAndClient resolve an ApplicationComponent and generate a Workload +// containing ALL information required by an Appfile from app revision, and will fall back to +// load external definitions if not found +func (p *Parser) ParseWorkloadFromRevisionAndClient(ctx context.Context, comp common.ApplicationComponent, appRev *v1beta1.ApplicationRevision) (*Workload, error) { + workload, err := p.makeWorkloadFromRevision(comp.Name, comp.Type, types.TypeComponentDefinition, comp.Properties, appRev) + if IsNotFoundInAppRevision(err) { + workload, err = p.makeWorkload(ctx, comp.Name, comp.Type, types.TypeComponentDefinition, comp.Properties) + } + if err != nil { + return nil, err + } + workload.ExternalRevision = comp.ExternalRevision + + 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) + } + trait, err := p.parseTraitFromRevision(traitValue.Type, properties, appRev) + if IsNotFoundInAppRevision(err) { + 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) + } + + workload.Traits = append(workload.Traits, trait) + } + + for scopeType, instanceName := range comp.Scopes { + sd, gvk, err := GetScopeDefAndGVKFromRevision(scopeType, appRev) + if IsNotFoundInAppRevision(err) { + sd, gvk, err = GetScopeDefAndGVK(ctx, p.client, p.dm, scopeType) + } + if err != nil { + return nil, err + } + workload.Scopes = append(workload.Scopes, Scope{ + Name: instanceName, + GVK: gvk, + ResourceVersion: sd.Spec.Reference.Name + "/" + sd.Spec.Reference.Version, + }) + workload.ScopeDefinition = append(workload.ScopeDefinition, sd) + } + return workload, nil +} + func (p *Parser) parseTrait(ctx context.Context, name string, properties map[string]interface{}) (*Trait, error) { templ, err := p.tmplLoader.LoadTemplate(ctx, p.dm, p.client, name, types.TypeTrait) if kerrors.IsNotFound(err) { diff --git a/pkg/appfile/template.go b/pkg/appfile/template.go index 21650b22b..dfc6ec6a7 100644 --- a/pkg/appfile/template.go +++ b/pkg/appfile/template.go @@ -231,6 +231,11 @@ func LoadTemplateFromRevision(capName string, capType types.CapType, apprev *v1b } } +// IsNotFoundInAppRevision check if the error is `not found in app revision` +func IsNotFoundInAppRevision(err error) bool { + return err != nil && strings.Contains(err.Error(), "not found in app revision") +} + func verifyRevisionName(capName string, capType types.CapType, apprev *v1beta1.ApplicationRevision) string { if strings.Contains(capName, "@") { splitName := capName[0:strings.LastIndex(capName, "@")] diff --git a/pkg/appfile/unit_test.go b/pkg/appfile/unit_test.go new file mode 100644 index 000000000..7d71d2466 --- /dev/null +++ b/pkg/appfile/unit_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2023 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 appfile + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) + +func TestIsNotFoundInAppFile(t *testing.T) { + require.True(t, IsNotFoundInAppFile(fmt.Errorf("ComponentDefinition XXX not found in appfile"))) +} + +func TestIsNotFoundInAppRevision(t *testing.T) { + require.True(t, IsNotFoundInAppRevision(fmt.Errorf("ComponentDefinition XXX not found in app revision"))) +} + +func TestParseWorkloadFromRevisionAndClient(t *testing.T) { + ctx := context.Background() + cli := fake.NewClientBuilder().WithScheme(scheme).Build() + p := &Parser{ + client: cli, + tmplLoader: LoadTemplate, + } + comp := common.ApplicationComponent{ + Name: "test", + Type: "test", + Properties: &runtime.RawExtension{Raw: []byte(`{}`)}, + Traits: []common.ApplicationTrait{{ + Type: "tr", + Properties: &runtime.RawExtension{Raw: []byte(`{}`)}, + }, { + Type: "internal", + Properties: &runtime.RawExtension{Raw: []byte(`{}`)}, + }}, + } + appRev := &v1beta1.ApplicationRevision{} + cd := &v1beta1.ComponentDefinition{ObjectMeta: metav1.ObjectMeta{Name: "test"}} + td := &v1beta1.TraitDefinition{ObjectMeta: metav1.ObjectMeta{Name: "tr"}} + require.NoError(t, cli.Create(ctx, cd)) + require.NoError(t, cli.Create(ctx, td)) + appRev.Spec.TraitDefinitions = map[string]v1beta1.TraitDefinition{"internal": {}} + _, err := p.ParseWorkloadFromRevisionAndClient(ctx, comp, appRev) + require.NoError(t, err) + + _comp1 := comp.DeepCopy() + _comp1.Type = "bad" + _, err = p.ParseWorkloadFromRevisionAndClient(ctx, *_comp1, appRev) + require.Error(t, err) + + _comp2 := comp.DeepCopy() + _comp2.Traits[0].Type = "bad" + _, err = p.ParseWorkloadFromRevisionAndClient(ctx, *_comp2, appRev) + require.Error(t, err) + + _comp3 := comp.DeepCopy() + _comp3.Traits[0].Properties = &runtime.RawExtension{Raw: []byte(`bad`)} + _, err = p.ParseWorkloadFromRevisionAndClient(ctx, *_comp3, appRev) + require.Error(t, err) +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/generator.go b/pkg/controller/core.oam.dev/v1alpha2/application/generator.go index 876c3dad8..b8bf1a7a7 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/generator.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/generator.go @@ -106,16 +106,14 @@ func (h *AppHandler) GenerateApplicationSteps(ctx monitorContext.Context, oamProvider.Install(handlerProviders, app, af, h.r.Client, h.applyComponentFunc( appParser, appRev, af), h.renderComponentFunc(appParser, appRev, af)) pCtx := velaprocess.NewContext(generateContextDataFromApp(app, appRev.Name)) + renderer := func(ctx context.Context, comp common.ApplicationComponent) (*appfile.Workload, error) { + return appParser.ParseWorkloadFromRevisionAndClient(ctx, comp, appRev) + } multiclusterProvider.Install(handlerProviders, h.r.Client, app, af, h.applyComponentFunc(appParser, appRev, af), h.checkComponentHealth(appParser, appRev, af), - func(_ context.Context, comp common.ApplicationComponent) (*appfile.Workload, error) { - return appParser.ParseWorkloadFromRevision(comp, appRev) - }, - ) - terraformProvider.Install(handlerProviders, app, func(_ context.Context, comp common.ApplicationComponent) (*appfile.Workload, error) { - return appParser.ParseWorkloadFromRevision(comp, appRev) - }) + renderer) + terraformProvider.Install(handlerProviders, app, renderer) query.Install(handlerProviders, h.r.Client, nil) instance := generateWorkflowInstance(af, app, appRev.Name) @@ -432,7 +430,7 @@ func (h *AppHandler) prepareWorkloadAndManifests(ctx context.Context, appRev *v1beta1.ApplicationRevision, patcher *value.Value, af *appfile.Appfile) (*appfile.Workload, *types.ComponentManifest, error) { - wl, err := appParser.ParseWorkloadFromRevision(comp, appRev) + wl, err := appParser.ParseWorkloadFromRevisionAndClient(ctx, comp, appRev) if err != nil { return nil, nil, errors.WithMessage(err, "ParseWorkload") } diff --git a/pkg/stdlib/pkgs/multicluster.cue b/pkg/stdlib/pkgs/multicluster.cue index ac68e7e55..dd3b4e372 100644 --- a/pkg/stdlib/pkgs/multicluster.cue +++ b/pkg/stdlib/pkgs/multicluster.cue @@ -245,4 +245,5 @@ policies: [...string] parallelism: int ignoreTerraformComponent: bool + inlinePolicies: *[] | [...{...}] } diff --git a/pkg/workflow/providers/multicluster/deploy.go b/pkg/workflow/providers/multicluster/deploy.go index 12202f61a..73393dcb0 100644 --- a/pkg/workflow/providers/multicluster/deploy.go +++ b/pkg/workflow/providers/multicluster/deploy.go @@ -47,11 +47,13 @@ import ( // DeployParameter is the parameter of deploy workflow step type DeployParameter struct { // Declare the policies that used for this deployment. If not specified, the components will be deployed to the hub cluster. - Policies []string + Policies []string `json:"policies,omitempty"` // Maximum number of concurrent delivered components. - Parallelism int64 + Parallelism int64 `json:"parallelism"` // If set false, this step will apply the components with the terraform workload. - IgnoreTerraformComponent bool + IgnoreTerraformComponent bool `json:"ignoreTerraformComponent"` + // The policies that embeds in the `deploy` step directly + InlinePolicies []v1beta1.AppPolicy `json:"inlinePolicies,omitempty"` } // DeployWorkflowStepExecutor executor to run deploy workflow step @@ -86,6 +88,7 @@ func (executor *deployWorkflowStepExecutor) Deploy(ctx context.Context) (bool, s if err != nil { return false, "", err } + policies = append(policies, fillInlinePolicyNames(executor.parameter.InlinePolicies)...) components, err := loadComponents(ctx, executor.renderer, executor.cli, executor.af, executor.af.Components, executor.parameter.IgnoreTerraformComponent) if err != nil { return false, "", err @@ -123,6 +126,15 @@ func selectPolicies(policies []v1beta1.AppPolicy, policyNames []string) ([]v1bet return selectedPolicies, nil } +func fillInlinePolicyNames(policies []v1beta1.AppPolicy) []v1beta1.AppPolicy { + for i := range policies { + if policies[i].Name == "" { + policies[i].Name = fmt.Sprintf("inline-%s-policy-%d", policies[i].Type, i) + } + } + return policies +} + func loadComponents(ctx context.Context, renderer oamProvider.WorkloadRenderer, cli client.Client, af *appfile.Appfile, components []common.ApplicationComponent, ignoreTerraformComponent bool) ([]common.ApplicationComponent, error) { var loadedComponents []common.ApplicationComponent for _, comp := range components { diff --git a/pkg/workflow/providers/multicluster/multicluster.go b/pkg/workflow/providers/multicluster/multicluster.go index a7e1a5ed0..64289ab90 100644 --- a/pkg/workflow/providers/multicluster/multicluster.go +++ b/pkg/workflow/providers/multicluster/multicluster.go @@ -172,26 +172,13 @@ func (p *provider) ListClusters(ctx monitorContext.Context, wfCtx wfContext.Cont } func (p *provider) Deploy(ctx monitorContext.Context, _ wfContext.Context, v *value.Value, act wfTypes.Action) error { - policyNames, err := v.GetStringSlice("policies") - if err != nil { + param := DeployParameter{} + if err := v.CueValue().Decode(¶m); err != nil { return err } - parallelism, err := v.GetInt64("parallelism") - if err != nil { - return err - } - if parallelism <= 0 { + if param.Parallelism <= 0 { return errors.Errorf("parallelism cannot be smaller than 1") } - ignoreTerraformComponent, err := v.GetBool("ignoreTerraformComponent") - if err != nil { - return err - } - param := DeployParameter{ - Policies: policyNames, - Parallelism: parallelism, - IgnoreTerraformComponent: ignoreTerraformComponent, - } executor := NewDeployWorkflowStepExecutor(p.Client, p.af, p.apply, p.healthCheck, p.renderer, param) healthy, reason, err := executor.Deploy(ctx) if err != nil { diff --git a/vela-templates/definitions/internal/workflowstep/deploy.cue b/vela-templates/definitions/internal/workflowstep/deploy.cue index 48ebaeec3..e6e877183 100644 --- a/vela-templates/definitions/internal/workflowstep/deploy.cue +++ b/vela-templates/definitions/internal/workflowstep/deploy.cue @@ -20,7 +20,7 @@ template: { //+usage=If set to false, the workflow will suspend automatically before this step, default to be true. auto: *true | bool //+usage=Declare the policies that used for this deployment. If not specified, the components will be deployed to the hub cluster. - policies?: [...string] + policies: *[] | [...string] //+usage=Maximum number of concurrent delivered components. parallelism: *5 | int //+usage=If set false, this step will apply the components with the terraform workload.