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  }