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 }