diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 746c40ced..dc217e8c4 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -1084,6 +1084,12 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error { return errors.Wrapf(err, "cannot pass definition to addon app's annotation") } + var auxiliaryOutputs []*unstructured.Unstructured + auxiliaryOutputs, err = renderOutputs(addon, h.args) + if err != nil { + return err + } + if err = h.createOrUpdate(app); err != nil { return err } @@ -1112,6 +1118,14 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error { } } + for _, o := range auxiliaryOutputs { + addOwner(o, app) + err = h.apply.Apply(h.ctx, o, apply.DisableUpdateAnnotation()) + if err != nil { + return err + } + } + if h.args != nil && len(h.args) > 0 { sec := RenderArgsSecret(addon, h.args) addOwner(sec, app) diff --git a/pkg/addon/render.go b/pkg/addon/render.go index 80a9b0e15..c2202b66f 100644 --- a/pkg/addon/render.go +++ b/pkg/addon/render.go @@ -24,6 +24,7 @@ import ( "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -42,6 +43,7 @@ const ( specifyAddonClustersTopologyPolicy = "deploy-addon-to-specified-clusters" addonAllClusterPolicy = "deploy-addon-to-all-clusters" renderOutputCuePath = "output" + renderAuxiliaryOutputsPath = "outputs" ) type addonCueTemplateRender struct { @@ -51,7 +53,7 @@ type addonCueTemplateRender struct { // 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, object interface{}) error { +func (a addonCueTemplateRender) toObject(cueTemplate string, path string, object interface{}) error { args := a.inputArgs if args == nil { args = map[string]interface{}{} @@ -82,7 +84,7 @@ func (a addonCueTemplateRender) toObject(cueTemplate string, object interface{}) if err != nil { return err } - outputContent, err := out.LookupValue(renderOutputCuePath) + outputContent, err := out.LookupValue(path) if err != nil { return err } @@ -131,7 +133,7 @@ func renderAppAccordingToCueTemplate(addon *InstallPackage, args map[string]inte addon: addon, inputArgs: args, } - if err := r.toObject(addon.AppCueTemplate.Data, &app); err != nil { + if err := r.toObject(addon.AppCueTemplate.Data, renderOutputCuePath, &app); err != nil { return nil, err } return &app, nil @@ -145,7 +147,7 @@ func renderCompAccordingCUETemplate(cueTemplate ElementFile, addon *InstallPacka addon: addon, inputArgs: args, } - if err := r.toObject(cueTemplate.Data, &comp); err != nil { + if err := r.toObject(cueTemplate.Data, renderOutputCuePath, &comp); err != nil { return nil, fmt.Errorf("error rendering file %s: %w", cueTemplate.Name, err) } // If the name of component has been set, just keep it, otherwise will set with file name. @@ -275,6 +277,27 @@ func checkNeedAttachTopologyPolicy(app *v1beta1.Application, addon *InstallPacka return true } +func renderOutputs(addon *InstallPackage, args map[string]interface{}) ([]*unstructured.Unstructured, error) { + outputs := map[string]interface{}{} + r := addonCueTemplateRender{ + addon: addon, + inputArgs: args, + } + if err := r.toObject(addon.AppCueTemplate.Data, renderAuxiliaryOutputsPath, &outputs); err != nil { + if isErrorCueRenderPathNotFound(err, renderAuxiliaryOutputsPath) { + return nil, nil + } + return nil, err + } + var res []*unstructured.Unstructured + for _, o := range outputs { + if ao, ok := o.(map[string]interface{}); ok { + res = append(res, &unstructured.Unstructured{Object: ao}) + } + } + return res, nil +} + func isDeployToRuntime(addon *InstallPackage) bool { if addon.DeployTo == nil { return false diff --git a/pkg/addon/render_test.go b/pkg/addon/render_test.go index a1194961b..085833734 100644 --- a/pkg/addon/render_test.go +++ b/pkg/addon/render_test.go @@ -87,7 +87,7 @@ func TestRenderAppTemplate(t *testing.T) { }, } app := v1beta1.Application{} - err := render.toObject(appTemplate, &app) + err := render.toObject(appTemplate, renderOutputCuePath, &app) assert.NoError(t, err) assert.Equal(t, len(app.Spec.Components), 1) str, err := json.Marshal(app.Spec.Components[0].Properties) @@ -100,6 +100,123 @@ func TestRenderAppTemplate(t *testing.T) { assert.True(t, strings.Contains(string(str), `"clusterLabelSelector":{}`)) } +func TestOutputsRender(t *testing.T) { + appTemplate := `output: { + apiVersion: "core.oam.dev/v1beta1" + kind: "Application" + metadata: { + name: "velaux" + namespace: "vela-system" + } + spec: { + components: [{ + type: "k8s-objects" + name: "vela-namespace" + properties: objects: [{ + apiVersion: "v1" + kind: "Namespace" + metadata: name: parameter.namespace + }] + }] + policies: [{ + type: "shared-resource" + name: "namespace" + properties: rules: [{selector: resourceTypes: ["Namespace"]}] + }, { + type: "topology" + name: "deploy-topology" + properties: { + if parameter.clusters != _|_ { + clusters: parameter.clusters + } + if parameter.clusters == _|_ { + clusterLabelSelector: {} + } + namespace: parameter.namespace + } + }] + } + }, + outputs: configmap: { + apiVersion: "v1" + kind: "Configmap" + metadata: { + name: "test-cm" + namespace: "default" + } + data: parameter.data + } +` + paraDefined := `parameter: { + // +usage=The clusters to install + data: "myData" +}` + appTemplateNoOutputs := `output: { + apiVersion: "core.oam.dev/v1beta1" + kind: "Application" + metadata: { + name: "velaux" + namespace: "vela-system" + } + spec: { + components: [{ + type: "k8s-objects" + name: "vela-namespace" + properties: objects: [{ + apiVersion: "v1" + kind: "Namespace" + metadata: name: parameter.namespace + }] + }] + policies: [{ + type: "shared-resource" + name: "namespace" + properties: rules: [{selector: resourceTypes: ["Namespace"]}] + }, { + type: "topology" + name: "deploy-topology" + properties: { + if parameter.clusters != _|_ { + clusters: parameter.clusters + } + if parameter.clusters == _|_ { + clusterLabelSelector: {} + } + namespace: parameter.namespace + } + }] + } + }, +` + + addon := &InstallPackage{ + Meta: Meta{ + Name: "velaux", + DeployTo: &DeployTo{ + RuntimeCluster: true, + }, + }, + Parameters: paraDefined, + AppCueTemplate: ElementFile{Data: appTemplate}, + } + list, err := renderOutputs(addon, nil) + assert.NoError(t, err) + assert.Equal(t, true, len(list) == 1) + + addon = &InstallPackage{ + Meta: Meta{ + Name: "velaux", + DeployTo: &DeployTo{ + RuntimeCluster: true, + }, + }, + Parameters: paraDefined, + AppCueTemplate: ElementFile{Data: appTemplateNoOutputs}, + } + _, err = renderOutputs(addon, nil) + assert.NoError(t, err) +} + func TestAppComponentRender(t *testing.T) { paraDefined := `parameter: { image: string @@ -127,7 +244,7 @@ func TestAppComponentRender(t *testing.T) { }, } comp := common.ApplicationComponent{} - err := render.toObject(compTemplate, &comp) + err := render.toObject(compTemplate, renderOutputCuePath, &comp) assert.NoError(t, err) assert.Equal(t, comp.Name, "velaux") assert.Equal(t, comp.Type, "webservice") diff --git a/pkg/addon/utils.go b/pkg/addon/utils.go index 42eb01caf..23527f1d8 100644 --- a/pkg/addon/utils.go +++ b/pkg/addon/utils.go @@ -405,3 +405,7 @@ func generateAnnotation(meta *Meta) map[string]string { } return res } + +func isErrorCueRenderPathNotFound(err error, path string) bool { + return err.Error() == fmt.Sprintf("var(path=%s) not exist", path) +}