kubevela/pkg/controller/utils/capability.go

409 lines
14 KiB
Go
Raw Normal View History

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"context"
"fmt"
"regexp"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/build"
"github.com/getkin/kin-openapi/openapi3"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
2021-03-24 13:27:29 +08:00
commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
2021-04-09 12:28:04 +08:00
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/appfile/helm"
velacue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/cue/packages"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
// ErrNoSectionParameterInCue means there is not parameter section in Cue template of a workload
const ErrNoSectionParameterInCue = "capability %s doesn't contain section `parameter`"
// CapabilityDefinitionInterface is the interface for Capability (WorkloadDefinition and TraitDefinition)
type CapabilityDefinitionInterface interface {
GetCapabilityObject(ctx context.Context, k8sClient client.Client, namespace, name string) (*types.Capability, error)
GetOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name string) ([]byte, error)
}
// CapabilityComponentDefinition is the struct for ComponentDefinition
type CapabilityComponentDefinition struct {
2021-04-09 12:28:04 +08:00
Name string `json:"name"`
ComponentDefinition v1beta1.ComponentDefinition `json:"componentDefinition"`
2021-03-17 17:18:54 +08:00
WorkloadType util.WorkloadType `json:"workloadType"`
WorkloadDefName string `json:"workloadDefName"`
2021-03-24 13:27:29 +08:00
Helm *commontypes.Helm `json:"helm"`
Kube *commontypes.Kube `json:"kube"`
CapabilityBaseDefinition
}
// NewCapabilityComponentDef will create a CapabilityComponentDefinition
func NewCapabilityComponentDef(componentDefinition *v1beta1.ComponentDefinition) CapabilityComponentDefinition {
var def CapabilityComponentDefinition
def.Name = componentDefinition.Name
if componentDefinition.Spec.Workload.Definition == (commontypes.WorkloadGVK{}) && componentDefinition.Spec.Workload.Type != "" {
def.WorkloadType = util.ReferWorkload
def.WorkloadDefName = componentDefinition.Spec.Workload.Type
}
if componentDefinition.Spec.Schematic != nil && componentDefinition.Spec.Schematic.HELM != nil {
def.WorkloadType = util.HELMDef
def.Helm = componentDefinition.Spec.Schematic.HELM
}
if componentDefinition.Spec.Schematic != nil && componentDefinition.Spec.Schematic.KUBE != nil {
def.WorkloadType = util.KubeDef
def.Kube = componentDefinition.Spec.Schematic.KUBE
}
def.ComponentDefinition = *componentDefinition.DeepCopy()
return def
}
// GetOpenAPISchema gets OpenAPI v3 schema by WorkloadDefinition name
func (def *CapabilityComponentDefinition) GetOpenAPISchema(pd *packages.PackageDiscover, name string) ([]byte, error) {
capability, err := appfile.ConvertTemplateJSON2Object(name, def.ComponentDefinition.Spec.Extension, def.ComponentDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert ComponentDefinition to Capability Object")
}
return getOpenAPISchema(capability, pd)
}
// GetKubeSchematicOpenAPISchema gets OpenAPI v3 schema based on kube schematic parameters for component and trait definition
func GetKubeSchematicOpenAPISchema(params []commontypes.KubeParameter) ([]byte, error) {
required := []string{}
properties := map[string]*openapi3.Schema{}
for _, p := range params {
var tmp *openapi3.Schema
switch p.ValueType {
case commontypes.StringType:
tmp = openapi3.NewStringSchema()
case commontypes.NumberType:
tmp = openapi3.NewFloat64Schema()
case commontypes.BooleanType:
tmp = openapi3.NewBoolSchema()
default:
tmp = openapi3.NewStringSchema()
}
if p.Required != nil && *p.Required {
required = append(required, p.Name)
}
if p.Description != nil {
tmp.Description = fmt.Sprintf("%s %s", tmp.Description, *p.Description)
} else {
// save FieldPaths into description
tmp.Description = fmt.Sprintf("The value will be applied to fields: [%s].", strings.Join(p.FieldPaths, ","))
}
properties[p.Name] = tmp
}
s := openapi3.NewObjectSchema().WithProperties(properties)
if len(required) > 0 {
s.Required = required
}
b, err := s.MarshalJSON()
if err != nil {
return nil, errors.Wrap(err, "cannot marshal generated schema into json")
}
return b, nil
}
// StoreOpenAPISchema stores OpenAPI v3 schema in ConfigMap from WorkloadDefinition
func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client,
pd *packages.PackageDiscover, namespace, name, revName string) (string, error) {
var jsonSchema []byte
var err error
switch def.WorkloadType {
case util.HELMDef:
jsonSchema, err = helm.GetChartValuesJSONSchema(ctx, def.Helm)
case util.KubeDef:
jsonSchema, err = GetKubeSchematicOpenAPISchema(def.Kube.Parameters)
default:
jsonSchema, err = def.GetOpenAPISchema(pd, name)
}
if err != nil {
return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err)
}
componentDefinition := def.ComponentDefinition
ownerReference := []metav1.OwnerReference{{
APIVersion: componentDefinition.APIVersion,
Kind: componentDefinition.Kind,
Name: componentDefinition.Name,
UID: componentDefinition.GetUID(),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
}}
cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, componentDefinition.Name, jsonSchema, ownerReference)
if err != nil {
return cmName, err
}
_, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, jsonSchema, ownerReference)
if err != nil {
return cmName, err
}
return cmName, nil
}
// CapabilityTraitDefinition is the Capability struct for TraitDefinition
type CapabilityTraitDefinition struct {
Name string `json:"name"`
TraitDefinition v1beta1.TraitDefinition `json:"traitDefinition"`
DefCategoryType util.WorkloadType `json:"defCategoryType"`
Kube *commontypes.Kube `json:"kube"`
CapabilityBaseDefinition
}
// NewCapabilityTraitDef will create a CapabilityTraitDefinition
func NewCapabilityTraitDef(traitdefinition *v1beta1.TraitDefinition) CapabilityTraitDefinition {
var def CapabilityTraitDefinition
def.Name = traitdefinition.Name // or def.Name = req.NamespacedName.Name
if traitdefinition.Spec.Schematic != nil && traitdefinition.Spec.Schematic.KUBE != nil {
def.DefCategoryType = util.KubeDef
def.Kube = traitdefinition.Spec.Schematic.KUBE
}
def.TraitDefinition = *traitdefinition.DeepCopy()
return def
}
// GetOpenAPISchema gets OpenAPI v3 schema by TraitDefinition name
func (def *CapabilityTraitDefinition) GetOpenAPISchema(pd *packages.PackageDiscover, name string) ([]byte, error) {
capability, err := appfile.ConvertTemplateJSON2Object(name, def.TraitDefinition.Spec.Extension, def.TraitDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert WorkloadDefinition to Capability Object")
}
return getOpenAPISchema(capability, pd)
}
// StoreOpenAPISchema stores OpenAPI v3 schema from TraitDefinition in ConfigMap
func (def *CapabilityTraitDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, pd *packages.PackageDiscover, namespace, name string, revName string) (string, error) {
var jsonSchema []byte
var err error
switch def.DefCategoryType {
case util.KubeDef: // Kube template
jsonSchema, err = GetKubeSchematicOpenAPISchema(def.Kube.Parameters)
default: // CUE template
jsonSchema, err = def.GetOpenAPISchema(pd, name)
}
if err != nil {
return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err)
}
traitDefinition := def.TraitDefinition
ownerReference := []metav1.OwnerReference{{
APIVersion: traitDefinition.APIVersion,
Kind: traitDefinition.Kind,
Name: traitDefinition.Name,
UID: traitDefinition.GetUID(),
Controller: pointer.BoolPtr(true),
BlockOwnerDeletion: pointer.BoolPtr(true),
}}
cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, traitDefinition.Name, jsonSchema, ownerReference)
if err != nil {
return cmName, err
}
def.TraitDefinition.Status.ConfigMapRef = cmName
_, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, jsonSchema, ownerReference)
if err != nil {
return cmName, err
}
return cmName, nil
}
// CapabilityBaseDefinition is the base struct for CapabilityWorkloadDefinition and CapabilityTraitDefinition
type CapabilityBaseDefinition struct {
}
// CreateOrUpdateConfigMap creates ConfigMap to store OpenAPI v3 schema or or updates data in ConfigMap
func (def *CapabilityBaseDefinition) CreateOrUpdateConfigMap(ctx context.Context, k8sClient client.Client, namespace,
definitionName string, jsonSchema []byte, ownerReferences []metav1.OwnerReference) (string, error) {
cmName := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, definitionName)
var cm v1.ConfigMap
var data = map[string]string{
types.OpenapiV3JSONSchema: string(jsonSchema),
}
// No need to check the existence of namespace, if it doesn't exist, API server will return the error message
// before it's to be reconciled by ComponentDefinition/TraitDefinition controller.
err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: cmName}, &cm)
if err != nil && apierrors.IsNotFound(err) {
cm = v1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Name: cmName,
Namespace: namespace,
OwnerReferences: ownerReferences,
Labels: map[string]string{
"definition.oam.dev": "schema",
"definition.oam.dev/name": definitionName,
},
},
Data: data,
}
err = k8sClient.Create(ctx, &cm)
if err != nil {
return cmName, fmt.Errorf(util.ErrUpdateCapabilityInConfigMap, definitionName, err)
}
return cmName, nil
}
cm.Data = data
if err = k8sClient.Update(ctx, &cm); err != nil {
return cmName, fmt.Errorf(util.ErrUpdateCapabilityInConfigMap, definitionName, err)
}
return cmName, nil
}
// getDefinition is the main function for GetDefinition API
func getOpenAPISchema(capability types.Capability, pd *packages.PackageDiscover) ([]byte, error) {
openAPISchema, err := generateOpenAPISchemaFromCapabilityParameter(capability, pd)
if err != nil {
return nil, err
}
schema, err := ConvertOpenAPISchema2SwaggerObject(openAPISchema)
if err != nil {
return nil, err
}
fixOpenAPISchema("", schema)
parameter, err := schema.MarshalJSON()
if err != nil {
return nil, err
}
return parameter, nil
}
// generateOpenAPISchemaFromCapabilityParameter returns the parameter of a definition in cue.Value format
func generateOpenAPISchemaFromCapabilityParameter(capability types.Capability, pd *packages.PackageDiscover) ([]byte, error) {
template, err := prepareParameterCue(capability.Name, capability.CueTemplate)
if err != nil {
return nil, err
}
2021-04-09 12:28:04 +08:00
template += velacue.BaseTemplate
if pd == nil {
var r cue.Runtime
2021-04-09 12:28:04 +08:00
cueInst, err := r.Compile("-", template)
if err != nil {
return nil, err
}
2021-04-09 12:28:04 +08:00
return common.GenOpenAPI(cueInst)
}
bi := build.NewContext().NewInstance("", nil)
err = bi.AddFile("-", template)
if err != nil {
return nil, err
}
2021-04-09 20:15:02 +08:00
cueInst, err := pd.ImportPackagesAndBuildInstance(bi)
2021-04-09 12:28:04 +08:00
if err != nil {
return nil, err
}
return common.GenOpenAPI(cueInst)
}
// GenerateOpenAPISchemaFromDefinition returns the parameter of a definition
func GenerateOpenAPISchemaFromDefinition(definitionName, cueTemplate string) ([]byte, error) {
capability := types.Capability{
Name: definitionName,
CueTemplate: cueTemplate,
}
return generateOpenAPISchemaFromCapabilityParameter(capability, nil)
}
// prepareParameterCue cuts `parameter` section form definition .cue file
func prepareParameterCue(capabilityName, capabilityTemplate string) (string, error) {
var template string
var withParameterFlag bool
2021-05-19 20:08:51 +08:00
r := regexp.MustCompile(`[[:space:]]*parameter:[[:space:]]*`)
trimRe := regexp.MustCompile(`\s+`)
for _, text := range strings.Split(capabilityTemplate, "\n") {
if r.MatchString(text) {
// a variable has to be refined as a definition which starts with "#"
2021-05-19 20:08:51 +08:00
// text may be start with space or tab, we should clean up text
text = fmt.Sprintf("parameter: #parameter\n#%s", trimRe.ReplaceAllString(text, ""))
withParameterFlag = true
}
template += fmt.Sprintf("%s\n", text)
}
if !withParameterFlag {
return "", fmt.Errorf(ErrNoSectionParameterInCue, capabilityName)
}
return template, nil
}
// fixOpenAPISchema fixes tainted `description` filed, missing of title `field`.
func fixOpenAPISchema(name string, schema *openapi3.Schema) {
t := schema.Type
switch t {
case "object":
for k, v := range schema.Properties {
s := v.Value
fixOpenAPISchema(k, s)
}
case "array":
fixOpenAPISchema("", schema.Items.Value)
}
if name != "" {
schema.Title = name
}
description := schema.Description
if strings.Contains(description, appfile.UsageTag) {
description = strings.Split(description, appfile.UsageTag)[1]
}
if strings.Contains(description, appfile.ShortTag) {
description = strings.Split(description, appfile.ShortTag)[0]
description = strings.TrimSpace(description)
}
schema.Description = description
}
// ConvertOpenAPISchema2SwaggerObject converts OpenAPI v2 JSON schema to Swagger Object
func ConvertOpenAPISchema2SwaggerObject(data []byte) (*openapi3.Schema, error) {
swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(data)
if err != nil {
return nil, err
}
schemaRef, ok := swagger.Components.Schemas[velacue.ParameterTag]
if !ok {
return nil, errors.New(util.ErrGenerateOpenAPIV2JSONSchemaForCapability)
}
return schemaRef.Value, nil
}