github.com/oam-dev/kubevela@v1.9.11/pkg/addon/render.go (about)

     1  /*
     2  Copyright 2022 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package addon
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"path"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"cuelang.org/go/cue/ast"
    28  	"cuelang.org/go/cue/build"
    29  	"cuelang.org/go/cue/parser"
    30  	"github.com/cue-exp/kubevelafix"
    31  	"github.com/pkg/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/klog/v2"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  
    38  	"github.com/kubevela/workflow/pkg/cue/model/value"
    39  	"github.com/kubevela/workflow/pkg/cue/packages"
    40  
    41  	common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    42  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
    43  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    44  	"github.com/oam-dev/kubevela/apis/types"
    45  	"github.com/oam-dev/kubevela/pkg/cue/process"
    46  	"github.com/oam-dev/kubevela/pkg/multicluster"
    47  	"github.com/oam-dev/kubevela/pkg/oam"
    48  	"github.com/oam-dev/kubevela/pkg/oam/util"
    49  	addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
    50  	verrors "github.com/oam-dev/kubevela/pkg/utils/errors"
    51  )
    52  
    53  const (
    54  	specifyAddonClustersTopologyPolicy = "deploy-addon-to-specified-clusters"
    55  	addonAllClusterPolicy              = "deploy-addon-to-all-clusters"
    56  	renderOutputCuePath                = "output"
    57  	renderAuxiliaryOutputsPath         = "outputs"
    58  	defaultCuePackageHeader            = "main"
    59  	defaultPackageHeader               = "package main\n"
    60  )
    61  
    62  type addonCueTemplateRender struct {
    63  	addon       *InstallPackage
    64  	inputArgs   map[string]interface{}
    65  	contextInfo map[string]interface{}
    66  }
    67  
    68  func (a addonCueTemplateRender) formatContext() (string, error) {
    69  	args := a.inputArgs
    70  	if args == nil {
    71  		args = map[string]interface{}{}
    72  	}
    73  	contextInfo := a.contextInfo
    74  	if contextInfo == nil {
    75  		contextInfo = map[string]interface{}{}
    76  	}
    77  	bt, err := json.Marshal(args)
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  	paramFile := fmt.Sprintf("%s: %s", process.ParameterFieldName, string(bt))
    82  
    83  	var contextFile = strings.Builder{}
    84  	// user custom parameter but be the first data and generated data should be appended at last
    85  	// in case the user defined data has packages
    86  	contextFile.WriteString(a.addon.Parameters + "\n")
    87  
    88  	// add metadata of addon into context
    89  	contextInfo["metadata"] = a.addon.Meta
    90  	contextJSON, err := json.Marshal(contextInfo)
    91  	if err != nil {
    92  		return "", err
    93  	}
    94  	contextFile.WriteString(fmt.Sprintf("context: %s\n", string(contextJSON)))
    95  	// parameter definition
    96  	contextFile.WriteString(paramFile + "\n")
    97  
    98  	return contextFile.String(), nil
    99  }
   100  
   101  // This func can be used for addon render component.
   102  // Please notice the result will be stored in object parameter, so object must be a pointer type
   103  func (a addonCueTemplateRender) toObject(cueTemplate string, path string, object interface{}) error {
   104  	contextFile, err := a.formatContext()
   105  	if err != nil {
   106  		return err
   107  	}
   108  	v, err := value.NewValue(contextFile, nil, "")
   109  	if err != nil {
   110  		return err
   111  	}
   112  	out, err := v.LookupByScript(cueTemplate)
   113  	if err != nil {
   114  		return err
   115  	}
   116  	outputContent, err := out.LookupValue(path)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	return outputContent.UnmarshalTo(object)
   121  }
   122  
   123  // renderApp will render Application from CUE files
   124  func (a addonCueTemplateRender) renderApp() (*v1beta1.Application, []*unstructured.Unstructured, error) {
   125  	var app v1beta1.Application
   126  	var outputs = map[string]interface{}{}
   127  	var res []*unstructured.Unstructured
   128  
   129  	contextFile, err := a.formatContext()
   130  	if err != nil {
   131  		return nil, nil, errors.Wrap(err, "format context for app render")
   132  	}
   133  	contextCue, err := parser.ParseFile("parameter.cue", contextFile, parser.ParseComments)
   134  	if err != nil {
   135  		return nil, nil, errors.Wrap(err, "parse parameter context")
   136  	}
   137  	if contextCue.PackageName() == "" {
   138  		contextFile = value.DefaultPackageHeader + contextFile
   139  	}
   140  
   141  	var files = []string{contextFile}
   142  	for _, cuef := range a.addon.CUETemplates {
   143  		files = append(files, cuef.Data)
   144  	}
   145  
   146  	// TODO(wonderflow): add package discover to support vela own packages if needed
   147  	v, err := newValueWithMainAndFiles(a.addon.AppCueTemplate.Data, files, nil, "")
   148  	if err != nil {
   149  		return nil, nil, errors.Wrap(err, "load app template with CUE files")
   150  	}
   151  	if v.Error() != nil {
   152  		return nil, nil, errors.Wrap(v.Error(), "load app template with CUE files")
   153  	}
   154  
   155  	outputContent, err := v.LookupValue(renderOutputCuePath)
   156  	if err != nil {
   157  		return nil, nil, errors.Wrap(err, "render app from output field from CUE")
   158  	}
   159  	err = outputContent.UnmarshalTo(&app)
   160  	if err != nil {
   161  		return nil, nil, errors.Wrap(err, "decode app from CUE")
   162  	}
   163  	auxiliaryContent, err := v.LookupValue(renderAuxiliaryOutputsPath)
   164  	if err != nil {
   165  		// no outputs defined in app template, return normal data
   166  		if verrors.IsCuePathNotFound(err) {
   167  			return &app, res, nil
   168  		}
   169  		return nil, nil, errors.Wrap(err, "render app from output field from CUE")
   170  	}
   171  
   172  	err = auxiliaryContent.UnmarshalTo(&outputs)
   173  	if err != nil {
   174  		return nil, nil, errors.Wrap(err, "decode app from CUE")
   175  	}
   176  	for k, o := range outputs {
   177  		if ao, ok := o.(map[string]interface{}); ok {
   178  			auxO := &unstructured.Unstructured{Object: ao}
   179  			auxO.SetLabels(util.MergeMapOverrideWithDst(auxO.GetLabels(), map[string]string{oam.LabelAddonAuxiliaryName: k}))
   180  			res = append(res, auxO)
   181  		}
   182  	}
   183  	return &app, res, nil
   184  }
   185  
   186  // newValueWithMainAndFiles new a value from main and appendix files
   187  func newValueWithMainAndFiles(main string, slaveFiles []string, pd *packages.PackageDiscover, tagTempl string, opts ...func(*ast.File) error) (*value.Value, error) {
   188  	builder := &build.Instance{}
   189  
   190  	mainFile, err := parser.ParseFile("main.cue", main, parser.ParseComments)
   191  	mainFile = kubevelafix.Fix(mainFile).(*ast.File)
   192  	if err != nil {
   193  		return nil, errors.Wrap(err, "parse main file")
   194  	}
   195  	if mainFile.PackageName() == "" {
   196  		// add a default package main if not exist
   197  		mainFile, err = parser.ParseFile("main.cue", defaultPackageHeader+main, parser.ParseComments)
   198  		if err != nil {
   199  			return nil, errors.Wrap(err, "parse main file with added package main header")
   200  		}
   201  	}
   202  	for _, opt := range opts {
   203  		if err := opt(mainFile); err != nil {
   204  			return nil, errors.Wrap(err, "run option func for main file")
   205  		}
   206  	}
   207  	if err := builder.AddSyntax(mainFile); err != nil {
   208  		return nil, errors.Wrap(err, "add main file to CUE builder")
   209  	}
   210  
   211  	for idx, sf := range slaveFiles {
   212  		cueSF, err := parser.ParseFile("sf-"+strconv.Itoa(idx)+".cue", sf, parser.ParseComments)
   213  		cueSF = kubevelafix.Fix(cueSF).(*ast.File)
   214  		if err != nil {
   215  			return nil, errors.Wrap(err, "parse added file "+strconv.Itoa(idx)+" \n"+sf)
   216  		}
   217  		if cueSF.PackageName() != mainFile.PackageName() {
   218  			continue
   219  		}
   220  		for _, opt := range opts {
   221  			if err := opt(cueSF); err != nil {
   222  				return nil, errors.Wrap(err, "run option func for files")
   223  			}
   224  		}
   225  		if err := builder.AddSyntax(cueSF); err != nil {
   226  			return nil, errors.Wrap(err, "add slave files to CUE builder")
   227  		}
   228  	}
   229  	return value.NewValueWithInstance(builder, pd, tagTempl)
   230  }
   231  
   232  // generateAppFramework generate application from yaml defined by template.yaml or cue file from template.cue
   233  func generateAppFramework(addon *InstallPackage, parameters map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) {
   234  	if len(addon.AppCueTemplate.Data) != 0 && addon.AppTemplate != nil {
   235  		return nil, nil, ErrBothCueAndYamlTmpl
   236  	}
   237  
   238  	var app *v1beta1.Application
   239  	var auxiliaryObjects []*unstructured.Unstructured
   240  	var err error
   241  	if len(addon.AppCueTemplate.Data) != 0 {
   242  		app, auxiliaryObjects, err = renderAppAccordingToCueTemplate(addon, parameters)
   243  		if err != nil {
   244  			return nil, nil, err
   245  		}
   246  	} else {
   247  		app = addon.AppTemplate
   248  		if app == nil {
   249  			app = &v1beta1.Application{
   250  				TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: v1beta1.ApplicationKind},
   251  			}
   252  		}
   253  		if app.Spec.Components == nil {
   254  			app.Spec.Components = []common2.ApplicationComponent{}
   255  		}
   256  	}
   257  
   258  	if app.Name != "" && app.Name != addonutil.Addon2AppName(addon.Name) {
   259  		klog.Warningf("Application name %s will be overwritten with %s. Consider removing metadata.name in template.", app.Name, addonutil.Addon2AppName(addon.Name))
   260  	}
   261  	app.SetName(addonutil.Addon2AppName(addon.Name))
   262  
   263  	if app.Namespace != "" && app.Namespace != types.DefaultKubeVelaNS {
   264  		klog.Warningf("Namespace %s will be overwritten with %s. Consider removing metadata.namespace in template.", app.Namespace, types.DefaultKubeVelaNS)
   265  	}
   266  	// force override the namespace defined vela with DefaultVelaNS. This value can be modified by env
   267  	app.SetNamespace(types.DefaultKubeVelaNS)
   268  
   269  	if app.Labels == nil {
   270  		app.Labels = make(map[string]string)
   271  	}
   272  	app.Labels[oam.LabelAddonName] = addon.Name
   273  	app.Labels[oam.LabelAddonVersion] = addon.Version
   274  
   275  	for _, aux := range auxiliaryObjects {
   276  		aux.SetLabels(util.MergeMapOverrideWithDst(aux.GetLabels(), map[string]string{oam.LabelAddonName: addon.Name, oam.LabelAddonVersion: addon.Version}))
   277  	}
   278  
   279  	return app, auxiliaryObjects, nil
   280  }
   281  
   282  func renderAppAccordingToCueTemplate(addon *InstallPackage, args map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) {
   283  	r := addonCueTemplateRender{
   284  		addon:     addon,
   285  		inputArgs: args,
   286  	}
   287  	return r.renderApp()
   288  }
   289  
   290  // renderCompAccordingCUETemplate will return a component from cue template
   291  func renderCompAccordingCUETemplate(cueTemplate ElementFile, addon *InstallPackage, args map[string]interface{}) (*common2.ApplicationComponent, error) {
   292  	comp := common2.ApplicationComponent{}
   293  
   294  	r := addonCueTemplateRender{
   295  		addon:     addon,
   296  		inputArgs: args,
   297  	}
   298  	if err := r.toObject(cueTemplate.Data, renderOutputCuePath, &comp); err != nil {
   299  		return nil, fmt.Errorf("error rendering file %s: %w", cueTemplate.Name, err)
   300  	}
   301  	// If the name of component has been set, just keep it, otherwise will set with file name.
   302  	if len(comp.Name) == 0 {
   303  		fileName := strings.ReplaceAll(cueTemplate.Name, path.Ext(cueTemplate.Name), "")
   304  		comp.Name = strings.ReplaceAll(fileName, ".", "-")
   305  	}
   306  	return &comp, nil
   307  }
   308  
   309  // RenderApp render a K8s application
   310  func RenderApp(ctx context.Context, addon *InstallPackage, k8sClient client.Client, args map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) {
   311  	if args == nil {
   312  		args = map[string]interface{}{}
   313  	}
   314  	app, auxiliaryObjects, err := generateAppFramework(addon, args)
   315  	if err != nil {
   316  		return nil, nil, err
   317  	}
   318  	app.Spec.Components = append(app.Spec.Components, renderNeededNamespaceAsComps(addon)...)
   319  
   320  	resources, err := renderResources(addon, args)
   321  	if err != nil {
   322  		return nil, nil, err
   323  	}
   324  	app.Spec.Components = append(app.Spec.Components, resources...)
   325  
   326  	// for legacy addons those hasn't define policy in template.cue but still want to deploy runtime cluster
   327  	// attach topology policy to application.
   328  	if checkNeedAttachTopologyPolicy(app, addon) {
   329  		if err := attachPolicyForLegacyAddon(ctx, app, addon, args, k8sClient); err != nil {
   330  			return nil, nil, err
   331  		}
   332  	}
   333  	return app, auxiliaryObjects, nil
   334  }
   335  
   336  func attachPolicyForLegacyAddon(ctx context.Context, app *v1beta1.Application, addon *InstallPackage, args map[string]interface{}, k8sClient client.Client) error {
   337  	deployClusters, err := checkDeployClusters(ctx, k8sClient, args)
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	if !isDeployToRuntime(addon) {
   343  		return nil
   344  	}
   345  
   346  	if len(deployClusters) == 0 {
   347  		// empty cluster args deploy to all clusters
   348  		clusterSelector := map[string]interface{}{
   349  			// empty labelSelector means deploy resources to all clusters
   350  			ClusterLabelSelector: map[string]string{},
   351  		}
   352  		properties, err := json.Marshal(clusterSelector)
   353  		if err != nil {
   354  			return err
   355  		}
   356  		policy := v1beta1.AppPolicy{
   357  			Name:       addonAllClusterPolicy,
   358  			Type:       v1alpha1.TopologyPolicyType,
   359  			Properties: &runtime.RawExtension{Raw: properties},
   360  		}
   361  		app.Spec.Policies = append(app.Spec.Policies, policy)
   362  	} else {
   363  		var found bool
   364  		for _, c := range deployClusters {
   365  			if c == multicluster.ClusterLocalName {
   366  				found = true
   367  				break
   368  			}
   369  		}
   370  		if !found {
   371  			deployClusters = append(deployClusters, multicluster.ClusterLocalName)
   372  		}
   373  		// deploy to specified clusters
   374  		if app.Spec.Policies == nil {
   375  			app.Spec.Policies = []v1beta1.AppPolicy{}
   376  		}
   377  		body, err := json.Marshal(map[string][]string{types.ClustersArg: deployClusters})
   378  		if err != nil {
   379  			return err
   380  		}
   381  		app.Spec.Policies = append(app.Spec.Policies, v1beta1.AppPolicy{
   382  			Name:       specifyAddonClustersTopologyPolicy,
   383  			Type:       v1alpha1.TopologyPolicyType,
   384  			Properties: &runtime.RawExtension{Raw: body},
   385  		})
   386  	}
   387  
   388  	return nil
   389  }
   390  
   391  func renderResources(addon *InstallPackage, args map[string]interface{}) ([]common2.ApplicationComponent, error) {
   392  	var resources []common2.ApplicationComponent
   393  	if len(addon.YAMLTemplates) != 0 {
   394  		comp, err := renderK8sObjectsComponent(addon.YAMLTemplates, addon.Name)
   395  		if err != nil {
   396  			return nil, errors.Wrapf(err, "render components from yaml template")
   397  		}
   398  		resources = append(resources, *comp)
   399  	}
   400  
   401  	for _, tmpl := range addon.CUETemplates {
   402  		isMainCueTemplate, err := checkCueFileHasPackageHeader(tmpl)
   403  		if err != nil {
   404  			return nil, err
   405  		}
   406  		if isMainCueTemplate {
   407  			continue
   408  		}
   409  		comp, err := renderCompAccordingCUETemplate(tmpl, addon, args)
   410  		if err != nil && strings.Contains(err.Error(), "var(path=output) not exist") {
   411  			continue
   412  		}
   413  		if err != nil {
   414  			return nil, NewAddonError(fmt.Sprintf("fail to render cue template %s", err.Error()))
   415  		}
   416  		resources = append(resources, *comp)
   417  	}
   418  	return resources, nil
   419  }
   420  
   421  // checkNeedAttachTopologyPolicy will check this addon want to deploy to runtime-cluster, but application template doesn't specify the
   422  // topology policy, then will attach the policy to application automatically.
   423  func checkNeedAttachTopologyPolicy(app *v1beta1.Application, addon *InstallPackage) bool {
   424  	// the cue template will not be attached topology policy for the white-box principle
   425  	if len(addon.AppCueTemplate.Data) != 0 {
   426  		return false
   427  	}
   428  	if !isDeployToRuntime(addon) {
   429  		return false
   430  	}
   431  	for _, policy := range app.Spec.Policies {
   432  		if policy.Type == v1alpha1.TopologyPolicyType {
   433  			klog.Warningf("deployTo in metadata will NOT have any effect. It conflicts with %s policy named %s. Consider removing deployTo field in addon metadata.", v1alpha1.TopologyPolicyType, policy.Name)
   434  			return false
   435  		}
   436  	}
   437  	return true
   438  }
   439  
   440  func isDeployToRuntime(addon *InstallPackage) bool {
   441  	if addon.DeployTo == nil {
   442  		return false
   443  	}
   444  	return addon.DeployTo.RuntimeCluster || addon.DeployTo.LegacyRuntimeCluster
   445  }
   446  
   447  func checkCueFileHasPackageHeader(cueTemplate ElementFile) (bool, error) {
   448  	cueFile, err := parser.ParseFile(cueTemplate.Name, cueTemplate.Data, parser.ParseComments)
   449  	if err != nil {
   450  		return false, err
   451  	}
   452  	if cueFile.PackageName() == defaultCuePackageHeader {
   453  		return true, nil
   454  	}
   455  	return false, nil
   456  }