github.com/oam-dev/kubevela@v1.9.11/pkg/cue/script/template.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 script
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/kubevela/pkg/cue/cuex"
    26  
    27  	cuelang "cuelang.org/go/cue"
    28  	"cuelang.org/go/cue/errors"
    29  	"github.com/kubevela/workflow/pkg/cue/model/value"
    30  
    31  	"github.com/oam-dev/kubevela/pkg/cue"
    32  	velacuex "github.com/oam-dev/kubevela/pkg/cue/cuex"
    33  )
    34  
    35  // CUE the cue script with the template format
    36  // Like this:
    37  // ------------
    38  // metadata: {}
    39  //
    40  //	template: {
    41  //		parameter: {}
    42  //		output: {}
    43  //	}
    44  //
    45  // ------------
    46  type CUE string
    47  
    48  // BuildCUEScriptWithDefaultContext build a cue script instance from a byte array.
    49  func BuildCUEScriptWithDefaultContext(defaultContext []byte, content []byte) CUE {
    50  	return CUE(content) + "\n" + CUE(defaultContext)
    51  }
    52  
    53  // ParseToValue parse the cue script to cue.Value
    54  func (c CUE) ParseToValue() (*value.Value, error) {
    55  	// the cue script must be first, it could include the imports
    56  	template := string(c) + "\n" + cue.BaseTemplate
    57  	v, err := value.NewValue(template, nil, "")
    58  	if err != nil {
    59  		return nil, fmt.Errorf("fail to parse the template:%w", err)
    60  	}
    61  	return v, nil
    62  }
    63  
    64  // ParseToValueWithCueX parse the cue script to cue.Value
    65  func (c CUE) ParseToValueWithCueX() (cuelang.Value, error) {
    66  	// the cue script must be first, it could include the imports
    67  	template := string(c) + "\n" + cue.BaseTemplate
    68  	val, err := velacuex.KubeVelaDefaultCompiler.Get().CompileStringWithOptions(context.Background(), template, cuex.DisableResolveProviderFunctions{})
    69  	if err != nil {
    70  		return cuelang.Value{}, fmt.Errorf("failed to compile config template: %w", err)
    71  	}
    72  	return val, nil
    73  }
    74  
    75  // ParseToTemplateValue parse the cue script to cue.Value. It must include a valid template.
    76  func (c CUE) ParseToTemplateValue() (*value.Value, error) {
    77  	// the cue script must be first, it could include the imports
    78  	template := string(c) + "\n" + cue.BaseTemplate
    79  	v, err := value.NewValue(template, nil, "")
    80  	if err != nil {
    81  		return nil, fmt.Errorf("fail to parse the template:%w", err)
    82  	}
    83  	_, err = v.LookupValue("template")
    84  	if err != nil {
    85  		if v.Error() != nil {
    86  			return nil, fmt.Errorf("the template cue is invalid:%w", v.Error())
    87  		}
    88  		return nil, fmt.Errorf("the template cue must include the template field:%w", err)
    89  	}
    90  	_, err = v.LookupValue("template", "parameter")
    91  	if err != nil {
    92  		return nil, fmt.Errorf("the template cue must include the template.parameter field")
    93  	}
    94  	return v, nil
    95  }
    96  
    97  // ParseToTemplateValueWithCueX parse the cue script to cue.Value. It must include a valid template.
    98  func (c CUE) ParseToTemplateValueWithCueX() (cuelang.Value, error) {
    99  	val, err := c.ParseToValueWithCueX()
   100  	if err != nil {
   101  		return cuelang.Value{}, err
   102  	}
   103  	templateValue := val.LookupPath(cuelang.ParsePath("template"))
   104  	if !templateValue.Exists() {
   105  		return cuelang.Value{}, fmt.Errorf("the template cue must include the template field")
   106  	}
   107  	tmplParamValue := val.LookupPath(cuelang.ParsePath("template.parameter"))
   108  	if !tmplParamValue.Exists() {
   109  		return cuelang.Value{}, fmt.Errorf("the template cue must include the template.parameter field")
   110  	}
   111  	return val, nil
   112  }
   113  
   114  // MergeValues merge the input values to the cue script
   115  // The context variables could be referenced in all fields.
   116  // The parameter only could be referenced in the template area.
   117  func (c CUE) MergeValues(context interface{}, properties map[string]interface{}) (*value.Value, error) {
   118  	parameterByte, err := json.Marshal(properties)
   119  	if err != nil {
   120  		return nil, fmt.Errorf("the parameter is invalid %w", err)
   121  	}
   122  	contextByte, err := json.Marshal(context)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("the context is invalid %w", err)
   125  	}
   126  	var script = strings.Builder{}
   127  	_, err = script.WriteString(string(c) + "\n")
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	if properties != nil {
   132  		_, err = script.WriteString(fmt.Sprintf("template: parameter: %s \n", string(parameterByte)))
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  	}
   137  	if context != nil {
   138  		_, err = script.WriteString(fmt.Sprintf("context: %s \n", string(contextByte)))
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  	}
   143  	mergeValue, err := value.NewValue(script.String(), nil, "")
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	if err := mergeValue.CueValue().Validate(); err != nil {
   148  		return nil, fmt.Errorf("fail to validate the merged value %w", err)
   149  	}
   150  	return mergeValue, nil
   151  }
   152  
   153  // RunAndOutput run the cue script and return the values of the specified field.
   154  // The output field must be under the template field.
   155  func (c CUE) RunAndOutput(context interface{}, properties map[string]interface{}, outputField ...string) (*value.Value, error) {
   156  	// Validate the properties
   157  	if err := c.ValidateProperties(properties); err != nil {
   158  		return nil, err
   159  	}
   160  	render, err := c.MergeValues(context, properties)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("fail to merge the properties to template %w", err)
   163  	}
   164  	if render.Error() != nil {
   165  		return nil, fmt.Errorf("fail to merge the properties to template %w", render.Error())
   166  	}
   167  	if len(outputField) == 0 {
   168  		outputField = []string{"template", "output"}
   169  	}
   170  	return render.LookupValue(outputField...)
   171  }
   172  
   173  // RunAndOutputWithCueX run the cue script and return the values of the specified field.
   174  // The output field must be under the template field.
   175  func (c CUE) RunAndOutputWithCueX(ctx context.Context, context interface{}, properties map[string]interface{}, outputField ...string) (cuelang.Value, error) {
   176  	// Validate the properties
   177  	if err := c.ValidatePropertiesWithCueX(properties); err != nil {
   178  		return cuelang.Value{}, err
   179  	}
   180  	contextOption := cuex.WithExtraData("context", context)
   181  	parameterOption := cuex.WithExtraData("template.parameter", properties)
   182  	val, err := velacuex.KubeVelaDefaultCompiler.Get().CompileStringWithOptions(ctx, string(c), contextOption, parameterOption)
   183  	if !val.Exists() {
   184  		return cuelang.Value{}, fmt.Errorf("failed to compile config template")
   185  	}
   186  	if err != nil {
   187  		return cuelang.Value{}, fmt.Errorf("failed to compile config template: %w", err)
   188  	}
   189  	if Error(val) != nil {
   190  		return cuelang.Value{}, fmt.Errorf("failed to compile config template: %w", Error(val))
   191  	}
   192  	if len(outputField) == 0 {
   193  		return val, nil
   194  	}
   195  	outputFieldVal := val.LookupPath(cuelang.ParsePath(strings.Join(outputField, ".")))
   196  	if !outputFieldVal.Exists() {
   197  		return cuelang.Value{}, fmt.Errorf("failed to lookup value: var(path=%s) not exist", strings.Join(outputField, "."))
   198  	}
   199  	return outputFieldVal, nil
   200  }
   201  
   202  // ValidateProperties validate the input properties by the template
   203  func (c CUE) ValidateProperties(properties map[string]interface{}) error {
   204  	template, err := c.ParseToTemplateValue()
   205  	if err != nil {
   206  		return err
   207  	}
   208  	parameter, err := template.LookupValue("template", "parameter")
   209  	if err != nil {
   210  		return err
   211  	}
   212  	parameterStr, err := parameter.String()
   213  	if err != nil {
   214  		return fmt.Errorf("the parameter is invalid %w", err)
   215  	}
   216  	propertiesByte, err := json.Marshal(properties)
   217  	if err != nil {
   218  		return fmt.Errorf("the properties is invalid %w", err)
   219  	}
   220  	newCue := strings.Builder{}
   221  	newCue.WriteString(parameterStr + "\n")
   222  	newCue.WriteString(string(propertiesByte) + "\n")
   223  	newValue, err := value.NewValue(newCue.String(), nil, "")
   224  	if err != nil {
   225  		return ConvertFieldError(err)
   226  	}
   227  	if err := newValue.CueValue().Validate(); err != nil {
   228  		return ConvertFieldError(err)
   229  	}
   230  	_, err = newValue.CueValue().MarshalJSON()
   231  	if err != nil {
   232  		return ConvertFieldError(err)
   233  	}
   234  	return nil
   235  }
   236  
   237  // ValidatePropertiesWithCueX validate the input properties by the template
   238  func (c CUE) ValidatePropertiesWithCueX(properties map[string]interface{}) error {
   239  	template, err := c.ParseToTemplateValueWithCueX()
   240  	if err != nil {
   241  		return err
   242  	}
   243  	paramPath := cuelang.ParsePath("template.parameter")
   244  	parameter := template.LookupPath(paramPath)
   245  	if !parameter.Exists() {
   246  		return fmt.Errorf("failed to lookup value: var(path=template.parameter) not exist")
   247  	}
   248  	props := parameter.FillPath(cuelang.ParsePath(""), properties)
   249  	if props.Err() != nil {
   250  		return ConvertFieldError(props.Err())
   251  	}
   252  	if err := props.Validate(); err != nil {
   253  		return ConvertFieldError(err)
   254  	}
   255  	_, err = props.MarshalJSON()
   256  	if err != nil {
   257  		return ConvertFieldError(err)
   258  	}
   259  	return nil
   260  }
   261  
   262  // ParameterError the error report of the parameter field validation
   263  type ParameterError struct {
   264  	Name    string
   265  	Message string
   266  }
   267  
   268  // Error return the error message
   269  func (e *ParameterError) Error() string {
   270  	return fmt.Sprintf("Field: %s Message: %s", e.Name, e.Message)
   271  }
   272  
   273  // ConvertFieldError convert the cue error to the field error
   274  func ConvertFieldError(err error) error {
   275  	var cueErr errors.Error
   276  	if errors.As(err, &cueErr) {
   277  		path := cueErr.Path()
   278  		fieldName := path[len(path)-1]
   279  		format, args := cueErr.Msg()
   280  		message := fmt.Sprintf(format, args...)
   281  		if strings.Contains(message, "cannot convert incomplete value") {
   282  			message = "This parameter is required"
   283  		}
   284  		return &ParameterError{
   285  			Name:    fieldName,
   286  			Message: message,
   287  		}
   288  	}
   289  	return err
   290  }
   291  
   292  // Error return value's error information.
   293  func Error(val cuelang.Value) error {
   294  	if !val.Exists() {
   295  		return errors.New("empty value")
   296  	}
   297  	if err := val.Err(); err != nil {
   298  		return err
   299  	}
   300  	var gerr error
   301  	val.Walk(func(value cuelang.Value) bool {
   302  		if err := value.Eval().Err(); err != nil {
   303  			gerr = err
   304  			return false
   305  		}
   306  		return true
   307  	}, nil)
   308  	return gerr
   309  }