github.com/oam-dev/kubevela@v1.9.11/pkg/definition/definition.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 contains some helper functions used in vela CLI 18 // and vela addon mechanism 19 package definition 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "strings" 26 27 "cuelang.org/go/cue" 28 "cuelang.org/go/cue/ast" 29 "cuelang.org/go/cue/cuecontext" 30 "cuelang.org/go/cue/format" 31 "cuelang.org/go/cue/parser" 32 "cuelang.org/go/encoding/gocode/gocodec" 33 "cuelang.org/go/tools/fix" 34 "github.com/pkg/errors" 35 "k8s.io/apimachinery/pkg/api/meta" 36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 "k8s.io/apimachinery/pkg/runtime" 38 "k8s.io/apimachinery/pkg/runtime/schema" 39 "k8s.io/client-go/rest" 40 "sigs.k8s.io/controller-runtime/pkg/client" 41 "sigs.k8s.io/yaml" 42 43 "github.com/kubevela/workflow/pkg/cue/model/sets" 44 "github.com/kubevela/workflow/pkg/cue/model/value" 45 "github.com/kubevela/workflow/pkg/cue/packages" 46 47 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 48 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 49 velacue "github.com/oam-dev/kubevela/pkg/cue" 50 "github.com/oam-dev/kubevela/pkg/oam" 51 "github.com/oam-dev/kubevela/pkg/utils" 52 "github.com/oam-dev/kubevela/pkg/utils/filters" 53 ) 54 55 const ( 56 // DescriptionKey the key for accessing definition description 57 DescriptionKey = "definition.oam.dev/description" 58 // AliasKey the key for accessing definition alias 59 AliasKey = "definition.oam.dev/alias" 60 // UserPrefix defines the prefix of user customized label or annotation 61 UserPrefix = "custom.definition.oam.dev/" 62 ) 63 64 // the names for different type of definition 65 const ( 66 componentDefType = "component" 67 traitDefType = "trait" 68 policyDefType = "policy" 69 workflowStepDefType = "workflow-step" 70 workloadDefType = "workload" 71 ) 72 73 var ( 74 // DefinitionTemplateKeys the keys for accessing definition template 75 DefinitionTemplateKeys = []string{"spec", "schematic", "cue", "template"} 76 // DefinitionTypeToKind maps the definition types to corresponding kinds 77 DefinitionTypeToKind = map[string]string{ 78 componentDefType: v1beta1.ComponentDefinitionKind, 79 traitDefType: v1beta1.TraitDefinitionKind, 80 policyDefType: v1beta1.PolicyDefinitionKind, 81 workloadDefType: v1beta1.WorkloadDefinitionKind, 82 workflowStepDefType: v1beta1.WorkflowStepDefinitionKind, 83 } 84 // StringToDefinitionType converts user input to DefinitionType used in DefinitionRevisions 85 StringToDefinitionType = map[string]common.DefinitionType{ 86 // component 87 componentDefType: common.ComponentType, 88 // trait 89 traitDefType: common.TraitType, 90 // policy 91 policyDefType: common.PolicyType, 92 // workflow-step 93 workflowStepDefType: common.WorkflowStepType, 94 } 95 // DefinitionKindToNameLabel records DefinitionRevision types and labels to search its name 96 DefinitionKindToNameLabel = map[common.DefinitionType]string{ 97 common.ComponentType: oam.LabelComponentDefinitionName, 98 common.TraitType: oam.LabelTraitDefinitionName, 99 common.PolicyType: oam.LabelPolicyDefinitionName, 100 common.WorkflowStepType: oam.LabelWorkflowStepDefinitionName, 101 } 102 // DefinitionKindToType maps the definition kinds to a shorter type 103 DefinitionKindToType = map[string]string{ 104 v1beta1.ComponentDefinitionKind: componentDefType, 105 v1beta1.TraitDefinitionKind: traitDefType, 106 v1beta1.PolicyDefinitionKind: policyDefType, 107 v1beta1.WorkloadDefinitionKind: workloadDefType, 108 v1beta1.WorkflowStepDefinitionKind: workflowStepDefType, 109 } 110 ) 111 112 // Definition the general struct for handling all kinds of definitions like ComponentDefinition or TraitDefinition 113 type Definition struct { 114 unstructured.Unstructured 115 } 116 117 // SetGVK set the GroupVersionKind of Definition 118 func (def *Definition) SetGVK(kind string) { 119 def.SetGroupVersionKind(schema.GroupVersionKind{ 120 Group: v1beta1.Group, 121 Version: v1beta1.Version, 122 Kind: kind, 123 }) 124 } 125 126 // GetType gets the type of Definition 127 func (def *Definition) GetType() string { 128 kind := def.GetKind() 129 for k, v := range DefinitionTypeToKind { 130 if v == kind { 131 return k 132 } 133 } 134 return strings.ToLower(strings.TrimSuffix(kind, "Definition")) 135 } 136 137 // SetType sets the type of Definition 138 func (def *Definition) SetType(t string) error { 139 kind, ok := DefinitionTypeToKind[t] 140 if !ok { 141 return fmt.Errorf("invalid type %s", t) 142 } 143 def.SetGVK(kind) 144 return nil 145 } 146 147 // ToCUE converts Definition to CUE value (with predefined Definition's cue format) 148 // nolint:staticcheck 149 func (def *Definition) ToCUE() (*cue.Value, string, error) { 150 annotations := map[string]string{} 151 for key, val := range def.GetAnnotations() { 152 if strings.HasPrefix(key, UserPrefix) { 153 annotations[strings.TrimPrefix(key, UserPrefix)] = val 154 } 155 } 156 alias := def.GetAnnotations()[AliasKey] 157 desc := def.GetAnnotations()[DescriptionKey] 158 labels := map[string]string{} 159 for key, val := range def.GetLabels() { 160 if strings.HasPrefix(key, UserPrefix) { 161 labels[strings.TrimPrefix(key, UserPrefix)] = val 162 } 163 } 164 spec := map[string]interface{}{} 165 for key, val := range def.Object["spec"].(map[string]interface{}) { 166 if key != "schematic" { 167 spec[key] = val 168 } 169 } 170 obj := map[string]interface{}{ 171 def.GetName(): map[string]interface{}{ 172 "type": def.GetType(), 173 "alias": alias, 174 "description": desc, 175 "annotations": annotations, 176 "labels": labels, 177 "attributes": spec, 178 }, 179 } 180 codec := gocodec.New((*cue.Runtime)(cuecontext.New()), &gocodec.Config{}) 181 val, err := codec.Decode(obj) 182 if err != nil { 183 return nil, "", err 184 } 185 186 templateString, _, err := unstructured.NestedString(def.Object, DefinitionTemplateKeys...) 187 if err != nil { 188 return nil, "", err 189 } 190 templateString, err = formatCUEString(templateString) 191 if err != nil { 192 return nil, "", err 193 } 194 return &val, templateString, nil 195 } 196 197 // ToCUEString converts definition to CUE value and then encode to string 198 func (def *Definition) ToCUEString() (string, error) { 199 val, templateString, err := def.ToCUE() 200 if err != nil { 201 return "", err 202 } 203 metadataString, err := sets.ToString(*val) 204 if err != nil { 205 return "", err 206 } 207 208 f, err := parser.ParseFile("-", templateString, parser.ParseComments) 209 if err != nil { 210 return "", errors.Wrapf(err, "failed to parse template cue string") 211 } 212 f = fix.File(f) 213 var importDecls, templateDecls []ast.Decl 214 for _, decl := range f.Decls { 215 if importDecl, ok := decl.(*ast.ImportDecl); ok { 216 importDecls = append(importDecls, importDecl) 217 } else { 218 templateDecls = append(templateDecls, decl) 219 } 220 } 221 importString, err := encodeDeclsToString(importDecls) 222 if err != nil { 223 return "", errors.Wrapf(err, "failed to encode import decls") 224 } 225 templateString, err = encodeDeclsToString(templateDecls) 226 if err != nil { 227 return "", errors.Wrapf(err, "failed to encode template decls") 228 } 229 templateString = fmt.Sprintf("template: {\n%s}", templateString) 230 231 completeCUEString := importString + "\n" + metadataString + "\n" + templateString 232 if completeCUEString, err = formatCUEString(completeCUEString); err != nil { 233 return "", errors.Wrapf(err, "failed to format cue format string") 234 } 235 return completeCUEString, nil 236 } 237 238 // FromCUE converts CUE value (predefined Definition's cue format) to Definition 239 // nolint:gocyclo,staticcheck 240 func (def *Definition) FromCUE(val *cue.Value, templateString string) error { 241 if def.Object == nil { 242 def.Object = map[string]interface{}{} 243 } 244 annotations := map[string]string{} 245 for k, v := range def.GetAnnotations() { 246 if !strings.HasPrefix(k, UserPrefix) && k != DescriptionKey { 247 annotations[k] = v 248 } 249 } 250 labels := map[string]string{} 251 for k, v := range def.GetLabels() { 252 if !strings.HasPrefix(k, UserPrefix) { 253 labels[k] = v 254 } 255 } 256 spec, ok := def.Object["spec"].(map[string]interface{}) 257 if !ok { 258 spec = map[string]interface{}{} 259 } 260 codec := gocodec.New(&cue.Runtime{}, &gocodec.Config{}) 261 nameFlag := false 262 fields, err := val.Fields() 263 if err != nil { 264 return err 265 } 266 for fields.Next() { 267 definitionName := fields.Label() 268 v := fields.Value() 269 if nameFlag { 270 return fmt.Errorf("duplicated definition name found, %s and %s", def.GetName(), definitionName) 271 } 272 nameFlag = true 273 def.SetName(definitionName) 274 _fields, err := v.Fields() 275 if err != nil { 276 return err 277 } 278 for _fields.Next() { 279 _key := _fields.Label() 280 _value := _fields.Value() 281 switch _key { 282 case "type": 283 _type, err := _value.String() 284 if err != nil { 285 return err 286 } 287 if err = def.SetType(_type); err != nil { 288 return err 289 } 290 case "alias": 291 alias, err := _value.String() 292 if err != nil { 293 return err 294 } 295 annotations[AliasKey] = alias 296 case "description": 297 desc, err := _value.String() 298 if err != nil { 299 return err 300 } 301 annotations[DescriptionKey] = desc 302 case "annotations": 303 var _annotations map[string]string 304 if err := codec.Encode(_value, &_annotations); err != nil { 305 return err 306 } 307 for _k, _v := range _annotations { 308 if strings.Contains(_k, "oam.dev") { 309 annotations[_k] = _v 310 } else { 311 annotations[UserPrefix+_k] = _v 312 } 313 } 314 case "labels": 315 var _labels map[string]string 316 if err := codec.Encode(_value, &_labels); err != nil { 317 return err 318 } 319 for _k, _v := range _labels { 320 if strings.Contains(_k, "oam.dev") { 321 labels[_k] = _v 322 } else { 323 labels[UserPrefix+_k] = _v 324 } 325 } 326 case "attributes": 327 if err := codec.Encode(_value, &spec); err != nil { 328 return err 329 } 330 } 331 } 332 } 333 def.SetAnnotations(annotations) 334 def.SetLabels(labels) 335 if err := unstructured.SetNestedField(spec, templateString, DefinitionTemplateKeys[1:]...); err != nil { 336 return err 337 } 338 if err = validateSpec(spec, def.GetType()); err != nil { 339 return fmt.Errorf("invalid definition spec: %w", err) 340 } 341 def.Object["spec"] = spec 342 return nil 343 } 344 345 func validateSpec(spec map[string]interface{}, t string) error { 346 bs, err := json.Marshal(spec) 347 if err != nil { 348 return err 349 } 350 var tpl interface{} 351 switch t { 352 case componentDefType: 353 tpl = &v1beta1.ComponentDefinitionSpec{} 354 case traitDefType: 355 tpl = &v1beta1.TraitDefinitionSpec{} 356 case policyDefType: 357 tpl = &v1beta1.PolicyDefinitionSpec{} 358 case workflowStepDefType: 359 tpl = &v1beta1.WorkflowStepDefinitionSpec{} 360 default: 361 } 362 if tpl != nil { 363 return utils.StrictUnmarshal(bs, tpl) 364 } 365 return nil 366 } 367 368 func encodeDeclsToString(decls []ast.Decl) (string, error) { 369 bs, err := format.Node(&ast.File{Decls: decls}, format.Simplify()) 370 if err != nil { 371 return "", fmt.Errorf("failed to encode cue: %w", err) 372 } 373 return strings.TrimSpace(string(bs)) + "\n", nil 374 } 375 376 // FromYAML converts yaml into Definition 377 func (def *Definition) FromYAML(data []byte) error { 378 return yaml.Unmarshal(data, def) 379 } 380 381 // FromCUEString converts cue string into Definition 382 func (def *Definition) FromCUEString(cueString string, config *rest.Config) error { 383 cuectx := cuecontext.New() 384 f, err := parser.ParseFile("-", cueString, parser.ParseComments) 385 if err != nil { 386 return err 387 } 388 n := fix.File(f) 389 var importDecls, metadataDecls, templateDecls []ast.Decl 390 for _, decl := range n.Decls { 391 if importDecl, ok := decl.(*ast.ImportDecl); ok { 392 importDecls = append(importDecls, importDecl) 393 } else if field, ok := decl.(*ast.Field); ok { 394 label := "" 395 switch l := field.Label.(type) { 396 case *ast.Ident: 397 label = l.Name 398 case *ast.BasicLit: 399 label = l.Value 400 } 401 if label == "" { 402 return errors.Errorf("found unexpected decl when parsing cue: %v", label) 403 } 404 if label == "template" { 405 if v, ok := field.Value.(*ast.StructLit); ok { 406 templateDecls = append(templateDecls, v.Elts...) 407 } else { 408 return errors.Errorf("unexpected decl found in template: %v", decl) 409 } 410 } else { 411 metadataDecls = append(metadataDecls, field) 412 } 413 } 414 } 415 if len(metadataDecls) == 0 { 416 return errors.Errorf("no metadata found, invalid") 417 } 418 if len(templateDecls) == 0 { 419 return errors.Errorf("no template found, invalid") 420 } 421 var importString, metadataString, templateString string 422 if importString, err = encodeDeclsToString(importDecls); err != nil { 423 return errors.Wrapf(err, "failed to encode import decls to string") 424 } 425 if metadataString, err = encodeDeclsToString(metadataDecls); err != nil { 426 return errors.Wrapf(err, "failed to encode metadata decls to string") 427 } 428 // notice that current template decls are concatenated without any blank lines which might be inconsistent with original cue file, but it would not affect the syntax 429 if templateString, err = encodeDeclsToString(templateDecls); err != nil { 430 return errors.Wrapf(err, "failed to encode template decls to string") 431 } 432 433 inst := cuectx.CompileString(metadataString) 434 if inst.Err() != nil { 435 return inst.Err() 436 } 437 templateString, err = formatCUEString(importString + templateString) 438 if err != nil { 439 return err 440 } 441 var pd *packages.PackageDiscover 442 // validate template 443 if config != nil { 444 pd, err = packages.NewPackageDiscover(config) 445 if err != nil { 446 return err 447 } 448 } 449 if _, err = value.NewValue(templateString+"\n"+velacue.BaseTemplate, pd, ""); err != nil { 450 return err 451 } 452 return def.FromCUE(&inst, templateString) 453 } 454 455 // ValidDefinitionTypes return the list of valid definition types 456 func ValidDefinitionTypes() []string { 457 var types []string 458 for k := range DefinitionTypeToKind { 459 types = append(types, k) 460 } 461 return types 462 } 463 464 // SearchDefinition search the Definition in k8s by traversing all possible results across types or namespaces 465 func SearchDefinition(c client.Client, definitionType, namespace string, additionalFilters ...filters.Filter) ([]unstructured.Unstructured, error) { 466 ctx := context.Background() 467 var kinds []string 468 if definitionType != "" { 469 kind, ok := DefinitionTypeToKind[definitionType] 470 if !ok { 471 return nil, fmt.Errorf("invalid definition type %s", kind) 472 } 473 kinds = []string{kind} 474 } else { 475 for _, kind := range DefinitionTypeToKind { 476 kinds = append(kinds, kind) 477 } 478 } 479 var listOptions []client.ListOption 480 if namespace != "" { 481 listOptions = []client.ListOption{client.InNamespace(namespace)} 482 } 483 var definitions []unstructured.Unstructured 484 for _, kind := range kinds { 485 objs := unstructured.UnstructuredList{} 486 objs.SetGroupVersionKind(schema.GroupVersionKind{ 487 Group: v1beta1.Group, 488 Version: v1beta1.Version, 489 Kind: kind + "List", 490 }) 491 if err := c.List(ctx, &objs, listOptions...); err != nil { 492 if meta.IsNoMatchError(err) { 493 continue 494 } 495 return nil, errors.Wrapf(err, "failed to get %s", kind) 496 } 497 498 // Apply filters to the object list 499 filteredList := filters.ApplyToList(objs, additionalFilters...) 500 501 definitions = append(definitions, filteredList.Items...) 502 } 503 return definitions, nil 504 } 505 506 // SearchDefinitionRevisions finds DefinitionRevisions. 507 // Use defName to filter DefinitionRevisions using the name of the underlying Definition. 508 // Empty defName will keep everything. 509 // Use defType to only keep DefinitionRevisions of the specified DefinitionType. 510 // Empty defType will search every possible type. 511 // Use rev to only keep the revision you want. rev=0 will keep every revision. 512 func SearchDefinitionRevisions(ctx context.Context, c client.Client, namespace string, 513 defName string, defType common.DefinitionType, rev int64) ([]v1beta1.DefinitionRevision, error) { 514 var nameLabels []string 515 516 if defName == "" { 517 // defName="" means we don't care about the underlying definition names. 518 // So, no need to add name labels, just use anything to let the loop run once. 519 nameLabels = append(nameLabels, "") 520 } else { 521 // Since different definitions have different labels for its name, we need to 522 // find the corresponding label for definition names, to match names later. 523 // Empty defType will give all possible name labels of DefinitionRevisions, 524 // so that we can search for DefinitionRevisions of all Definition types. 525 for k, v := range DefinitionKindToNameLabel { 526 if defType != "" && defType != k { 527 continue 528 } 529 nameLabels = append(nameLabels, v) 530 } 531 } 532 533 var defRev []v1beta1.DefinitionRevision 534 535 // Search DefinitionRevisions using each possible label 536 for _, l := range nameLabels { 537 var listOptions []client.ListOption 538 if namespace != "" { 539 listOptions = append(listOptions, client.InNamespace(namespace)) 540 } 541 // Using name label to find DefinitionRevisions with specified name. 542 if defName != "" { 543 listOptions = append(listOptions, client.MatchingLabels{ 544 l: defName, 545 }) 546 } 547 548 objs := v1beta1.DefinitionRevisionList{} 549 objs.SetGroupVersionKind(schema.GroupVersionKind{ 550 Group: v1beta1.Group, 551 Version: v1beta1.Version, 552 Kind: v1beta1.DefinitionRevisionKind, 553 }) 554 555 // Search for DefinitionRevisions 556 if err := c.List(ctx, &objs, listOptions...); err != nil { 557 return nil, errors.Wrapf(err, "failed to list DefinitionRevisions of %s", defName) 558 } 559 560 for _, dr := range objs.Items { 561 // Keep only the specified type 562 if defType != "" && defType != dr.Spec.DefinitionType { 563 continue 564 } 565 // Only give the revision that the user wants 566 if rev != 0 && rev != dr.Spec.Revision { 567 continue 568 } 569 defRev = append(defRev, dr) 570 } 571 } 572 573 return defRev, nil 574 } 575 576 // GetDefinitionFromDefinitionRevision will extract the underlying Definition from a DefinitionRevision. 577 func GetDefinitionFromDefinitionRevision(rev *v1beta1.DefinitionRevision) (*Definition, error) { 578 var def *Definition 579 var u map[string]interface{} 580 var err error 581 582 switch rev.Spec.DefinitionType { 583 case common.ComponentType: 584 u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.ComponentDefinition) 585 case common.TraitType: 586 u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.TraitDefinition) 587 case common.PolicyType: 588 u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.PolicyDefinition) 589 case common.WorkflowStepType: 590 u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.WorkflowStepDefinition) 591 default: 592 return nil, fmt.Errorf("unsupported definition type: %s", rev.Spec.DefinitionType) 593 } 594 595 if err != nil { 596 return nil, err 597 } 598 599 def = &Definition{Unstructured: unstructured.Unstructured{Object: u}} 600 601 return def, nil 602 } 603 604 // GetDefinitionDefaultSpec returns the default spec of Definition with given kind. This may be implemented with cue in the future. 605 func GetDefinitionDefaultSpec(kind string) map[string]interface{} { 606 switch kind { 607 case v1beta1.ComponentDefinitionKind: 608 return map[string]interface{}{ 609 "workload": map[string]interface{}{ 610 "definition": map[string]interface{}{ 611 "apiVersion": "<change me> apps/v1", 612 "kind": "<change me> Deployment", 613 }, 614 }, 615 "schematic": map[string]interface{}{ 616 "cue": map[string]interface{}{ 617 "template": "output: {}\nparameter: {}\n", 618 }, 619 }, 620 } 621 case v1beta1.TraitDefinitionKind: 622 return map[string]interface{}{ 623 "appliesToWorkloads": []interface{}{}, 624 "conflictsWith": []interface{}{}, 625 "workloadRefPath": "", 626 "definitionRef": map[string]interface{}{}, 627 "podDisruptive": false, 628 "schematic": map[string]interface{}{ 629 "cue": map[string]interface{}{ 630 "template": "patch: {}\nparameter: {}\n", 631 }, 632 }, 633 } 634 } 635 return map[string]interface{}{} 636 } 637 638 func formatCUEString(cueString string) (string, error) { 639 f, err := parser.ParseFile("-", cueString, parser.ParseComments) 640 if err != nil { 641 return "", errors.Wrapf(err, "failed to parse file during format cue string") 642 } 643 n := fix.File(f) 644 b, err := format.Node(n, format.Simplify()) 645 if err != nil { 646 return "", errors.Wrapf(err, "failed to format node during formating cue string") 647 } 648 return string(b), nil 649 }