From 8fd47ef5b92e076579d13d4c60e2ef88ece90113 Mon Sep 17 00:00:00 2001 From: Jianbo Sun Date: Mon, 25 Jul 2022 22:15:59 +0800 Subject: [PATCH] Feat: support vela addon enable with package (#4443) Signed-off-by: Jianbo Sun --- pkg/addon/addon.go | 1 - pkg/addon/render.go | 62 +++++++++++++++++++++++++++++------- pkg/addon/render_test.go | 59 +++++++++++++++++++++++++++++----- pkg/cue/model/value/value.go | 47 +++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 20 deletions(-) diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index dc217e8c4..295e1bb9a 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -1056,7 +1056,6 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error { if err != nil { return errors.Wrap(err, "render addon application fail") } - appName, err := determineAddonAppName(h.ctx, h.cli, h.addon.Name) if err != nil { return err diff --git a/pkg/addon/render.go b/pkg/addon/render.go index c2202b66f..bcf6eb9c7 100644 --- a/pkg/addon/render.go +++ b/pkg/addon/render.go @@ -23,6 +23,7 @@ import ( "path" "strings" + "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -51,32 +52,42 @@ type addonCueTemplateRender struct { inputArgs map[string]interface{} } -// This func can be used for addon render, supporting render app template and component. -// Please notice the result will be stored in object parameter, so object must be a pointer type -func (a addonCueTemplateRender) toObject(cueTemplate string, path string, object interface{}) error { +func (a addonCueTemplateRender) formatContext() (string, error) { args := a.inputArgs if args == nil { args = map[string]interface{}{} } bt, err := json.Marshal(args) if err != nil { - return err + return "", err } paramFile := fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt)) var contextFile = strings.Builder{} + // user custom parameter but be the first data and generated data should be appended at last + // in case the user defined data has packages + contextFile.WriteString(a.addon.Parameters + "\n") + // addon metadata context metadataJSON, err := json.Marshal(a.addon.Meta) if err != nil { - return err + return "", err } contextFile.WriteString(fmt.Sprintf("context: metadata: %s\n", string(metadataJSON))) // parameter definition contextFile.WriteString(paramFile + "\n") - // user custom parameter - contextFile.WriteString(a.addon.Parameters + "\n") - v, err := value.NewValue(contextFile.String(), nil, "") + return contextFile.String(), nil +} + +// This func can be used for addon render component. +// Please notice the result will be stored in object parameter, so object must be a pointer type +func (a addonCueTemplateRender) toObject(cueTemplate string, path string, object interface{}) error { + contextFile, err := a.formatContext() + if err != nil { + return err + } + v, err := value.NewValue(contextFile, nil, "") if err != nil { return err } @@ -91,6 +102,35 @@ func (a addonCueTemplateRender) toObject(cueTemplate string, path string, object return outputContent.UnmarshalTo(object) } +// renderApp will render Application from CUE files +func (a addonCueTemplateRender) renderApp() (*v1beta1.Application, error) { + var app v1beta1.Application + + contextFile, err := a.formatContext() + if err != nil { + return nil, errors.Wrap(err, "format context for app render") + } + var files = []string{contextFile} + for _, cuef := range a.addon.CUETemplates { + files = append(files, cuef.Data) + } + + // TODO(wonderflow): add package discover to support vela own packages if needed + v, err := value.NewValueWithFiles(a.addon.AppCueTemplate.Data, files, nil, "") + if err != nil { + return nil, errors.Wrap(err, "load app template with CUE files") + } + outputContent, err := v.LookupValue(renderOutputCuePath) + if err != nil { + return nil, errors.Wrap(err, "render app from output field from CUE") + } + err = outputContent.UnmarshalTo(&app) + if err != nil { + return nil, errors.Wrap(err, "decode app from CUE") + } + return &app, nil +} + // generateAppFramework generate application from yaml defined by template.yaml or cue file from template.cue func generateAppFramework(addon *InstallPackage, parameters map[string]interface{}) (*v1beta1.Application, error) { if len(addon.AppCueTemplate.Data) != 0 && addon.AppTemplate != nil { @@ -128,15 +168,15 @@ func generateAppFramework(addon *InstallPackage, parameters map[string]interface } func renderAppAccordingToCueTemplate(addon *InstallPackage, args map[string]interface{}) (*v1beta1.Application, error) { - app := v1beta1.Application{} r := addonCueTemplateRender{ addon: addon, inputArgs: args, } - if err := r.toObject(addon.AppCueTemplate.Data, renderOutputCuePath, &app); err != nil { + app, err := r.renderApp() + if err != nil { return nil, err } - return &app, nil + return app, nil } // renderCompAccordingCUETemplate will return a component from cue template diff --git a/pkg/addon/render_test.go b/pkg/addon/render_test.go index 085833734..5c8af62a0 100644 --- a/pkg/addon/render_test.go +++ b/pkg/addon/render_test.go @@ -34,6 +34,14 @@ func TestRenderAppTemplate(t *testing.T) { clusters?: [...string] namespace: string }` + resourceComponent1 := ` +myref: { + type: "ref-objects" + properties: { + urls: ["https://hello.yaml"] + } +} +` appTemplate := `output: { apiVersion: "core.oam.dev/v1beta1" kind: "Application" @@ -50,7 +58,7 @@ func TestRenderAppTemplate(t *testing.T) { kind: "Namespace" metadata: name: parameter.namespace }] - }] + },myref] policies: [{ type: "shared-resource" name: "namespace" @@ -77,7 +85,9 @@ func TestRenderAppTemplate(t *testing.T) { RuntimeCluster: true, }, }, - Parameters: paraDefined, + Parameters: paraDefined, + CUETemplates: []ElementFile{{Data: resourceComponent1}}, + AppCueTemplate: ElementFile{Data: appTemplate}, } render := addonCueTemplateRender{ @@ -86,18 +96,43 @@ func TestRenderAppTemplate(t *testing.T) { "namespace": "vela-system", }, } - app := v1beta1.Application{} - err := render.toObject(appTemplate, renderOutputCuePath, &app) + app, err := render.renderApp() assert.NoError(t, err) - assert.Equal(t, len(app.Spec.Components), 1) + assert.Equal(t, len(app.Spec.Components), 2) str, err := json.Marshal(app.Spec.Components[0].Properties) assert.NoError(t, err) assert.True(t, strings.Contains(string(str), `{"name":"vela-system"}`)) + str2, err := json.Marshal(app.Spec.Components[1].Properties) + assert.NoError(t, err) + assert.True(t, strings.Contains(string(str2), `{"urls":["https://hello.yaml"]}`)) assert.Equal(t, len(app.Spec.Policies), 2) str, err = json.Marshal(app.Spec.Policies) assert.NoError(t, err) assert.True(t, strings.Contains(string(str), `"clusterLabelSelector":{}`)) + + addon.CUETemplates = []ElementFile{{Data: resourceComponent1}} + addon.AppCueTemplate = ElementFile{Data: "package main\n" + appTemplate} + app, err = render.renderApp() + assert.NoError(t, err) + assert.Equal(t, len(app.Spec.Components), 2) + + addon.Parameters = "package main\n" + paraDefined + app, err = render.renderApp() + assert.NoError(t, err) + assert.Equal(t, len(app.Spec.Components), 2) + + addon.CUETemplates = []ElementFile{{Data: "package hello\n" + resourceComponent1}} + addon.AppCueTemplate = ElementFile{Data: "package main\n" + appTemplate} + _, err = render.renderApp() + assert.Equal(t, err.Error(), `load app template with CUE files: reference "myref" not found`) + + addon.CUETemplates = []ElementFile{{Data: "package hello\n" + resourceComponent1}} + addon.Parameters = paraDefined + addon.AppCueTemplate = ElementFile{Data: appTemplate} + _, err = render.renderApp() + assert.Equal(t, err.Error(), `load app template with CUE files: reference "myref" not found`) + } func TestOutputsRender(t *testing.T) { @@ -370,13 +405,17 @@ func TestGenerateAppFrameworkWithYamlTemplate(t *testing.T) { } func TestRenderCueResourceError(t *testing.T) { - cueTemplate1 := `{ + cueTemplate1 := `output: { type: "webservice" name: "velaux" }` cueTemplate2 := `output: { type: "webservice" - name: "velaux" + name: "velaux2" +}` + cueTemplate3 := `nooutput: { + type: "webservice" + name: "velaux3" }` comp, err := renderResources(&InstallPackage{ CUETemplates: []ElementFile{ @@ -388,8 +427,12 @@ func TestRenderCueResourceError(t *testing.T) { Data: cueTemplate2, Name: "tmplaate2.cue", }, + { + Data: cueTemplate3, + Name: "tmplaate3.cue", + }, }, }, nil) assert.NoError(t, err) - assert.Equal(t, len(comp), 1) + assert.Equal(t, len(comp), 2) } diff --git a/pkg/cue/model/value/value.go b/pkg/cue/model/value/value.go index 06df1e668..2ea3e0309 100644 --- a/pkg/cue/model/value/value.go +++ b/pkg/cue/model/value/value.go @@ -75,6 +75,49 @@ func (val *Value) UnmarshalTo(x interface{}) error { return json.Unmarshal(data, x) } +// NewValueWithFiles new a value from main and appendix files +func NewValueWithFiles(main string, slaveFiles []string, pd *packages.PackageDiscover, tagTempl string, opts ...func(*ast.File) error) (*Value, error) { + builder := &build.Instance{} + + mainFile, err := parser.ParseFile("main.cue", main, parser.ParseComments) + if err != nil { + return nil, errors.Wrap(err, "parse main file") + } + for _, opt := range opts { + if err := opt(mainFile); err != nil { + return nil, errors.Wrap(err, "run option func for main file") + } + } + if err := builder.AddSyntax(mainFile); err != nil { + return nil, errors.Wrap(err, "add main file to CUE builder") + } + + for idx, sf := range slaveFiles { + cueSF, err := parser.ParseFile("sf-"+strconv.Itoa(idx)+".cue", sf, parser.ParseComments) + if err != nil { + return nil, errors.Wrap(err, "parse added file "+strconv.Itoa(idx)+" \n"+sf) + } + if mainFile.PackageName() != "" && cueSF.PackageName() == "" { + cueSF, err = parser.ParseFile("sf-"+strconv.Itoa(idx)+".cue", "package "+mainFile.PackageName()+"\n"+sf, parser.ParseComments) + if err != nil { + return nil, errors.Wrap(err, "add package for added file") + } + } + if cueSF.PackageName() != mainFile.PackageName() { + continue + } + for _, opt := range opts { + if err := opt(cueSF); err != nil { + return nil, errors.Wrap(err, "run option func for files") + } + } + if err := builder.AddSyntax(cueSF); err != nil { + return nil, errors.Wrap(err, "add slave files to CUE builder") + } + } + return newValue(builder, pd, tagTempl) +} + // NewValue new a value func NewValue(s string, pd *packages.PackageDiscover, tagTempl string, opts ...func(*ast.File) error) (*Value, error) { builder := &build.Instance{} @@ -91,6 +134,10 @@ func NewValue(s string, pd *packages.PackageDiscover, tagTempl string, opts ...f if err := builder.AddSyntax(file); err != nil { return nil, err } + return newValue(builder, pd, tagTempl) +} + +func newValue(builder *build.Instance, pd *packages.PackageDiscover, tagTempl string) (*Value, error) { addImports := func(inst *build.Instance) error { if pd != nil { pd.ImportBuiltinPackagesFor(inst)