github.com/oam-dev/kubevela@v1.9.11/pkg/controller/utils/capability.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 18 package utils 19 20 import ( 21 "context" 22 "fmt" 23 "os" 24 "path/filepath" 25 "strings" 26 27 "github.com/getkin/kin-openapi/openapi3" 28 "github.com/go-git/go-git/v5" 29 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" 30 "github.com/pkg/errors" 31 v1 "k8s.io/api/core/v1" 32 apierrors "k8s.io/apimachinery/pkg/api/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 k8stypes "k8s.io/apimachinery/pkg/types" 35 "k8s.io/klog/v2" 36 "k8s.io/utils/pointer" 37 "sigs.k8s.io/controller-runtime/pkg/client" 38 39 commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 40 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 41 "github.com/oam-dev/kubevela/apis/types" 42 "github.com/oam-dev/kubevela/pkg/appfile" 43 "github.com/oam-dev/kubevela/pkg/cue/script" 44 "github.com/oam-dev/kubevela/pkg/oam/util" 45 "github.com/oam-dev/kubevela/pkg/utils/common" 46 "github.com/oam-dev/kubevela/pkg/utils/terraform" 47 ) 48 49 // data types of parameter value 50 const ( 51 TerraformVariableString string = "string" 52 TerraformVariableNumber string = "number" 53 TerraformVariableBool string = "bool" 54 TerraformVariableList string = "list" 55 TerraformVariableTuple string = "tuple" 56 TerraformVariableMap string = "map" 57 TerraformVariableObject string = "object" 58 TerraformVariableNull string = "" 59 TerraformVariableAny string = "any" 60 61 TerraformListTypePrefix string = "list(" 62 TerraformTupleTypePrefix string = "tuple(" 63 TerraformMapTypePrefix string = "map(" 64 TerraformObjectTypePrefix string = "object(" 65 TerraformSetTypePrefix string = "set(" 66 67 typeTraitDefinition = "trait" 68 typeComponentDefinition = "component" 69 typeWorkflowStepDefinition = "workflowstep" 70 typePolicyStepDefinition = "policy" 71 ) 72 73 const ( 74 // GitCredsKnownHosts is a key in git credentials secret 75 GitCredsKnownHosts string = "known_hosts" 76 ) 77 78 // ErrNoSectionParameterInCue means there is not parameter section in Cue template of a workload 79 type ErrNoSectionParameterInCue struct { 80 capName string 81 } 82 83 func (e ErrNoSectionParameterInCue) Error() string { 84 return fmt.Sprintf("capability %s doesn't contain section `parameter`", e.capName) 85 } 86 87 // CapabilityDefinitionInterface is the interface for Capability (WorkloadDefinition and TraitDefinition) 88 type CapabilityDefinitionInterface interface { 89 GetCapabilityObject(ctx context.Context, k8sClient client.Client, namespace, name string) (*types.Capability, error) 90 GetOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name string) ([]byte, error) 91 } 92 93 // CapabilityComponentDefinition is the struct for ComponentDefinition 94 type CapabilityComponentDefinition struct { 95 Name string `json:"name"` 96 ComponentDefinition v1beta1.ComponentDefinition `json:"componentDefinition"` 97 98 WorkloadType util.WorkloadType `json:"workloadType"` 99 WorkloadDefName string `json:"workloadDefName"` 100 101 Terraform *commontypes.Terraform `json:"terraform"` 102 CapabilityBaseDefinition 103 } 104 105 // NewCapabilityComponentDef will create a CapabilityComponentDefinition 106 func NewCapabilityComponentDef(componentDefinition *v1beta1.ComponentDefinition) CapabilityComponentDefinition { 107 var def CapabilityComponentDefinition 108 def.Name = componentDefinition.Name 109 if componentDefinition.Spec.Workload.Definition == (commontypes.WorkloadGVK{}) && componentDefinition.Spec.Workload.Type != "" { 110 def.WorkloadType = util.ReferWorkload 111 def.WorkloadDefName = componentDefinition.Spec.Workload.Type 112 } 113 if componentDefinition.Spec.Schematic != nil { 114 if componentDefinition.Spec.Schematic.Terraform != nil { 115 def.WorkloadType = util.TerraformDef 116 def.Terraform = componentDefinition.Spec.Schematic.Terraform 117 } 118 } 119 def.ComponentDefinition = *componentDefinition.DeepCopy() 120 return def 121 } 122 123 // GetOpenAPISchema gets OpenAPI v3 schema by WorkloadDefinition name 124 func (def *CapabilityComponentDefinition) GetOpenAPISchema(name string) ([]byte, error) { 125 capability, err := appfile.ConvertTemplateJSON2Object(name, def.ComponentDefinition.Spec.Extension, def.ComponentDefinition.Spec.Schematic) 126 if err != nil { 127 return nil, fmt.Errorf("failed to convert ComponentDefinition to Capability Object") 128 } 129 return getOpenAPISchema(capability) 130 } 131 132 // GetOpenAPISchemaFromTerraformComponentDefinition gets OpenAPI v3 schema by WorkloadDefinition name 133 func GetOpenAPISchemaFromTerraformComponentDefinition(configuration string) ([]byte, error) { 134 schemas := make(map[string]*openapi3.Schema) 135 var required []string 136 variables, _, err := common.ParseTerraformVariables(configuration) 137 if err != nil { 138 return nil, errors.Wrap(err, "failed to generate capability properties") 139 } 140 for k, v := range variables { 141 var schema *openapi3.Schema 142 switch v.Type { 143 case TerraformVariableString: 144 schema = openapi3.NewStringSchema() 145 case TerraformVariableNumber: 146 schema = openapi3.NewFloat64Schema() 147 case TerraformVariableBool: 148 schema = openapi3.NewBoolSchema() 149 case TerraformVariableList, TerraformVariableTuple: 150 schema = openapi3.NewArraySchema() 151 case TerraformVariableMap, TerraformVariableObject: 152 schema = openapi3.NewObjectSchema() 153 case TerraformVariableAny: 154 switch v.Default.(type) { 155 case []interface{}: 156 schema = openapi3.NewArraySchema() 157 case map[string]interface{}: 158 schema = openapi3.NewObjectSchema() 159 } 160 case TerraformVariableNull: 161 switch v.Default.(type) { 162 case nil, string: 163 schema = openapi3.NewStringSchema() 164 case []interface{}: 165 schema = openapi3.NewArraySchema() 166 case map[string]interface{}: 167 schema = openapi3.NewObjectSchema() 168 case int, float64: 169 schema = openapi3.NewFloat64Schema() 170 default: 171 return nil, fmt.Errorf("null type variable is NOT supported, please specify a type for the variable: %s", v.Name) 172 } 173 } 174 175 // To identify unusual list type 176 if schema == nil { 177 switch { 178 case strings.HasPrefix(v.Type, TerraformListTypePrefix) || strings.HasPrefix(v.Type, TerraformTupleTypePrefix) || 179 strings.HasPrefix(v.Type, TerraformSetTypePrefix): 180 schema = openapi3.NewArraySchema() 181 case strings.HasPrefix(v.Type, TerraformMapTypePrefix) || strings.HasPrefix(v.Type, TerraformObjectTypePrefix): 182 schema = openapi3.NewObjectSchema() 183 default: 184 return nil, fmt.Errorf("the type `%s` of variable %s is NOT supported", v.Type, v.Name) 185 } 186 } 187 schema.Title = k 188 if v.Required { 189 required = append(required, k) 190 } 191 if v.Default != nil { 192 schema.Default = v.Default 193 } 194 schema.Description = v.Description 195 schemas[v.Name] = schema 196 } 197 198 otherProperties := parseOtherProperties4TerraformDefinition() 199 for k, v := range otherProperties { 200 schemas[k] = v 201 } 202 203 return generateJSONSchemaWithRequiredProperty(schemas, required) 204 } 205 206 // GetTerraformConfigurationFromRemote gets Terraform Configuration(HCL) 207 func GetTerraformConfigurationFromRemote(name, remoteURL, remotePath string, sshPublicKey *gitssh.PublicKeys) (string, error) { 208 userHome, err := os.UserHomeDir() 209 if err != nil { 210 return "", err 211 } 212 cachePath := filepath.Join(userHome, ".vela", "terraform", name) 213 // Check if the directory exists. If yes, remove it. 214 entities, err := os.ReadDir(cachePath) 215 if err != nil || len(entities) == 0 { 216 fmt.Printf("loading terraform module %s into %s from %s\n", name, cachePath, remoteURL) 217 cloneOptions := &git.CloneOptions{ 218 URL: remoteURL, 219 Progress: os.Stdout, 220 } 221 if sshPublicKey != nil { 222 cloneOptions.Auth = sshPublicKey 223 } 224 if _, err = git.PlainClone(cachePath, false, cloneOptions); err != nil { 225 return "", err 226 } 227 } 228 sshKnownHostsPath := os.Getenv("SSH_KNOWN_HOSTS") 229 _ = os.Remove(sshKnownHostsPath) 230 231 tfPath := filepath.Join(cachePath, remotePath, "variables.tf") 232 if _, err := os.Stat(tfPath); err != nil { 233 tfPath = filepath.Join(cachePath, remotePath, "main.tf") 234 if _, err := os.Stat(tfPath); err != nil { 235 return "", errors.Wrap(err, "failed to find main.tf or variables.tf in Terraform configurations of the remote repository") 236 } 237 } 238 conf, err := os.ReadFile(filepath.Clean(tfPath)) 239 if err != nil { 240 return "", errors.Wrap(err, "failed to read Terraform configuration") 241 } 242 return string(conf), nil 243 } 244 245 func parseOtherProperties4TerraformDefinition() map[string]*openapi3.Schema { 246 otherProperties := make(map[string]*openapi3.Schema) 247 248 // 1. writeConnectionSecretToRef 249 secretName := openapi3.NewStringSchema() 250 secretName.Title = "name" 251 secretName.Description = terraform.TerraformSecretNameDescription 252 253 secretNamespace := openapi3.NewStringSchema() 254 secretNamespace.Title = "namespace" 255 secretNamespace.Description = terraform.TerraformSecretNamespaceDescription 256 257 secret := openapi3.NewObjectSchema() 258 secret.Title = terraform.TerraformWriteConnectionSecretToRefName 259 secret.Description = terraform.TerraformWriteConnectionSecretToRefDescription 260 secret.Properties = openapi3.Schemas{ 261 "name": &openapi3.SchemaRef{Value: secretName}, 262 "namespace": &openapi3.SchemaRef{Value: secretNamespace}, 263 } 264 secret.Required = []string{"name"} 265 266 otherProperties[terraform.TerraformWriteConnectionSecretToRefName] = secret 267 268 // 2. providerRef 269 providerName := openapi3.NewStringSchema() 270 providerName.Title = "name" 271 providerName.Description = "The name of the Terraform Cloud provider" 272 273 providerNamespace := openapi3.NewStringSchema() 274 providerNamespace.Title = "namespace" 275 providerNamespace.Default = "default" 276 providerNamespace.Description = "The namespace of the Terraform Cloud provider" 277 278 var providerRefName = "providerRef" 279 provider := openapi3.NewObjectSchema() 280 provider.Title = providerRefName 281 provider.Description = "specifies the Provider" 282 provider.Properties = openapi3.Schemas{ 283 "name": &openapi3.SchemaRef{Value: providerName}, 284 "namespace": &openapi3.SchemaRef{Value: providerNamespace}, 285 } 286 provider.Required = []string{"name"} 287 288 otherProperties[providerRefName] = provider 289 290 // 3. deleteResource 291 var deleteResourceName = "deleteResource" 292 deleteResource := openapi3.NewBoolSchema() 293 deleteResource.Title = deleteResourceName 294 deleteResource.Description = "DeleteResource will determine whether provisioned cloud resources will be deleted when application is deleted" 295 deleteResource.Default = true 296 otherProperties[deleteResourceName] = deleteResource 297 298 // 4. region 299 var regionName = "region" 300 region := openapi3.NewStringSchema() 301 region.Title = regionName 302 region.Description = "Region is cloud provider's region. It will override providerRef" 303 otherProperties[regionName] = region 304 305 return otherProperties 306 307 } 308 309 func generateJSONSchemaWithRequiredProperty(schemas map[string]*openapi3.Schema, required []string) ([]byte, error) { 310 s := openapi3.NewObjectSchema().WithProperties(schemas) 311 if len(required) > 0 { 312 s.Required = required 313 } 314 b, err := s.MarshalJSON() 315 if err != nil { 316 return nil, errors.Wrap(err, "cannot marshal generated schema into json") 317 } 318 return b, nil 319 } 320 321 // GetGitSSHPublicKey gets a kubernetes secret containing the SSH private key based on gitCredentialsSecretReference parameters for component and trait definition 322 func GetGitSSHPublicKey(ctx context.Context, k8sClient client.Client, gitCredentialsSecretReference *v1.SecretReference) (*gitssh.PublicKeys, error) { 323 gitCredentialsSecretName := gitCredentialsSecretReference.Name 324 gitCredentialsSecretNamespace := gitCredentialsSecretReference.Namespace 325 gitCredentialsNamespacedName := k8stypes.NamespacedName{Namespace: gitCredentialsSecretNamespace, Name: gitCredentialsSecretName} 326 327 secret := &v1.Secret{} 328 err := k8sClient.Get(ctx, gitCredentialsNamespacedName, secret) 329 if err != nil { 330 return nil, fmt.Errorf("failed to get git credentials secret: %w", err) 331 } 332 needSecretKeys := []string{GitCredsKnownHosts, v1.SSHAuthPrivateKey} 333 for _, key := range needSecretKeys { 334 if _, ok := secret.Data[key]; !ok { 335 err := errors.Errorf("'%s' not in git credentials secret", key) 336 return nil, err 337 } 338 } 339 340 klog.InfoS("Reconcile gitCredentialsSecretReference", "gitCredentialsSecretReference", klog.KRef(gitCredentialsSecretNamespace, gitCredentialsSecretName)) 341 342 sshPrivateKey := secret.Data[v1.SSHAuthPrivateKey] 343 publicKey, err := gitssh.NewPublicKeys("git", sshPrivateKey, "") 344 if err != nil { 345 return nil, fmt.Errorf("failed to generate public key from private key: %w", err) 346 } 347 sshKnownHosts := secret.Data[GitCredsKnownHosts] 348 sshDir := filepath.Join(os.TempDir(), ".ssh") 349 sshKnownHostsPath := filepath.Join(sshDir, GitCredsKnownHosts) 350 _ = os.Mkdir(sshDir, 0700) 351 err = os.WriteFile(sshKnownHostsPath, sshKnownHosts, 0600) 352 if err != nil { 353 return nil, fmt.Errorf("failed to write known hosts into file: %w", err) 354 } 355 _ = os.Setenv("SSH_KNOWN_HOSTS", sshKnownHostsPath) 356 return publicKey, nil 357 } 358 359 // StoreOpenAPISchema stores OpenAPI v3 schema in ConfigMap from WorkloadDefinition 360 func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name, revName string) (string, error) { 361 var jsonSchema []byte 362 var err error 363 switch def.WorkloadType { 364 case util.TerraformDef: 365 if def.Terraform == nil { 366 return "", fmt.Errorf("no Configuration is set in Terraform specification: %s", def.Name) 367 } 368 configuration := def.Terraform.Configuration 369 if def.Terraform.Type == "remote" { 370 var publicKey *gitssh.PublicKeys 371 publicKey = nil 372 if def.Terraform.GitCredentialsSecretReference != nil { 373 gitCredentialsSecretReference := def.Terraform.GitCredentialsSecretReference 374 publicKey, err = GetGitSSHPublicKey(ctx, k8sClient, gitCredentialsSecretReference) 375 if err != nil { 376 return "", fmt.Errorf("issue with gitCredentialsSecretReference %s/%s: %w", gitCredentialsSecretReference.Namespace, gitCredentialsSecretReference.Name, err) 377 } 378 } 379 configuration, err = GetTerraformConfigurationFromRemote(def.Name, def.Terraform.Configuration, def.Terraform.Path, publicKey) 380 if err != nil { 381 return "", fmt.Errorf("cannot get Terraform configuration %s from remote: %w", def.Name, err) 382 } 383 } 384 jsonSchema, err = GetOpenAPISchemaFromTerraformComponentDefinition(configuration) 385 default: 386 jsonSchema, err = def.GetOpenAPISchema(name) 387 } 388 if err != nil { 389 return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err) 390 } 391 componentDefinition := def.ComponentDefinition 392 ownerReference := []metav1.OwnerReference{{ 393 APIVersion: componentDefinition.APIVersion, 394 Kind: componentDefinition.Kind, 395 Name: componentDefinition.Name, 396 UID: componentDefinition.GetUID(), 397 Controller: pointer.Bool(true), 398 BlockOwnerDeletion: pointer.Bool(true), 399 }} 400 cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, componentDefinition.Name, typeComponentDefinition, componentDefinition.Labels, nil, jsonSchema, ownerReference) 401 if err != nil { 402 return cmName, err 403 } 404 405 // Create a configmap to store parameter for each definitionRevision 406 defRev := new(v1beta1.DefinitionRevision) 407 if err = k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: revName}, defRev); err != nil { 408 return "", err 409 } 410 ownerReference = []metav1.OwnerReference{{ 411 APIVersion: defRev.APIVersion, 412 Kind: defRev.Kind, 413 Name: defRev.Name, 414 UID: defRev.GetUID(), 415 Controller: pointer.Bool(true), 416 BlockOwnerDeletion: pointer.Bool(true), 417 }} 418 _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, typeComponentDefinition, defRev.Spec.ComponentDefinition.Labels, nil, jsonSchema, ownerReference) 419 if err != nil { 420 return cmName, err 421 } 422 return cmName, nil 423 } 424 425 // CapabilityTraitDefinition is the Capability struct for TraitDefinition 426 type CapabilityTraitDefinition struct { 427 Name string `json:"name"` 428 TraitDefinition v1beta1.TraitDefinition `json:"traitDefinition"` 429 430 DefCategoryType util.WorkloadType `json:"defCategoryType"` 431 432 CapabilityBaseDefinition 433 } 434 435 // NewCapabilityTraitDef will create a CapabilityTraitDefinition 436 func NewCapabilityTraitDef(traitdefinition *v1beta1.TraitDefinition) CapabilityTraitDefinition { 437 var def CapabilityTraitDefinition 438 def.Name = traitdefinition.Name // or def.Name = req.NamespacedName.Name 439 def.TraitDefinition = *traitdefinition.DeepCopy() 440 return def 441 } 442 443 // GetOpenAPISchema gets OpenAPI v3 schema by TraitDefinition name 444 func (def *CapabilityTraitDefinition) GetOpenAPISchema(name string) ([]byte, error) { 445 capability, err := appfile.ConvertTemplateJSON2Object(name, def.TraitDefinition.Spec.Extension, def.TraitDefinition.Spec.Schematic) 446 if err != nil { 447 return nil, fmt.Errorf("failed to convert WorkloadDefinition to Capability Object") 448 } 449 return getOpenAPISchema(capability) 450 } 451 452 // StoreOpenAPISchema stores OpenAPI v3 schema from TraitDefinition in ConfigMap 453 func (def *CapabilityTraitDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name string, revName string) (string, error) { 454 jsonSchema, err := def.GetOpenAPISchema(name) 455 if err != nil { 456 return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err) 457 } 458 459 traitDefinition := def.TraitDefinition 460 ownerReference := []metav1.OwnerReference{{ 461 APIVersion: traitDefinition.APIVersion, 462 Kind: traitDefinition.Kind, 463 Name: traitDefinition.Name, 464 UID: traitDefinition.GetUID(), 465 Controller: pointer.Bool(true), 466 BlockOwnerDeletion: pointer.Bool(true), 467 }} 468 cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, traitDefinition.Name, typeTraitDefinition, traitDefinition.Labels, traitDefinition.Spec.AppliesToWorkloads, jsonSchema, ownerReference) 469 if err != nil { 470 return cmName, err 471 } 472 473 // Create a configmap to store parameter for each definitionRevision 474 defRev := new(v1beta1.DefinitionRevision) 475 if err = k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: revName}, defRev); err != nil { 476 return "", err 477 } 478 ownerReference = []metav1.OwnerReference{{ 479 APIVersion: defRev.APIVersion, 480 Kind: defRev.Kind, 481 Name: defRev.Name, 482 UID: defRev.GetUID(), 483 Controller: pointer.Bool(true), 484 BlockOwnerDeletion: pointer.Bool(true), 485 }} 486 _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, typeTraitDefinition, defRev.Spec.TraitDefinition.Labels, defRev.Spec.TraitDefinition.Spec.AppliesToWorkloads, jsonSchema, ownerReference) 487 if err != nil { 488 return cmName, err 489 } 490 return cmName, nil 491 } 492 493 // CapabilityStepDefinition is the Capability struct for WorkflowStepDefinition 494 type CapabilityStepDefinition struct { 495 Name string `json:"name"` 496 StepDefinition v1beta1.WorkflowStepDefinition `json:"stepDefinition"` 497 498 CapabilityBaseDefinition 499 } 500 501 // NewCapabilityStepDef will create a CapabilityStepDefinition 502 func NewCapabilityStepDef(stepdefinition *v1beta1.WorkflowStepDefinition) CapabilityStepDefinition { 503 var def CapabilityStepDefinition 504 def.Name = stepdefinition.Name 505 def.StepDefinition = *stepdefinition.DeepCopy() 506 return def 507 } 508 509 // GetOpenAPISchema gets OpenAPI v3 schema by StepDefinition name 510 func (def *CapabilityStepDefinition) GetOpenAPISchema(name string) ([]byte, error) { 511 capability, err := appfile.ConvertTemplateJSON2Object(name, nil, def.StepDefinition.Spec.Schematic) 512 if err != nil { 513 return nil, fmt.Errorf("failed to convert WorkflowStepDefinition to Capability Object") 514 } 515 return getOpenAPISchema(capability) 516 } 517 518 // StoreOpenAPISchema stores OpenAPI v3 schema from StepDefinition in ConfigMap 519 func (def *CapabilityStepDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name string, revName string) (string, error) { 520 var jsonSchema []byte 521 var err error 522 523 jsonSchema, err = def.GetOpenAPISchema(name) 524 if err != nil { 525 return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err) 526 } 527 528 stepDefinition := def.StepDefinition 529 ownerReference := []metav1.OwnerReference{{ 530 APIVersion: stepDefinition.APIVersion, 531 Kind: stepDefinition.Kind, 532 Name: stepDefinition.Name, 533 UID: stepDefinition.GetUID(), 534 Controller: pointer.Bool(true), 535 BlockOwnerDeletion: pointer.Bool(true), 536 }} 537 cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, stepDefinition.Name, typeWorkflowStepDefinition, stepDefinition.Labels, nil, jsonSchema, ownerReference) 538 if err != nil { 539 return cmName, err 540 } 541 542 // Create a configmap to store parameter for each definitionRevision 543 defRev := new(v1beta1.DefinitionRevision) 544 if err = k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: revName}, defRev); err != nil { 545 return "", err 546 } 547 ownerReference = []metav1.OwnerReference{{ 548 APIVersion: defRev.APIVersion, 549 Kind: defRev.Kind, 550 Name: defRev.Name, 551 UID: defRev.GetUID(), 552 Controller: pointer.Bool(true), 553 BlockOwnerDeletion: pointer.Bool(true), 554 }} 555 _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, typeWorkflowStepDefinition, defRev.Spec.WorkflowStepDefinition.Labels, nil, jsonSchema, ownerReference) 556 if err != nil { 557 return cmName, err 558 } 559 return cmName, nil 560 } 561 562 // CapabilityPolicyDefinition is the Capability struct for PolicyDefinition 563 type CapabilityPolicyDefinition struct { 564 Name string `json:"name"` 565 PolicyDefinition v1beta1.PolicyDefinition `json:"policyDefinition"` 566 567 CapabilityBaseDefinition 568 } 569 570 // NewCapabilityPolicyDef will create a CapabilityPolicyDefinition 571 func NewCapabilityPolicyDef(policydefinition *v1beta1.PolicyDefinition) CapabilityPolicyDefinition { 572 var def CapabilityPolicyDefinition 573 def.Name = policydefinition.Name 574 def.PolicyDefinition = *policydefinition.DeepCopy() 575 return def 576 } 577 578 // GetOpenAPISchema gets OpenAPI v3 schema by StepDefinition name 579 func (def *CapabilityPolicyDefinition) GetOpenAPISchema(name string) ([]byte, error) { 580 capability, err := appfile.ConvertTemplateJSON2Object(name, nil, def.PolicyDefinition.Spec.Schematic) 581 if err != nil { 582 return nil, fmt.Errorf("failed to convert WorkflowStepDefinition to Capability Object") 583 } 584 return getOpenAPISchema(capability) 585 } 586 587 // StoreOpenAPISchema stores OpenAPI v3 schema from StepDefinition in ConfigMap 588 func (def *CapabilityPolicyDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name, revName string) (string, error) { 589 var jsonSchema []byte 590 var err error 591 592 jsonSchema, err = def.GetOpenAPISchema(name) 593 if err != nil { 594 return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err) 595 } 596 597 policyDefinition := def.PolicyDefinition 598 ownerReference := []metav1.OwnerReference{{ 599 APIVersion: policyDefinition.APIVersion, 600 Kind: policyDefinition.Kind, 601 Name: policyDefinition.Name, 602 UID: policyDefinition.GetUID(), 603 Controller: pointer.Bool(true), 604 BlockOwnerDeletion: pointer.Bool(true), 605 }} 606 cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, policyDefinition.Name, typePolicyStepDefinition, policyDefinition.Labels, nil, jsonSchema, ownerReference) 607 if err != nil { 608 return cmName, err 609 } 610 611 // Create a configmap to store parameter for each definitionRevision 612 defRev := new(v1beta1.DefinitionRevision) 613 if err = k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: revName}, defRev); err != nil { 614 return "", err 615 } 616 ownerReference = []metav1.OwnerReference{{ 617 APIVersion: defRev.APIVersion, 618 Kind: defRev.Kind, 619 Name: defRev.Name, 620 UID: defRev.GetUID(), 621 Controller: pointer.Bool(true), 622 BlockOwnerDeletion: pointer.Bool(true), 623 }} 624 _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, typePolicyStepDefinition, defRev.Spec.PolicyDefinition.Labels, nil, jsonSchema, ownerReference) 625 if err != nil { 626 return cmName, err 627 } 628 return cmName, nil 629 } 630 631 // CapabilityBaseDefinition is the base struct for CapabilityWorkloadDefinition and CapabilityTraitDefinition 632 type CapabilityBaseDefinition struct { 633 } 634 635 // CreateOrUpdateConfigMap creates ConfigMap to store OpenAPI v3 schema or or updates data in ConfigMap 636 func (def *CapabilityBaseDefinition) CreateOrUpdateConfigMap(ctx context.Context, k8sClient client.Client, namespace, 637 definitionName, definitionType string, labels map[string]string, appliedWorkloads []string, jsonSchema []byte, ownerReferences []metav1.OwnerReference) (string, error) { 638 cmName := fmt.Sprintf("%s-%s%s", definitionType, types.CapabilityConfigMapNamePrefix, definitionName) 639 var cm v1.ConfigMap 640 var data = map[string]string{ 641 types.OpenapiV3JSONSchema: string(jsonSchema), 642 } 643 if labels == nil { 644 labels = make(map[string]string) 645 } 646 labels[types.LabelDefinition] = "schema" 647 labels[types.LabelDefinitionName] = definitionName 648 annotations := make(map[string]string) 649 if appliedWorkloads != nil { 650 annotations[types.AnnoDefinitionAppliedWorkloads] = strings.Join(appliedWorkloads, ",") 651 } 652 653 // No need to check the existence of namespace, if it doesn't exist, API server will return the error message 654 // before it's to be reconciled by ComponentDefinition/TraitDefinition controller. 655 err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: cmName}, &cm) 656 if err != nil && apierrors.IsNotFound(err) { 657 cm = v1.ConfigMap{ 658 TypeMeta: metav1.TypeMeta{ 659 APIVersion: "v1", 660 Kind: "ConfigMap", 661 }, 662 ObjectMeta: metav1.ObjectMeta{ 663 Name: cmName, 664 Namespace: namespace, 665 OwnerReferences: ownerReferences, 666 Labels: labels, 667 Annotations: annotations, 668 }, 669 Data: data, 670 } 671 err = k8sClient.Create(ctx, &cm) 672 if err != nil { 673 return cmName, fmt.Errorf(util.ErrUpdateCapabilityInConfigMap, definitionName, err) 674 } 675 klog.InfoS("Successfully stored Capability Schema in ConfigMap", "configMap", klog.KRef(namespace, cmName)) 676 return cmName, nil 677 } 678 679 cm.Data = data 680 cm.Labels = labels 681 cm.Annotations = annotations 682 if err = k8sClient.Update(ctx, &cm); err != nil { 683 return cmName, fmt.Errorf(util.ErrUpdateCapabilityInConfigMap, definitionName, err) 684 } 685 klog.InfoS("Successfully update Capability Schema in ConfigMap", "configMap", klog.KRef(namespace, cmName)) 686 return cmName, nil 687 } 688 689 // getOpenAPISchema is the main function for GetDefinition API 690 func getOpenAPISchema(capability types.Capability) ([]byte, error) { 691 cueTemplate := script.CUE(capability.CueTemplate) 692 schema, err := cueTemplate.ParsePropertiesToSchema() 693 if err != nil { 694 return nil, err 695 } 696 klog.Infof("parsed %d properties by %s/%s", len(schema.Properties), capability.Type, capability.Name) 697 parameter, err := schema.MarshalJSON() 698 if err != nil { 699 return nil, err 700 } 701 return parameter, nil 702 }