mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
* Docs(KEP): Go SDK for X-Definition Authoring (defkit) Introduces KEP proposal for defkit, a Go SDK that enables platform engineers to author X-Definitions using native Go code instead of CUE. Key proposed features: - Fluent builder API for Component, Trait, Policy, and WorkflowStep definitions - Transparent Go-to-CUE compilation - IDE support with autocomplete and type checking - Schema-agnostic resource construction - Collection operations (map, filter, dedupe) - Composable health and status expressions - Addon integration with godef/ folder support - Module dependencies for definition sharing via go get Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(KEP): Examples and minor api changes given in the document Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(KEP): align defkit examples - Fix golang version in CI - Fix variable declaration in example for testing - Add Is() comparison method to status check Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): add security considerations section - Add goal #7 for secure code execution model - Add Security Considerations section covering: - Code execution model (compile-time only, not runtime) - Security benefits over CUE (static analysis, dependency scanning) - Threat model with mitigations Addresses PR feedback about code execution safety. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): add module versioning and definition placement sections - Add Module Versioning section explaining git-based version derivation - Add Definition Placement section covering: - Motivation for placement constraints in multi-cluster environments - Fluent API for placement (RunOn, NotRunOn, label conditions) - Logical combinators (And, Or, Not) - Module-level placement defaults - Placement evaluation logic - CLI experience for managing cluster labels - Add Module Hooks section for lifecycle callbacks - Minor fixes and clarifications throughout Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): add module hooks and update addon integration sections - Add Module Hooks section covering: - Use cases (CRD installation, setup scripts, post-install samples) - Hook configuration in module.yaml (pre-apply, post-apply) - Hook types (path for manifests, script for shell scripts) - waitFor field with condition names and CUE expressions - CLI usage (--skip-hooks, --dry-run) - Update Addon Integration section with implementation details: - godef/ folder structure with module.yaml - CLI flags (--godef, --components, --traits, --policies, --workflowsteps) - Conflict detection and --override-definitions flag - Development workflow Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): address PR review comments and clarify placement labels - Fix misleading "Sandboxed Compilation" claim (cubic-ai feedback) - renamed to "Isolated Compilation" and clarified that security relies on trust model, not technical sandboxing - Fix inconsistent apiVersion in module hooks example (defkit.oam.dev/v1 → core.oam.dev/v1beta1) - Clarify that placement uses vela-cluster-identity ConfigMap directly, not the vela cluster labels command (which is planned for future) - Add --stats flag to apply-module CLI documentation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): fix API documentation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add core fluent API types for Go-based definitions Introduce the defkit package providing a fluent Go API for defining KubeVela X-Definitions (components, traits, policies, workflow steps). Core types added: - types.go: Value, Condition, Param interfaces - base.go: Base definition types and interfaces - param.go: Parameter builders (String, Int, Bool, Array, Map, Struct, Enum) - expr.go: Expression builders for conditions and comparisons - resource.go: Resource operations (Set, SetIf, Spread) - context.go: KubeVela context references (appName, namespace, etc.) - test_context.go: Test utilities for definition validation This enables writing type-safe Go definitions that compile to CUE. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add collection operations and helper builders Add fluent API for array/collection transformations: - CollectionOp with Filter, Map, Pick, Wrap, Dedupe operations - From() and Each() entry points for collection pipelines - FieldRef, FieldEquals, FieldMap for field-level operations - MultiSource for complex multi-array comprehensions - Add helper builders for template variables - Add value transformation utilities Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add CUE code generator Implement CUEGenerator that transforms Go definitions into CUE code Added helper methods and writers for conversion Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add status and health policy builders Add fluent builders for customStatus and healthPolicy CUE generation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add definition type builders Add fluent builders for all four KubeVela X-Definition types: - ComponentDefinition - TraitDefinition - PolicyDefinition - WorkflowStepDefinition Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(goloader): add Go module loader for definitions - Definition interface and registry for runtime discovery - Discover and parse Go-based definition files - Compile Go definitions to CUE at runtime - Module environment for batch processing - Parallel generation for better performance Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(cli): add vela def commands for Go-based definitions - init-module: scaffold a new Go definition module - apply-module: compile and apply definitions to cluster - list-module: show definitions in a module - validate-module: validate definitions without applying - Also support the cue commands for xdefintions for go code Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add testing utilities and matchers - CUE comparison matchers for Ginkgo/Gomega tests - Test helpers for definition validation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add patch container helpers for container mod operations Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(cli): update the go module to 1.23.8 for defkit init-module command Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Refactor: Add grouped help output for vela def command Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add definition placement for cluster-aware deployments Enable definitions to specify which clusters they should run on based on cluster identity labels stored in a well-known ConfigMap. Also derives module version from git tags and improves init-module to create directories from --name flag. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add RunOn/NotRunOn fluent API for placement constraints Add placement methods to all definition builders allowing definitions to specify cluster eligibility using the placement package's fluent API. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(defkit): add commented placement example to module.yaml template Show users the placement syntax in generated module.yaml without setting actual values. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add module-level placement support Add placement constraints at the module level in module.yaml that apply to all definitions unless overridden at definition level. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add CLI placement enforcement in apply-module Add placement constraint checking to `vela def apply-module` command. Definitions are skipped if cluster labels don't match module placement. - Add --ignore-placement flag to bypass placement checks - Display placement status during apply with clear skip reasons - Track placement-skipped count in summary output Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): show all flags in subcommand help output Fix custom help function to properly display flags for def subcommands like init-module and apply-module instead of only showing parent flags. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): apply name prefix to definitions in apply-module The --prefix flag was not being applied to definition names. The prefix was set in module loader metadata but not used when creating Kubernetes objects from parsed CUE. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore(defkit): align module command help with standard vela pattern Remove argument placeholders from command Use field to align with other vela commands (addon, cluster, workflow). Arguments are shown in examples and individual --help output instead of the listing. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(goloader): use json.Unmarshal for go mod download output The downloadGoModule function parses JSON output from 'go mod download -json' but was incorrectly using yaml.Unmarshal with json struct tags. The yaml.v3 library ignores json tags, resulting in empty field values. This would cause remote Go module loading (e.g., github.com/foo/bar@v1.0.0) to fail with "go mod download did not return a directory" because result.Dir would be empty. Fix: Use json.Unmarshal instead since the data is JSON from the Go toolchain. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(goloader): use semver for MinVelaVersion comparison String comparison of version numbers is incorrect for cases like "v1.10.0" > "v1.9.0" which returns false due to lexicographic ordering. Use the Masterminds/semver library (already a dependency) for proper semantic version comparison in ValidateModule(). Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(placement): validate operator in module placement conditions Add validation to catch invalid placement operators at module load time instead of silently failing at runtime evaluation. - Add Operator.IsValid() method to check for valid operators - Add ValidOperators() helper function - Add validatePlacementConditions() in ValidateModule() - Provides clear error message with valid operator list Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(cli): validate conflict strategy in apply-module Invalid --conflict values like "invalid" were silently accepted and would fall through the switch statement, behaving like "overwrite". Add ConflictStrategy.IsValid() method and validation at flag parsing to provide clear error message for invalid values. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(placement): support definition-level placement constraints Previously only module-level placement was enforced. Now individual definitions can specify their own placement constraints that override module defaults. Changes: - Add Placement field to DefinitionInfo and DefinitionPlacement types - Add GetPlacement/HasPlacement to Definition interface - Update registry ToJSON to include placement in output - Update goloader to capture definition placement from registry - Update CLI apply-module to use GetEffectivePlacement() for combining module-level and definition-level placement - Add comprehensive tests for definition placement Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore(defkit): remove dead PatchTemplate code PatchTemplate, PatchOp, SetPatchOp, and SetIfPatchOp were defined but never used anywhere in the codebase. The PatchResource type already provides the same functionality and is the one actually being used through Template.Patch(). Removed: - PatchTemplate struct and its methods (ToCue, SetIf, Set) - PatchOp interface - SetPatchOp struct and its ToCue method - SetIfPatchOp struct and its ToCue method - NewPatchTemplate constructor This cleanup reduces maintenance burden without affecting any functionality. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(cli): pass actual VelaVersion to validate-module command The help text for `vela def validate-module` promised to check minVelaVersion requirements but ValidateModule() was called with an empty string, causing the check to be silently skipped. Now passes velaversion.VelaVersion so modules specifying a minimum KubeVela version will be properly validated against the current CLI version. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): implement WithDetails() and FromTyped() APIs WithDetails(): - Adds WithDetails(message, details...) method to StatusBuilder - Allows adding structured key-value details alongside status messages - Uses existing StatusDetail and statusWithDetailsExpr infrastructure - Example: s.WithDetails(s.Format("Ready: %v", ...), s.Detail("endpoint", ...)) FromTyped(): - Converts typed Kubernetes objects (runtime.Object) to Resource - Provides compile-time type safety for building resources - Requires TypeMeta to be set on the object - Includes MustFromTyped() variant that panics on error - Example: defkit.FromTyped(&appsv1.Deployment{...}) Both APIs were documented in the KEP but not implemented. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Style(defkit): apply gofmt formatting Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): fix remote module download with @latest version When downloading a Go module without an explicit version, always append @latest to ensure go mod download fetches from the remote repository instead of skipping the download. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): support running def commands from any directory Previously, module commands like `vela def list-module` only worked when run from within the kubevela repository. Now they work from any directory by honoring replace directives in the source module's go.mod. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): generate doc.go files in init-module Create doc.go files with package documentation in each definition directory (components, traits, policies, workflowsteps). This ensures go mod tidy works correctly by making each directory a valid Go package, and provides helpful examples for users creating new definitions. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): deduplicate definitions from overlapping directory scans The module loader scans both conventional directories (components/, traits/, etc.) and the root directory. Since DiscoverDefinitions uses recursive filepath.Walk, files in subdirectories were found twice. Added file tracking to skip already-processed files. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): validate placement constraints and fix GOWORK interference Add validation for conflicting placement constraints at registration time. Definitions with logically impossible placement (e.g., same condition in both RunOn and NotRunOn) now fail fast with a clear error message. Also fix placement loading when parent directories contain go.work files by setting GOWORK=off when running the registry generator. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add parameter schema constraints and runtime condition methods Extend the parameter fluent API with comprehensive validation and conditional logic support: - Schema constraints for input validation (Min/Max, Pattern, MinLen/MaxLen, MinItems/MaxItems) - Runtime conditions for template logic (In, Contains, Matches, StartsWith/EndsWith, Len*, IsEmpty/IsNotEmpty, HasKey, IsFalse) Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add waitFor support with CUE expressions for module hooks Add the ability to specify custom readiness conditions for module hooks using the new `waitFor` field. This allows users to define precise conditions for when resources should be considered ready. The waitFor field supports two formats: - Simple condition name (e.g., "Ready", "Established") - checks status.conditions for the named condition with status "True" - CUE expression (e.g., "status.replicas == status.readyReplicas") - evaluated against the full resource for flexible readiness checks Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(addon): add godef support for Go-based definitions in addons Add support for a godef/ folder in addons that allows writing definitions in Go instead of CUE. When an addon is enabled, Go definitions are automatically compiled to CUE and deployed alongside traditional CUE definitions. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: lint issues and make reviewable Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: lint and build failure Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: lint and ci errors Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: golangci-lint errors for defkit package - Use standard library errors (errors.Is/As) instead of pkg/errors - Fix ineffassign issues by scoping variables correctly - Add nolint comments for intentional nilerr, makezero patterns - Combine chained appends in addon init.go - Add gosec nolint for CLI file operations and permissions - Increase gocyclo threshold to 35, nolint complex CLI commands Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: kubectl installation with retry and fallback version in github actions Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(ci): hardcode kubectl version to avoid flaky CDN endpoint Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore: improve test coverage for codecov Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore: add more tests for codecov and CI to pass Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: ci failure on style Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: OperatorNotEquals to fail closed with empty values Change NotEquals operator to return false when Values slice is empty, matching the fail-closed behavior of Equals operator. This prevents silent widening of placement eligibility when a malformed constraint is created. Following Kubernetes label selector semantics where In/NotIn operators require non-empty values, we apply a fail-closed approach for safety in placement decisions. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: OpenArrayParam field shadowing and remove redundant GetName() Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: path traversal vulnerability in Go definition scaffolding Validate Go definition names before using them in file paths to prevent creation of files outside the addon directory. Unsanitized names could contain path traversal segments (e.g., "../../../etc/passwd") allowing arbitrary file writes. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: unescaped string interpolation in health_expr CUE generation Use %q format verb in formatValue() to properly escape quotes and special characters when generating CUE strings. Update fieldContainsExpr to use formatValue() instead of raw string interpolation. This prevents invalid CUE when substring values contain quotes or backslashes. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: Guard against typed nil in Gomega matchers to prevent panic Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: Guard against malformed bracket path in parseBracketAccess Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: incomplete AppRevision test to actually verify resolution Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: apply fail-closed behavior to NotIn with empty values Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Doc: Added note about RawCUE and some alignment style Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> --------- Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in>
555 lines
19 KiB
Go
555 lines
19 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 addon
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"helm.sh/helm/v3/pkg/chart"
|
|
"helm.sh/helm/v3/pkg/chartutil"
|
|
errors2 "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/client-go/rest"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/pkg/definition"
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
|
"github.com/oam-dev/kubevela/pkg/oam/util"
|
|
"github.com/oam-dev/kubevela/pkg/utils/addon"
|
|
"github.com/oam-dev/kubevela/pkg/utils/common"
|
|
)
|
|
|
|
const (
|
|
compDefAnnotation = "addon.oam.dev/componentDefinitions"
|
|
traitDefAnnotation = "addon.oam.dev/traitDefinitions"
|
|
workflowStepDefAnnotation = "addon.oam.dev/workflowStepDefinitions"
|
|
policyDefAnnotation = "addon.oam.dev/policyDefinitions"
|
|
defKeytemplate = "addon-%s-%s"
|
|
compMapKey = "comp"
|
|
traitMapKey = "trait"
|
|
wfStepMapKey = "wfStep"
|
|
policyMapKey = "policy"
|
|
)
|
|
|
|
// parse addon's created x-defs in addon-app's annotation, this will be used to check whether app still using it while disabling.
|
|
func passDefInAppAnnotation(defs []*unstructured.Unstructured, app *v1beta1.Application) error {
|
|
var comps, traits, workflowSteps, policies []string
|
|
for _, def := range defs {
|
|
if !checkBondComponentExist(*def, *app) {
|
|
// if the definition binding a component, and the component not exist, skip recording.
|
|
continue
|
|
}
|
|
switch def.GetObjectKind().GroupVersionKind().Kind {
|
|
case v1beta1.ComponentDefinitionKind:
|
|
comps = append(comps, def.GetName())
|
|
case v1beta1.TraitDefinitionKind:
|
|
traits = append(traits, def.GetName())
|
|
case v1beta1.WorkflowStepDefinitionKind:
|
|
workflowSteps = append(workflowSteps, def.GetName())
|
|
case v1beta1.PolicyDefinitionKind:
|
|
policies = append(policies, def.GetName())
|
|
default:
|
|
return fmt.Errorf("cannot handle definition types %s, name %s", def.GetObjectKind().GroupVersionKind().Kind, def.GetName())
|
|
}
|
|
}
|
|
if len(comps) != 0 {
|
|
app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{compDefAnnotation: strings.Join(comps, ",")}))
|
|
}
|
|
if len(traits) != 0 {
|
|
app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{traitDefAnnotation: strings.Join(traits, ",")}))
|
|
}
|
|
if len(workflowSteps) != 0 {
|
|
app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{workflowStepDefAnnotation: strings.Join(workflowSteps, ",")}))
|
|
}
|
|
if len(policies) != 0 {
|
|
app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{policyDefAnnotation: strings.Join(policies, ",")}))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// check whether this addon has been used by some applications
|
|
func checkAddonHasBeenUsed(ctx context.Context, k8sClient client.Client, name string, addonApp v1beta1.Application, config *rest.Config) ([]v1beta1.Application, error) {
|
|
apps := v1beta1.ApplicationList{}
|
|
if err := k8sClient.List(ctx, &apps, client.InNamespace("")); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(apps.Items) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
createdDefs := make(map[string]bool)
|
|
for key, defNames := range addonApp.GetAnnotations() {
|
|
switch key {
|
|
case compDefAnnotation, traitDefAnnotation, workflowStepDefAnnotation, policyDefAnnotation:
|
|
merge2DefMap(key, defNames, createdDefs)
|
|
}
|
|
}
|
|
|
|
if len(createdDefs) == 0 {
|
|
if err := findLegacyAddonDefs(ctx, k8sClient, name, addonApp.GetLabels()[oam.LabelAddonRegistry], config, createdDefs); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var res []v1beta1.Application
|
|
CHECKNEXT:
|
|
for _, app := range apps.Items {
|
|
for _, component := range app.Spec.Components {
|
|
if createdDefs[fmt.Sprintf(defKeytemplate, compMapKey, component.Type)] {
|
|
res = append(res, app)
|
|
// this app has used this addon, there is no need check other components
|
|
continue CHECKNEXT
|
|
}
|
|
for _, trait := range component.Traits {
|
|
if createdDefs[fmt.Sprintf(defKeytemplate, traitMapKey, trait.Type)] {
|
|
res = append(res, app)
|
|
continue CHECKNEXT
|
|
}
|
|
}
|
|
}
|
|
|
|
if app.Spec.Workflow != nil && len(app.Spec.Workflow.Steps) != 0 {
|
|
for _, s := range app.Spec.Workflow.Steps {
|
|
if createdDefs[fmt.Sprintf(defKeytemplate, wfStepMapKey, s.Type)] {
|
|
res = append(res, app)
|
|
continue CHECKNEXT
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(app.Spec.Policies) != 0 {
|
|
for _, p := range app.Spec.Policies {
|
|
if createdDefs[fmt.Sprintf(defKeytemplate, policyMapKey, p.Type)] {
|
|
res = append(res, app)
|
|
continue CHECKNEXT
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// merge2DefMap will parse annotation in addon's app to 'created x-definition'. Then stroe them in defMap
|
|
func merge2DefMap(defType string, defNames string, defMap map[string]bool) {
|
|
list := strings.Split(defNames, ",")
|
|
template := "addon-%s-%s"
|
|
for _, defName := range list {
|
|
switch defType {
|
|
case compDefAnnotation:
|
|
defMap[fmt.Sprintf(template, compMapKey, defName)] = true
|
|
case traitDefAnnotation:
|
|
defMap[fmt.Sprintf(template, traitMapKey, defName)] = true
|
|
case workflowStepDefAnnotation:
|
|
defMap[fmt.Sprintf(template, wfStepMapKey, defName)] = true
|
|
case policyDefAnnotation:
|
|
defMap[fmt.Sprintf(template, policyMapKey, defName)] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// for old addon's app no 'created x-definitions' annotation, fetch the definitions from alive addon registry. Put them in defMap
|
|
func findLegacyAddonDefs(ctx context.Context, k8sClient client.Client, addonName string, registryName string, config *rest.Config, defs map[string]bool) error {
|
|
// if the addon enable by local we cannot fetch the source definitions yet, so skip the check
|
|
if registryName == "local" {
|
|
return nil
|
|
}
|
|
|
|
registryDS := NewRegistryDataStore(k8sClient)
|
|
registries, err := registryDS.ListRegistries(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var defObjects []*unstructured.Unstructured
|
|
for i, registry := range registries {
|
|
if registry.Name == registryName {
|
|
var uiData *UIData
|
|
if !IsVersionRegistry(registry) {
|
|
installer := NewAddonInstaller(ctx, k8sClient, nil, nil, config, ®istries[i], nil, nil, nil)
|
|
metas, err := installer.getAddonMeta()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
meta := metas[addonName]
|
|
// only fetch definition files from registry.
|
|
uiData, err = registry.GetUIData(&meta, UnInstallOptions)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "cannot fetch addon difinition files from registry")
|
|
}
|
|
} else {
|
|
versionedRegistry := BuildVersionedRegistry(registry.Name, registry.Helm.URL, &common.HTTPOption{
|
|
Username: registry.Helm.Username,
|
|
Password: registry.Helm.Password,
|
|
InsecureSkipTLS: registry.Helm.InsecureSkipTLS,
|
|
})
|
|
uiData, err = versionedRegistry.GetAddonUIData(ctx, addonName, "")
|
|
if err != nil {
|
|
return errors.Wrapf(err, "cannot fetch addon difinition files from registry")
|
|
}
|
|
}
|
|
|
|
for _, defYaml := range uiData.Definitions {
|
|
def, err := renderObject(defYaml)
|
|
if err != nil {
|
|
// don't let one error defined definition block whole disable process
|
|
continue
|
|
}
|
|
defObjects = append(defObjects, def)
|
|
}
|
|
for _, cueDef := range uiData.CUEDefinitions {
|
|
def := definition.Definition{Unstructured: unstructured.Unstructured{}}
|
|
err := def.FromCUEString(cueDef.Data, config)
|
|
if err != nil {
|
|
// don't let one error defined cue definition block whole disable process
|
|
continue
|
|
}
|
|
defObjects = append(defObjects, &def.Unstructured)
|
|
}
|
|
}
|
|
}
|
|
for _, defObject := range defObjects {
|
|
switch defObject.GetObjectKind().GroupVersionKind().Kind {
|
|
case v1beta1.ComponentDefinitionKind:
|
|
defs[fmt.Sprintf(defKeytemplate, "comp", defObject.GetName())] = true
|
|
case v1beta1.TraitDefinitionKind:
|
|
defs[fmt.Sprintf(defKeytemplate, "trait", defObject.GetName())] = true
|
|
case v1beta1.WorkflowStepDefinitionKind:
|
|
defs[fmt.Sprintf(defKeytemplate, "wfStep", defObject.GetName())] = true
|
|
case v1beta1.PolicyDefinitionKind:
|
|
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func appsDependsOnAddonErrInfo(apps []v1beta1.Application) string {
|
|
var appsNamespaceNameList []string
|
|
i := 0
|
|
for _, app := range apps {
|
|
appsNamespaceNameList = append(appsNamespaceNameList, app.Namespace+"/"+app.Name)
|
|
i++
|
|
if i > 2 && len(apps) > i {
|
|
appsNamespaceNameList = append(appsNamespaceNameList, fmt.Sprintf("and other %d more", len(apps)-i))
|
|
break
|
|
}
|
|
}
|
|
return fmt.Sprintf("this addon is being used by: %s applications. Please delete all of them before removing.", strings.Join(appsNamespaceNameList, ", "))
|
|
}
|
|
|
|
// IsLocalRegistry checks if the registry is local
|
|
func IsLocalRegistry(r Registry) bool {
|
|
return r.Name == LocalAddonRegistryName
|
|
}
|
|
|
|
// IsVersionRegistry check the repo source if support multi-version addon
|
|
func IsVersionRegistry(r Registry) bool {
|
|
return r.Helm != nil
|
|
}
|
|
|
|
// InstallOption define additional option for installation
|
|
type InstallOption func(installer *Installer)
|
|
|
|
// SkipValidateVersion means skip validating system version
|
|
func SkipValidateVersion(installer *Installer) {
|
|
installer.skipVersionValidate = true
|
|
}
|
|
|
|
// DryRunAddon means only generate yaml for addon instead of installing it
|
|
func DryRunAddon(installer *Installer) {
|
|
installer.dryRun = true
|
|
}
|
|
|
|
// OverrideDefinitions means override definitions within this addon if some of them already exist
|
|
func OverrideDefinitions(installer *Installer) {
|
|
installer.overrideDefs = true
|
|
}
|
|
|
|
// AllowGoDefOverride is a marker option indicating that Go definitions can override CUE definitions
|
|
// within the same addon. This is used when enabling local addons that have both definitions/ and godef/ folders.
|
|
type AllowGoDefOverride struct{}
|
|
|
|
// OverrideGoDefConflicts allows Go definitions to override CUE definitions when conflicts are detected
|
|
// within the same addon. Use this when you have intentionally defined the same definition in both
|
|
// definitions/ (CUE) and godef/ (Go) folders.
|
|
func OverrideGoDefConflicts() AllowGoDefOverride {
|
|
return AllowGoDefOverride{}
|
|
}
|
|
|
|
// IsAddonDir validates an addon directory.
|
|
// It checks required files like metadata.yaml and template.yaml
|
|
func IsAddonDir(dirName string) (bool, error) {
|
|
if fi, err := os.Stat(dirName); err != nil {
|
|
return false, err
|
|
} else if !fi.IsDir() {
|
|
return false, errors.Errorf("%q is not a directory", dirName)
|
|
}
|
|
|
|
// Load metadata.yaml
|
|
metadataYaml := filepath.Join(dirName, MetadataFileName)
|
|
if _, err := os.Stat(metadataYaml); os.IsNotExist(err) {
|
|
return false, errors.Errorf("no %s exists in directory %q", MetadataFileName, dirName)
|
|
}
|
|
metadataYamlContent, err := os.ReadFile(filepath.Clean(metadataYaml))
|
|
if err != nil {
|
|
return false, errors.Errorf("cannot read %s in directory %q", MetadataFileName, dirName)
|
|
}
|
|
|
|
// Check metadata.yaml contents
|
|
metadataContent := new(Meta)
|
|
if err := yaml.Unmarshal(metadataYamlContent, &metadataContent); err != nil {
|
|
return false, err
|
|
}
|
|
if metadataContent == nil {
|
|
return false, errors.Errorf("metadata (%s) missing", MetadataFileName)
|
|
}
|
|
if metadataContent.Name == "" {
|
|
return false, errors.Errorf("addon name is empty")
|
|
}
|
|
if metadataContent.Version == "" {
|
|
return false, errors.Errorf("addon version is empty")
|
|
}
|
|
|
|
// Load template.yaml/cue
|
|
var errYAML error
|
|
var errCUE error
|
|
templateYAML := filepath.Join(dirName, TemplateFileName)
|
|
templateCUE := filepath.Join(dirName, AppTemplateCueFileName)
|
|
_, errYAML = os.Stat(templateYAML)
|
|
_, errCUE = os.Stat(templateCUE)
|
|
if os.IsNotExist(errYAML) && os.IsNotExist(errCUE) {
|
|
return false, fmt.Errorf("no %s or %s exists in directory %q", TemplateFileName, AppTemplateCueFileName, dirName)
|
|
}
|
|
if errYAML != nil && errCUE != nil {
|
|
return false, errors.Errorf("cannot stat %s or %s", TemplateFileName, AppTemplateCueFileName)
|
|
}
|
|
|
|
// template.cue have higher priority
|
|
if errCUE == nil {
|
|
templateContent, err := os.ReadFile(filepath.Clean(templateCUE))
|
|
if err != nil {
|
|
return false, fmt.Errorf("cannot read %s: %w", AppTemplateCueFileName, err)
|
|
}
|
|
// Just look for `output` field is enough.
|
|
// No need to load the whole addon package to render the Application.
|
|
if !strings.Contains(string(templateContent), renderOutputCuePath) {
|
|
return false, fmt.Errorf("no %s field in %s", renderOutputCuePath, AppTemplateCueFileName)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// then check template.yaml
|
|
templateYamlContent, err := os.ReadFile(filepath.Clean(templateYAML))
|
|
if err != nil {
|
|
return false, errors.Errorf("cannot read %s in directory %q", TemplateFileName, dirName)
|
|
}
|
|
// Check template.yaml contents
|
|
template := new(v1beta1.Application)
|
|
if err := yaml.Unmarshal(templateYamlContent, &template); err != nil {
|
|
return false, err
|
|
}
|
|
if template == nil {
|
|
return false, errors.Errorf("template (%s) missing", TemplateFileName)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// MakeChartCompatible makes an addon directory compatible with Helm Charts.
|
|
// It essentially creates a Chart.yaml file in it (if it doesn't already have one).
|
|
// If overwrite is true, a Chart.yaml will always be created.
|
|
func MakeChartCompatible(addonDir string, overwrite bool) error {
|
|
// Check if it is an addon dir
|
|
isAddonDir, err := IsAddonDir(addonDir)
|
|
if !isAddonDir {
|
|
return fmt.Errorf("%s is not an addon dir: %w", addonDir, err)
|
|
}
|
|
|
|
// Check if the addon dir has valid Chart.yaml in it.
|
|
// No need to handle error here.
|
|
// If it doesn't contain a valid Chart.yaml (thus errors), we will create it later.
|
|
isChartDir, _ := chartutil.IsChartDir(addonDir)
|
|
|
|
// Only when it is already a Helm Chart, and we don't want to overwrite Chart.yaml,
|
|
// we do nothing.
|
|
if isChartDir && !overwrite {
|
|
return nil
|
|
}
|
|
|
|
// Creating Chart.yaml.
|
|
chartMeta, err := generateChartMetadata(addonDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = chartutil.SaveChartfile(filepath.Join(addonDir, chartutil.ChartfileName), chartMeta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateChartMetadata generates a Chart.yaml file (chart.Metadata) from an addon metadata file (metadata.yaml).
|
|
// It is mostly used to package an addon into a Helm Chart.
|
|
func generateChartMetadata(addonDirPath string) (*chart.Metadata, error) {
|
|
// Load addon metadata.yaml
|
|
meta := &Meta{}
|
|
metaData, err := os.ReadFile(filepath.Clean(filepath.Join(addonDirPath, MetadataFileName)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = yaml.Unmarshal(metaData, meta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate Chart.yaml from metadata.yaml
|
|
chartMeta := &chart.Metadata{
|
|
Name: meta.Name,
|
|
Description: meta.Description,
|
|
// Define Vela addon's type to be library in order to prevent installation of a common chart.
|
|
// Please refer to https://helm.sh/docs/topics/library_charts/
|
|
Type: "library",
|
|
Version: meta.Version,
|
|
AppVersion: meta.Version,
|
|
APIVersion: chart.APIVersionV2,
|
|
Icon: meta.Icon,
|
|
Home: meta.URL,
|
|
Keywords: meta.Tags,
|
|
}
|
|
annotation := generateAnnotation(meta)
|
|
if len(annotation) != 0 {
|
|
chartMeta.Annotations = annotation
|
|
}
|
|
return chartMeta, nil
|
|
}
|
|
|
|
// generateAnnotation generate addon annotation info for chart.yaml, will recorded in index.yaml in helm repo
|
|
func generateAnnotation(meta *Meta) map[string]string {
|
|
res := map[string]string{}
|
|
if meta.SystemRequirements != nil {
|
|
if len(meta.SystemRequirements.VelaVersion) != 0 {
|
|
res[velaSystemRequirement] = meta.SystemRequirements.VelaVersion
|
|
}
|
|
if len(meta.SystemRequirements.KubernetesVersion) != 0 {
|
|
res[kubernetesSystemRequirement] = meta.SystemRequirements.KubernetesVersion
|
|
}
|
|
}
|
|
res[addonSystemRequirement] = meta.Name
|
|
return res
|
|
}
|
|
|
|
func checkConflictDefs(ctx context.Context, k8sClient client.Client, defs []*unstructured.Unstructured, appName string) (map[string]string, error) {
|
|
res := map[string]string{}
|
|
for _, def := range defs {
|
|
checkDef := def.DeepCopy()
|
|
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(checkDef), checkDef)
|
|
if err == nil {
|
|
owner := metav1.GetControllerOf(checkDef)
|
|
if owner == nil || owner.Kind != v1beta1.ApplicationKind {
|
|
res[checkDef.GetName()] = fmt.Sprintf("definition: %s already exist and not belong to any addon \n", checkDef.GetName())
|
|
continue
|
|
}
|
|
if owner.Name != appName {
|
|
// if addon not belong to an addon or addon name is another one, we should put them in result
|
|
addonName := addon.AppName2Addon(owner.Name)
|
|
// If owner.Name isn't an addon app name, show the owner's name directly as the addon name
|
|
if addonName == "" {
|
|
addonName = owner.Name
|
|
}
|
|
res[checkDef.GetName()] = fmt.Sprintf("definition: %s in this addon already exist in %s \n", checkDef.GetName(), addonName)
|
|
}
|
|
}
|
|
if err != nil && !errors2.IsNotFound(err) {
|
|
return nil, errors.Wrapf(err, "check definition %s", checkDef.GetName())
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func produceDefConflictError(conflictDefs map[string]string) error {
|
|
if len(conflictDefs) == 0 {
|
|
return nil
|
|
}
|
|
var errorInfo string
|
|
for _, s := range conflictDefs {
|
|
errorInfo += s
|
|
}
|
|
errorInfo += "if you want override them, please use argument '--override-definitions' to enable \n"
|
|
return errors.New(errorInfo)
|
|
}
|
|
|
|
// checkBondComponentExist will check the ready-to-apply object(def or auxiliary outputs) whether bind to a component
|
|
// if the target component not exist, return false.
|
|
func checkBondComponentExist(u unstructured.Unstructured, app v1beta1.Application) bool {
|
|
var comp string
|
|
var existKey bool
|
|
comp, existKey = u.GetAnnotations()[oam.AnnotationAddonDefinitionBondCompKey]
|
|
if !existKey {
|
|
// this is compatibility logic for deprecated annotation
|
|
comp, existKey = u.GetAnnotations()[oam.AnnotationIgnoreWithoutCompKey]
|
|
if !existKey {
|
|
// if an object(def or auxiliary outputs ) binding no components return true
|
|
return true
|
|
}
|
|
}
|
|
for _, component := range app.Spec.Components {
|
|
if component.Name == comp {
|
|
// the bond component exists, return true
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func validateAddonPackage(addonPkg *InstallPackage) error {
|
|
if reflect.DeepEqual(addonPkg.Meta, Meta{}) {
|
|
return fmt.Errorf("the addon package doesn't have `metadata.yaml`")
|
|
}
|
|
if addonPkg.Name == "" {
|
|
return fmt.Errorf("`matadata.yaml` must define the name of addon")
|
|
}
|
|
if addonPkg.Version == "" {
|
|
return fmt.Errorf("`matadata.yaml` must define the version of addon")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FilterDependencyRegistries will return all registries besides the target registry itself
|
|
func FilterDependencyRegistries(i int, rs []Registry) []Registry {
|
|
if i >= len(rs) {
|
|
return rs
|
|
}
|
|
if i < 0 {
|
|
return rs
|
|
}
|
|
ret := make([]Registry, len(rs)-1)
|
|
copy(ret, rs[:i])
|
|
copy(ret[i:], rs[i+1:])
|
|
return ret
|
|
}
|