Files
kubevela/pkg/appfile/template.go
Kanchan Dhamane bc15e5b359
Some checks failed
CodeQL / Analyze (go) (push) Failing after 1m43s
Definition-Lint / definition-doc (push) Failing after 6m13s
E2E MultiCluster Test / detect-noop (push) Successful in 24s
E2E Test / detect-noop (push) Successful in 17s
Go / detect-noop (push) Successful in 21s
license / Check for unapproved licenses (push) Failing after 2m38s
Registry / publish-core-images (push) Failing after 40s
Unit-Test / detect-noop (push) Successful in 20s
E2E MultiCluster Test / e2e-multi-cluster-tests (v1.29) (push) Failing after 1m55s
E2E Test / e2e-tests (v1.29) (push) Failing after 1m18s
Go / staticcheck (push) Successful in 18m35s
Go / lint (push) Failing after 19m38s
Go / check-diff (push) Failing after 15m7s
Go / check-core-image-build (push) Failing after 3m45s
Go / check-cli-image-build (push) Failing after 2m23s
Unit-Test / unit-tests (push) Failing after 12m43s
Go / check-windows (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Failing after 48s
Feat: Semantic versioning support for Definitions (#6648)
* feature: Add Semantic versioning to KubeVela Definitions

Fixes https://github.com/kubevela/kubevela/issues/6435
Fixes https://github.com/kubevela/kubevela/issues/6534

Changes:
- Adds an optional "Version" field for all Definition Specs.
- Adds the following new validations to Webhooks for Definitions:
	- Validate the "Version" field follows Semantic versioning.
	- Dis-allow conflicting versioning fields ( Name annotation, Spec.Version)
- Adds the following new validations to Webhooks for Application:
	- Dis-allow the use of both the "publishVersion" & "autoUpdate" annotations.
- Enahnce "multiStageComponentApply" feature to support auto updates.

Boy Scout Changes:
- Fixes Plugin e2e tests broken by the fix for 6534.
- Fixes the dryRun and livediff commands to respect the "-n" namespace flag.
- Fixes the Application ValidationWebhook to respect the "-n" namespace flag.

Co-authored-by: Rahul Kumar <35751394+bugbounce@users.noreply.github.com>
Co-authored-by: Chaitanya Reddy <chaitanyareddy0702@gmail.com>
Co-authored-by: Vibhor Chinda <vibhorchinda@gmail.com>
Co-authored-by: Shivin Gopalani <gopalanishivin@gmail.com>

Signed-off-by: kanchan-dhamane <74534570+kanchan-dhamane@users.noreply.github.com>

* feature: Add KEP to define the proposal

Signed-off-by: kanchan-dhamane <74534570+kanchan-dhamane@users.noreply.github.com>

* fix: Rebase and fix merge conflicts

Signed-off-by: kanchan-dhamane <74534570+kanchan-dhamane@users.noreply.github.com>

* Fix: Adds unit test cases

Signed-off-by: kanchan-dhamane <74534570+kanchan-dhamane@users.noreply.github.com>

---------

Signed-off-by: kanchan-dhamane <74534570+kanchan-dhamane@users.noreply.github.com>
Co-authored-by: bugbounce <35751394+bugbounce@users.noreply.github.com>
2025-02-03 11:09:28 +08:00

401 lines
13 KiB
Go

/*
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 appfile
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/kubevela/pkg/multicluster"
"github.com/pkg/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
const (
// UsageTag is usage comment annotation
UsageTag = "+usage="
// ShortTag is the short alias annotation
ShortTag = "+short"
)
// Template is a helper struct for processing capability including
// ComponentDefinition, TraitDefinition.
// It mainly collects schematic and status data of a capability definition.
type Template struct {
TemplateStr string
Health string
CustomStatus string
CapabilityCategory types.CapabilityCategory
Reference common.WorkloadTypeDescriptor
Terraform *common.Terraform
ComponentDefinition *v1beta1.ComponentDefinition
WorkloadDefinition *v1beta1.WorkloadDefinition
TraitDefinition *v1beta1.TraitDefinition
PolicyDefinition *v1beta1.PolicyDefinition
WorkflowStepDefinition *v1beta1.WorkflowStepDefinition
}
// LoadTemplate gets the capability definition from cluster and resolve it.
// It returns a helper struct, Template, which will be used for further
// processing.
func LoadTemplate(ctx context.Context, cli client.Client, capName string, capType types.CapType, annotations map[string]string) (*Template, error) {
ctx = multicluster.WithCluster(ctx, multicluster.Local)
// Application Controller only loads template from ComponentDefinition and TraitDefinition
switch capType {
case types.TypeComponentDefinition, types.TypeWorkload:
cd := new(v1beta1.ComponentDefinition)
err := oamutil.GetCapabilityDefinition(ctx, cli, cd, capName, annotations)
if err != nil {
if kerrors.IsNotFound(err) {
wd := new(v1beta1.WorkloadDefinition)
if err := oamutil.GetDefinition(ctx, cli, wd, capName); err != nil {
return nil, errors.WithMessagef(err, "load template from component definition [%s] ", capName)
}
tmpl, err := newTemplateOfWorkloadDefinition(wd)
if err != nil {
return nil, err
}
gvk, err := oamutil.GetGVKFromDefinition(cli.RESTMapper(), wd.Spec.Reference)
if err != nil {
return nil, errors.WithMessagef(err, "get group version kind from component definition [%s]", capName)
}
tmpl.Reference = common.WorkloadTypeDescriptor{
Definition: common.WorkloadGVK{
APIVersion: metav1.GroupVersion{
Group: gvk.Group,
Version: gvk.Version,
}.String(),
Kind: gvk.Kind,
},
}
return tmpl, nil
}
return nil, errors.WithMessagef(err, "load template from component definition [%s] ", capName)
}
tmpl, err := newTemplateOfCompDefinition(cd)
if err != nil {
return nil, err
}
return tmpl, nil
case types.TypeTrait:
td := new(v1beta1.TraitDefinition)
err := oamutil.GetCapabilityDefinition(ctx, cli, td, capName, annotations)
if err != nil {
return nil, errors.WithMessagef(err, "load template from trait definition [%s] ", capName)
}
tmpl, err := newTemplateOfTraitDefinition(td)
if err != nil {
return nil, err
}
return tmpl, nil
case types.TypePolicy:
d := new(v1beta1.PolicyDefinition)
err := oamutil.GetCapabilityDefinition(ctx, cli, d, capName, annotations)
if err != nil {
return nil, errors.WithMessagef(err, "load template from policy definition [%s] ", capName)
}
tmpl, err := newTemplateOfPolicyDefinition(d)
if err != nil {
return nil, err
}
return tmpl, nil
case types.TypeWorkflowStep:
d := new(v1beta1.WorkflowStepDefinition)
err := oamutil.GetCapabilityDefinition(ctx, cli, d, capName, annotations)
if err != nil {
return nil, errors.WithMessagef(err, "load template from workflow step definition [%s] ", capName)
}
tmpl, err := newTemplateOfWorkflowStepDefinition(d)
if err != nil {
return nil, err
}
return tmpl, nil
}
return nil, fmt.Errorf("kind(%s) of %s not supported", capType, capName)
}
// LoadTemplateFromRevision will load Definition template from app revision
func LoadTemplateFromRevision(capName string, capType types.CapType, apprev *v1beta1.ApplicationRevision, mapper meta.RESTMapper) (*Template, error) {
if apprev == nil {
return nil, errors.Errorf("fail to find template for %s as app revision is empty", capName)
}
capName = verifyRevisionName(capName, capType, apprev)
switch capType {
case types.TypeComponentDefinition:
cd, ok := apprev.Spec.ComponentDefinitions[capName]
if !ok {
wd, ok := apprev.Spec.WorkloadDefinitions[capName]
if !ok {
return nil, errors.Errorf("component definition [%s] not found in app revision %s", capName, apprev.Name)
}
tmpl, err := newTemplateOfWorkloadDefinition(&wd)
if err != nil {
return nil, err
}
gvk, err := oamutil.GetGVKFromDefinition(mapper, wd.Spec.Reference)
if err != nil {
return nil, errors.WithMessagef(err, "Get group version kind from component definition [%s]", capName)
}
tmpl.Reference = common.WorkloadTypeDescriptor{
Definition: common.WorkloadGVK{
APIVersion: metav1.GroupVersion{
Group: gvk.Group,
Version: gvk.Version,
}.String(),
Kind: gvk.Kind,
},
}
return tmpl, nil
}
tmpl, err := newTemplateOfCompDefinition(cd.DeepCopy())
if err != nil {
return nil, err
}
return tmpl, nil
case types.TypeTrait:
td, ok := apprev.Spec.TraitDefinitions[capName]
if !ok {
return nil, errors.Errorf("TraitDefinition [%s] not found in app revision %s", capName, apprev.Name)
}
tmpl, err := newTemplateOfTraitDefinition(td.DeepCopy())
if err != nil {
return nil, err
}
return tmpl, nil
case types.TypePolicy:
d, ok := apprev.Spec.PolicyDefinitions[capName]
if !ok {
return nil, errors.Errorf("PolicyDefinition [%s] not found in app revision %s", capName, apprev.Name)
}
tmpl, err := newTemplateOfPolicyDefinition(d.DeepCopy())
if err != nil {
return nil, err
}
return tmpl, nil
case types.TypeWorkflowStep:
w, ok := apprev.Spec.WorkflowStepDefinitions[capName]
if !ok {
return nil, errors.Errorf("WorkflowStepDefinition [%s] not found in app revision %s", capName, apprev.Name)
}
tmpl, err := newTemplateOfWorkflowStepDefinition(w.DeepCopy())
if err != nil {
return nil, err
}
return tmpl, nil
default:
return nil, fmt.Errorf("kind(%s) of %s not supported", capType, capName)
}
}
// IsNotFoundInAppRevision check if the error is `not found in app revision`
func IsNotFoundInAppRevision(err error) bool {
return err != nil && strings.Contains(err.Error(), "not found in app revision")
}
func verifyRevisionName(capName string, capType types.CapType, apprev *v1beta1.ApplicationRevision) string {
if strings.Contains(capName, "@") {
splitName := capName[0:strings.LastIndex(capName, "@")]
ok := false
switch capType {
case types.TypeComponentDefinition:
_, ok = apprev.Spec.ComponentDefinitions[splitName]
case types.TypeTrait:
_, ok = apprev.Spec.TraitDefinitions[splitName]
case types.TypePolicy:
_, ok = apprev.Spec.PolicyDefinitions[splitName]
case types.TypeWorkflowStep:
_, ok = apprev.Spec.WorkflowStepDefinitions[splitName]
default:
return capName
}
if ok {
return splitName
}
}
return capName
}
// DryRunTemplateLoader return a function that do the same work as
// LoadTemplate, but load template from provided ones before loading from
// cluster through LoadTemplate
func DryRunTemplateLoader(defs []*unstructured.Unstructured) TemplateLoaderFn {
return func(ctx context.Context, r client.Client, capName string, capType types.CapType, annotations map[string]string) (*Template, error) {
// retrieve provided cap definitions
for _, def := range defs {
if def.GetKind() == v1beta1.ComponentDefinitionKind &&
capType == types.TypeComponentDefinition && def.GetName() == capName {
compDef := &v1beta1.ComponentDefinition{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(def.Object, compDef); err != nil {
return nil, errors.Wrap(err, "invalid component definition")
}
tmpl, err := newTemplateOfCompDefinition(compDef)
if err != nil {
return nil, errors.WithMessagef(err, "cannot load template of component definition %q", capName)
}
return tmpl, nil
}
if def.GetKind() == v1beta1.TraitDefinitionKind &&
capType == types.TypeTrait && def.GetName() == capName {
traitDef := &v1beta1.TraitDefinition{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(def.Object, traitDef); err != nil {
return nil, errors.Wrap(err, "invalid trait definition")
}
tmpl, err := newTemplateOfTraitDefinition(traitDef)
if err != nil {
return nil, errors.WithMessagef(err, "cannot load template of trait definition %q", capName)
}
return tmpl, nil
}
}
// not found in provided cap definitions
// then try to retrieve from cluster
tmpl, err := LoadTemplate(ctx, r, capName, capType, annotations)
if err != nil {
return nil, errors.WithMessagef(err, "cannot load template %q from cluster and provided ones", capName)
}
return tmpl, nil
}
}
func newTemplateOfCompDefinition(compDef *v1beta1.ComponentDefinition) (*Template, error) {
tmpl := &Template{
Reference: compDef.Spec.Workload,
ComponentDefinition: compDef,
}
if err := loadSchematicToTemplate(tmpl, compDef.Spec.Status, compDef.Spec.Schematic, compDef.Spec.Extension); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
if compDef.Annotations["type"] == string(types.TerraformCategory) {
tmpl.CapabilityCategory = types.TerraformCategory
}
return tmpl, nil
}
func newTemplateOfTraitDefinition(traitDef *v1beta1.TraitDefinition) (*Template, error) {
tmpl := &Template{
TraitDefinition: traitDef,
}
if err := loadSchematicToTemplate(tmpl, traitDef.Spec.Status, traitDef.Spec.Schematic, traitDef.Spec.Extension); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
return tmpl, nil
}
func newTemplateOfWorkloadDefinition(wlDef *v1beta1.WorkloadDefinition) (*Template, error) {
tmpl := &Template{
Reference: common.WorkloadTypeDescriptor{Type: wlDef.Spec.Reference.Name},
WorkloadDefinition: wlDef,
}
if err := loadSchematicToTemplate(tmpl, wlDef.Spec.Status, wlDef.Spec.Schematic, wlDef.Spec.Extension); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
return tmpl, nil
}
func newTemplateOfPolicyDefinition(def *v1beta1.PolicyDefinition) (*Template, error) {
tmpl := &Template{
PolicyDefinition: def,
}
if err := loadSchematicToTemplate(tmpl, nil, def.Spec.Schematic, nil); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
return tmpl, nil
}
func newTemplateOfWorkflowStepDefinition(def *v1beta1.WorkflowStepDefinition) (*Template, error) {
tmpl := &Template{
WorkflowStepDefinition: def,
}
if err := loadSchematicToTemplate(tmpl, nil, def.Spec.Schematic, nil); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
return tmpl, nil
}
// loadSchematicToTemplate loads common data that all kind definitions have.
func loadSchematicToTemplate(tmpl *Template, status *common.Status, schematic *common.Schematic, ext *runtime.RawExtension) error {
if status != nil {
tmpl.CustomStatus = status.CustomStatus
tmpl.Health = status.HealthPolicy
}
if schematic != nil {
if schematic.CUE != nil {
tmpl.CapabilityCategory = types.CUECategory
tmpl.TemplateStr = schematic.CUE.Template
}
if schematic.Terraform != nil {
tmpl.CapabilityCategory = types.TerraformCategory
tmpl.Terraform = schematic.Terraform
return nil
}
}
if tmpl.TemplateStr == "" && ext != nil {
tmpl.CapabilityCategory = types.CUECategory
extension := map[string]interface{}{}
if err := json.Unmarshal(ext.Raw, &extension); err != nil {
return errors.Wrap(err, "cannot parse capability extension")
}
if extTemplate, ok := extension["template"]; ok {
if tmpStr, ok := extTemplate.(string); ok {
tmpl.TemplateStr = tmpStr
}
}
}
return nil
}
// ConvertTemplateJSON2Object convert spec.extension or spec.schematic to object
func ConvertTemplateJSON2Object(capabilityName string, in *runtime.RawExtension, schematic *common.Schematic) (types.Capability, error) {
var t types.Capability
t.Name = capabilityName
if in != nil && in.Raw != nil {
err := json.Unmarshal(in.Raw, &t)
if err != nil {
return t, errors.Wrapf(err, "parse extension fail")
}
}
capTemplate := &Template{}
if err := loadSchematicToTemplate(capTemplate, nil, schematic, in); err != nil {
return t, errors.WithMessage(err, "cannot resolve schematic")
}
if capTemplate.TemplateStr != "" {
t.CueTemplate = capTemplate.TemplateStr
}
return t, nil
}