github.com/oam-dev/kubevela@v1.9.11/pkg/cue/definition/template.go (about)

     1  /*
     2  Copyright 2021 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 definition
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  
    24  	"cuelang.org/go/cue"
    25  	"cuelang.org/go/cue/build"
    26  	"cuelang.org/go/cue/cuecontext"
    27  	"github.com/kubevela/pkg/multicluster"
    28  
    29  	"github.com/pkg/errors"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	"github.com/kubevela/workflow/pkg/cue/model"
    34  	"github.com/kubevela/workflow/pkg/cue/model/sets"
    35  	"github.com/kubevela/workflow/pkg/cue/model/value"
    36  	"github.com/kubevela/workflow/pkg/cue/packages"
    37  	"github.com/kubevela/workflow/pkg/cue/process"
    38  
    39  	velaprocess "github.com/oam-dev/kubevela/pkg/cue/process"
    40  	"github.com/oam-dev/kubevela/pkg/cue/task"
    41  	"github.com/oam-dev/kubevela/pkg/oam"
    42  	"github.com/oam-dev/kubevela/pkg/oam/util"
    43  )
    44  
    45  const (
    46  	// OutputFieldName is the name of the struct contains the CR data
    47  	OutputFieldName = velaprocess.OutputFieldName
    48  	// OutputsFieldName is the name of the struct contains the map[string]CR data
    49  	OutputsFieldName = velaprocess.OutputsFieldName
    50  	// PatchFieldName is the name of the struct contains the patch of CR data
    51  	PatchFieldName = "patch"
    52  	// PatchOutputsFieldName is the name of the struct contains the patch of outputs CR data
    53  	PatchOutputsFieldName = "patchOutputs"
    54  	// CustomMessage defines the custom message in definition template
    55  	CustomMessage = "message"
    56  	// HealthCheckPolicy defines the health check policy in definition template
    57  	HealthCheckPolicy = "isHealth"
    58  	// ErrsFieldName check if errors contained in the cue
    59  	ErrsFieldName = "errs"
    60  )
    61  
    62  const (
    63  	// AuxiliaryWorkload defines the extra workload obj from a workloadDefinition,
    64  	// e.g. a workload composed by deployment and service, the service will be marked as AuxiliaryWorkload
    65  	AuxiliaryWorkload = "AuxiliaryWorkload"
    66  )
    67  
    68  // AbstractEngine defines Definition's Render interface
    69  type AbstractEngine interface {
    70  	Complete(ctx process.Context, abstractTemplate string, params interface{}) error
    71  	HealthCheck(templateContext map[string]interface{}, healthPolicyTemplate string, parameter interface{}) (bool, error)
    72  	Status(templateContext map[string]interface{}, customStatusTemplate string, parameter interface{}) (string, error)
    73  	GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error)
    74  }
    75  
    76  type def struct {
    77  	name string
    78  	pd   *packages.PackageDiscover
    79  }
    80  
    81  type workloadDef struct {
    82  	def
    83  }
    84  
    85  // NewWorkloadAbstractEngine create Workload Definition AbstractEngine
    86  func NewWorkloadAbstractEngine(name string, pd *packages.PackageDiscover) AbstractEngine {
    87  	return &workloadDef{
    88  		def: def{
    89  			name: name,
    90  			pd:   pd,
    91  		},
    92  	}
    93  }
    94  
    95  // Complete do workload definition's rendering
    96  func (wd *workloadDef) Complete(ctx process.Context, abstractTemplate string, params interface{}) error {
    97  	bi := build.NewContext().NewInstance("", nil)
    98  	if err := value.AddFile(bi, "-", renderTemplate(abstractTemplate)); err != nil {
    99  		return errors.WithMessagef(err, "invalid cue template of workload %s", wd.name)
   100  	}
   101  	var paramFile = velaprocess.ParameterFieldName + ": {}"
   102  	if params != nil {
   103  		bt, err := json.Marshal(params)
   104  		if err != nil {
   105  			return errors.WithMessagef(err, "marshal parameter of workload %s", wd.name)
   106  		}
   107  		if string(bt) != "null" {
   108  			paramFile = fmt.Sprintf("%s: %s", velaprocess.ParameterFieldName, string(bt))
   109  		}
   110  	}
   111  	if err := value.AddFile(bi, velaprocess.ParameterFieldName, paramFile); err != nil {
   112  		return errors.WithMessagef(err, "invalid parameter of workload %s", wd.name)
   113  	}
   114  
   115  	c, err := ctx.BaseContextFile()
   116  	if err != nil {
   117  		return err
   118  	}
   119  	if err := value.AddFile(bi, "context", c); err != nil {
   120  		return err
   121  	}
   122  
   123  	val, err := wd.pd.ImportPackagesAndBuildValue(bi)
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	if err := val.Validate(); err != nil {
   129  		return errors.WithMessagef(err, "invalid cue template of workload %s after merge parameter and context", wd.name)
   130  	}
   131  	output := val.LookupPath(value.FieldPath(OutputFieldName))
   132  	base, err := model.NewBase(output)
   133  	if err != nil {
   134  		return errors.WithMessagef(err, "invalid output of workload %s", wd.name)
   135  	}
   136  	if err := ctx.SetBase(base); err != nil {
   137  		return err
   138  	}
   139  
   140  	// we will support outputs for workload composition, and it will become trait in AppConfig.
   141  	outputs := val.LookupPath(value.FieldPath(OutputsFieldName))
   142  	if !outputs.Exists() {
   143  		return nil
   144  	}
   145  	iter, err := outputs.Fields(cue.Definitions(true), cue.Hidden(true), cue.All())
   146  	if err != nil {
   147  		return errors.WithMessagef(err, "invalid outputs of workload %s", wd.name)
   148  	}
   149  	for iter.Next() {
   150  		if iter.Selector().IsDefinition() || iter.Selector().PkgPath() != "" || iter.IsOptional() {
   151  			continue
   152  		}
   153  		other, err := model.NewOther(iter.Value())
   154  		name := iter.Label()
   155  		if err != nil {
   156  			return errors.WithMessagef(err, "invalid outputs(%s) of workload %s", name, wd.name)
   157  		}
   158  		if err := ctx.AppendAuxiliaries(process.Auxiliary{Ins: other, Type: AuxiliaryWorkload, Name: name}); err != nil {
   159  			return err
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  func withCluster(ctx context.Context, o client.Object) context.Context {
   166  	if cluster := oam.GetCluster(o); cluster != "" {
   167  		return multicluster.WithCluster(ctx, cluster)
   168  	}
   169  	return ctx
   170  }
   171  
   172  func (wd *workloadDef) getTemplateContext(ctx process.Context, cli client.Reader, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
   173  	baseLabels := GetBaseContextLabels(ctx)
   174  	var root = initRoot(baseLabels)
   175  	var commonLabels = GetCommonLabels(baseLabels)
   176  
   177  	base, assists := ctx.Output()
   178  	componentWorkload, err := base.Unstructured()
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	// workload main resource will have a unique label("app.oam.dev/resourceType"="WORKLOAD") in per component/app level
   183  	_ctx := withCluster(ctx.GetCtx(), componentWorkload)
   184  	object, err := getResourceFromObj(_ctx, ctx, componentWorkload, cli, accessor.For(componentWorkload), util.MergeMapOverrideWithDst(map[string]string{
   185  		oam.LabelOAMResourceType: oam.ResourceTypeWorkload,
   186  	}, commonLabels), "")
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	root[OutputFieldName] = object
   191  	outputs := make(map[string]interface{})
   192  	for _, assist := range assists {
   193  		if assist.Type != AuxiliaryWorkload {
   194  			continue
   195  		}
   196  		if assist.Name == "" {
   197  			return nil, errors.New("the auxiliary of workload must have a name with format 'outputs.<my-name>'")
   198  		}
   199  		traitRef, err := assist.Ins.Unstructured()
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  		// AuxiliaryWorkload will have a unique label("trait.oam.dev/resource"="name of outputs") in per component/app level
   204  		_ctx := withCluster(ctx.GetCtx(), traitRef)
   205  		object, err := getResourceFromObj(_ctx, ctx, traitRef, cli, accessor.For(traitRef), util.MergeMapOverrideWithDst(map[string]string{
   206  			oam.TraitTypeLabel: AuxiliaryWorkload,
   207  		}, commonLabels), assist.Name)
   208  		if err != nil {
   209  			return nil, err
   210  		}
   211  		outputs[assist.Name] = object
   212  	}
   213  	if len(outputs) > 0 {
   214  		root[OutputsFieldName] = outputs
   215  	}
   216  	return root, nil
   217  }
   218  
   219  func formatRuntimeContext(templateContext map[string]interface{}, parameter interface{}) (string, error) {
   220  	var paramBuff = "parameter: {}\n"
   221  
   222  	bt, err := json.Marshal(templateContext)
   223  	if err != nil {
   224  		return "", errors.WithMessage(err, "json marshal template context")
   225  	}
   226  	ctxBuff := "context: " + string(bt) + "\n"
   227  
   228  	bt, err = json.Marshal(parameter)
   229  	if err != nil {
   230  		return "", errors.WithMessage(err, "json marshal template parameters")
   231  	}
   232  	if string(bt) != "null" {
   233  		paramBuff = "parameter: " + string(bt) + "\n"
   234  	}
   235  	return ctxBuff + paramBuff, nil
   236  }
   237  
   238  // HealthCheck address health check for workload
   239  func (wd *workloadDef) HealthCheck(templateContext map[string]interface{}, healthPolicyTemplate string, parameter interface{}) (bool, error) {
   240  	return checkHealth(templateContext, healthPolicyTemplate, parameter)
   241  }
   242  
   243  func checkHealth(templateContext map[string]interface{}, healthPolicyTemplate string, parameter interface{}) (bool, error) {
   244  	if healthPolicyTemplate == "" {
   245  		return true, nil
   246  	}
   247  	runtimeContextBuff, err := formatRuntimeContext(templateContext, parameter)
   248  	if err != nil {
   249  		return false, err
   250  	}
   251  	var buff = healthPolicyTemplate + "\n" + runtimeContextBuff
   252  
   253  	val := cuecontext.New().CompileString(buff)
   254  	healthy, err := val.LookupPath(value.FieldPath(HealthCheckPolicy)).Bool()
   255  	if err != nil {
   256  		return false, errors.WithMessage(err, "evaluate health status")
   257  	}
   258  	return healthy, nil
   259  }
   260  
   261  // Status get workload status by customStatusTemplate
   262  func (wd *workloadDef) Status(templateContext map[string]interface{}, customStatusTemplate string, parameter interface{}) (string, error) {
   263  	return getStatusMessage(wd.pd, templateContext, customStatusTemplate, parameter)
   264  }
   265  
   266  func getStatusMessage(pd *packages.PackageDiscover, templateContext map[string]interface{}, customStatusTemplate string, parameter interface{}) (string, error) {
   267  	if customStatusTemplate == "" {
   268  		return "", nil
   269  	}
   270  	runtimeContextBuff, err := formatRuntimeContext(templateContext, parameter)
   271  	if err != nil {
   272  		return "", err
   273  	}
   274  	var buff = customStatusTemplate + "\n" + runtimeContextBuff
   275  
   276  	val, err := value.NewValue(buff, pd, "")
   277  	if err != nil {
   278  		return "", errors.WithMessage(err, "compile status template")
   279  	}
   280  	message, err := val.CueValue().LookupPath(value.FieldPath(CustomMessage)).String()
   281  	if err != nil {
   282  		return "", errors.WithMessage(err, "evaluate customStatus.message")
   283  	}
   284  	return message, nil
   285  }
   286  
   287  func (wd *workloadDef) GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
   288  	return wd.getTemplateContext(ctx, cli, accessor)
   289  }
   290  
   291  type traitDef struct {
   292  	def
   293  }
   294  
   295  // NewTraitAbstractEngine create Trait Definition AbstractEngine
   296  func NewTraitAbstractEngine(name string, pd *packages.PackageDiscover) AbstractEngine {
   297  	return &traitDef{
   298  		def: def{
   299  			name: name,
   300  			pd:   pd,
   301  		},
   302  	}
   303  }
   304  
   305  // Complete do trait definition's rendering
   306  // nolint:gocyclo
   307  func (td *traitDef) Complete(ctx process.Context, abstractTemplate string, params interface{}) error {
   308  	bi := build.NewContext().NewInstance("", nil)
   309  	buff := abstractTemplate + "\n"
   310  	if params != nil {
   311  		bt, err := json.Marshal(params)
   312  		if err != nil {
   313  			return errors.WithMessagef(err, "marshal parameter of trait %s", td.name)
   314  		}
   315  		if string(bt) != "null" {
   316  			buff += fmt.Sprintf("%s: %s\n", velaprocess.ParameterFieldName, string(bt))
   317  		}
   318  	}
   319  	c, err := ctx.BaseContextFile()
   320  	if err != nil {
   321  		return err
   322  	}
   323  	buff += c
   324  	if err := value.AddFile(bi, "-", buff); err != nil {
   325  		return errors.WithMessagef(err, "invalid context of trait %s", td.name)
   326  	}
   327  
   328  	val, err := td.pd.ImportPackagesAndBuildValue(bi)
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	if err := val.Validate(); err != nil {
   334  		return errors.WithMessagef(err, "invalid template of trait %s after merge with parameter and context", td.name)
   335  	}
   336  	processing := val.LookupPath(value.FieldPath("processing"))
   337  	if processing.Exists() {
   338  		if val, err = task.Process(val); err != nil {
   339  			return errors.WithMessagef(err, "invalid process of trait %s", td.name)
   340  		}
   341  	}
   342  	outputs := val.LookupPath(value.FieldPath(OutputsFieldName))
   343  	if outputs.Exists() {
   344  		iter, err := outputs.Fields(cue.Definitions(true), cue.Hidden(true), cue.All())
   345  		if err != nil {
   346  			return errors.WithMessagef(err, "invalid outputs of trait %s", td.name)
   347  		}
   348  		for iter.Next() {
   349  			if iter.Selector().IsDefinition() || iter.Selector().PkgPath() != "" || iter.IsOptional() {
   350  				continue
   351  			}
   352  			other, err := model.NewOther(iter.Value())
   353  			name := iter.Label()
   354  			if err != nil {
   355  				return errors.WithMessagef(err, "invalid outputs(resource=%s) of trait %s", name, td.name)
   356  			}
   357  			if err := ctx.AppendAuxiliaries(process.Auxiliary{Ins: other, Type: td.name, Name: name}); err != nil {
   358  				return err
   359  			}
   360  		}
   361  	}
   362  
   363  	patcher := val.LookupPath(value.FieldPath(PatchFieldName))
   364  	base, auxiliaries := ctx.Output()
   365  	if patcher.Exists() {
   366  		if base == nil {
   367  			return fmt.Errorf("patch trait %s into an invalid workload", td.name)
   368  		}
   369  		if err := base.Unify(patcher, sets.CreateUnifyOptionsForPatcher(patcher)...); err != nil {
   370  			return errors.WithMessagef(err, "invalid patch trait %s into workload", td.name)
   371  		}
   372  	}
   373  	outputsPatcher := val.LookupPath(value.FieldPath(PatchOutputsFieldName))
   374  	if outputsPatcher.Exists() {
   375  		for _, auxiliary := range auxiliaries {
   376  			target := outputsPatcher.LookupPath(value.FieldPath(auxiliary.Name))
   377  			if !target.Exists() {
   378  				continue
   379  			}
   380  			if err = auxiliary.Ins.Unify(target); err != nil {
   381  				return errors.WithMessagef(err, "trait=%s, to=%s, invalid patch trait into auxiliary workload", td.name, auxiliary.Name)
   382  			}
   383  		}
   384  	}
   385  
   386  	errs := val.LookupPath(value.FieldPath(ErrsFieldName))
   387  	if errs.Exists() {
   388  		if err := parseErrors(errs); err != nil {
   389  			return err
   390  		}
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  func parseErrors(errs cue.Value) error {
   397  	if it, e := errs.List(); e == nil {
   398  		for it.Next() {
   399  			if s, err := it.Value().String(); err == nil && s != "" {
   400  				return errors.Errorf(s)
   401  			}
   402  		}
   403  	}
   404  	return nil
   405  }
   406  
   407  // GetCommonLabels will convert context based labels to OAM standard labels
   408  func GetCommonLabels(contextLabels map[string]string) map[string]string {
   409  	var commonLabels = map[string]string{}
   410  	for k, v := range contextLabels {
   411  		switch k {
   412  		case velaprocess.ContextAppName:
   413  			commonLabels[oam.LabelAppName] = v
   414  		case velaprocess.ContextName:
   415  			commonLabels[oam.LabelAppComponent] = v
   416  		case velaprocess.ContextAppRevision:
   417  			commonLabels[oam.LabelAppRevision] = v
   418  		case velaprocess.ContextReplicaKey:
   419  			commonLabels[oam.LabelReplicaKey] = v
   420  
   421  		}
   422  	}
   423  	return commonLabels
   424  }
   425  
   426  // GetBaseContextLabels get base context labels
   427  func GetBaseContextLabels(ctx process.Context) map[string]string {
   428  	baseLabels := ctx.BaseContextLabels()
   429  	baseLabels[velaprocess.ContextAppName] = ctx.GetData(velaprocess.ContextAppName).(string)
   430  	baseLabels[velaprocess.ContextAppRevision] = ctx.GetData(velaprocess.ContextAppRevision).(string)
   431  
   432  	return baseLabels
   433  }
   434  
   435  func initRoot(contextLabels map[string]string) map[string]interface{} {
   436  	var root = map[string]interface{}{}
   437  	for k, v := range contextLabels {
   438  		root[k] = v
   439  	}
   440  	return root
   441  }
   442  
   443  func renderTemplate(templ string) string {
   444  	return templ + `
   445  context: _
   446  parameter: _
   447  `
   448  }
   449  
   450  func (td *traitDef) getTemplateContext(ctx process.Context, cli client.Reader, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
   451  	baseLabels := GetBaseContextLabels(ctx)
   452  	var root = initRoot(baseLabels)
   453  	var commonLabels = GetCommonLabels(baseLabels)
   454  
   455  	_, assists := ctx.Output()
   456  	outputs := make(map[string]interface{})
   457  	for _, assist := range assists {
   458  		if assist.Type != td.name {
   459  			continue
   460  		}
   461  		traitRef, err := assist.Ins.Unstructured()
   462  		if err != nil {
   463  			return nil, err
   464  		}
   465  		_ctx := withCluster(ctx.GetCtx(), traitRef)
   466  		object, err := getResourceFromObj(_ctx, ctx, traitRef, cli, accessor.For(traitRef), util.MergeMapOverrideWithDst(map[string]string{
   467  			oam.TraitTypeLabel: assist.Type,
   468  		}, commonLabels), assist.Name)
   469  		if err != nil {
   470  			return nil, err
   471  		}
   472  		outputs[assist.Name] = object
   473  	}
   474  	if len(outputs) > 0 {
   475  		root[OutputsFieldName] = outputs
   476  	}
   477  	return root, nil
   478  }
   479  
   480  // Status get trait status by customStatusTemplate
   481  func (td *traitDef) Status(templateContext map[string]interface{}, customStatusTemplate string, parameter interface{}) (string, error) {
   482  	return getStatusMessage(td.pd, templateContext, customStatusTemplate, parameter)
   483  }
   484  
   485  // HealthCheck address health check for trait
   486  func (td *traitDef) HealthCheck(templateContext map[string]interface{}, healthPolicyTemplate string, parameter interface{}) (bool, error) {
   487  	return checkHealth(templateContext, healthPolicyTemplate, parameter)
   488  }
   489  
   490  func (td *traitDef) GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
   491  	return td.getTemplateContext(ctx, cli, accessor)
   492  }
   493  
   494  func getResourceFromObj(ctx context.Context, pctx process.Context, obj *unstructured.Unstructured, client client.Reader, namespace string, labels map[string]string, outputsResource string) (map[string]interface{}, error) {
   495  	if outputsResource != "" {
   496  		labels[oam.TraitResource] = outputsResource
   497  	}
   498  	if obj.GetName() != "" {
   499  		u, err := util.GetObjectGivenGVKAndName(ctx, client, obj.GroupVersionKind(), namespace, obj.GetName())
   500  		if err != nil {
   501  			return nil, err
   502  		}
   503  		return u.Object, nil
   504  	}
   505  	if ctxName := pctx.GetData(model.ContextName).(string); ctxName != "" {
   506  		u, err := util.GetObjectGivenGVKAndName(ctx, client, obj.GroupVersionKind(), namespace, ctxName)
   507  		if err == nil {
   508  			return u.Object, nil
   509  		}
   510  	}
   511  	list, err := util.GetObjectsGivenGVKAndLabels(ctx, client, obj.GroupVersionKind(), namespace, labels)
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  	if len(list.Items) == 1 {
   516  		return list.Items[0].Object, nil
   517  	}
   518  	for _, v := range list.Items {
   519  		if v.GetLabels()[oam.TraitResource] == outputsResource {
   520  			return v.Object, nil
   521  		}
   522  	}
   523  	return nil, errors.Errorf("no resources found gvk(%v) labels(%v)", obj.GroupVersionKind(), labels)
   524  }