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 }