Files
kubevela/references/cli/def_module.go
Anoop Gopalakrishnan 29a26d5650 Feat: Defkit implementation (#7037)
* 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>
2026-02-10 15:00:44 +00:00

2140 lines
68 KiB
Go

/*
Copyright 2025 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 cli
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
errors2 "k8s.io/apimachinery/pkg/api/errors"
types2 "k8s.io/apimachinery/pkg/types"
"github.com/oam-dev/kubevela/apis/types"
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/definition/defkit/placement"
"github.com/oam-dev/kubevela/pkg/definition/goloader"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/util"
velaversion "github.com/oam-dev/kubevela/version"
)
const (
// FlagModuleVersion is the flag for module version
FlagModuleVersion = "version"
// FlagModuleTypes is the flag for filtering definition types
FlagModuleTypes = "types"
// FlagModulePrefix is the flag for definition name prefix
FlagModulePrefix = "prefix"
// FlagIgnorePlacement is the flag to ignore placement constraints
FlagIgnorePlacement = "ignore-placement"
// FlagConflictStrategy is the flag for conflict resolution strategy
FlagConflictStrategy = "conflict"
// FlagSkipHooks is the flag to skip all hooks
FlagSkipHooks = "skip-hooks"
// FlagSkipPreApply is the flag to skip pre-apply hooks
FlagSkipPreApply = "skip-pre-apply"
// FlagSkipPostApply is the flag to skip post-apply hooks
FlagSkipPostApply = "skip-post-apply"
// FlagStats is the flag to show detailed statistics
FlagStats = "stats"
)
// ConflictStrategy represents how to handle name conflicts
type ConflictStrategy string
const (
// ConflictStrategySkip skips definitions that already exist
ConflictStrategySkip ConflictStrategy = "skip"
// ConflictStrategyOverwrite overwrites existing definitions
ConflictStrategyOverwrite ConflictStrategy = "overwrite"
// ConflictStrategyFail fails if any definition already exists
ConflictStrategyFail ConflictStrategy = "fail"
// ConflictStrategyRename renames conflicting definitions with a suffix
ConflictStrategyRename ConflictStrategy = "rename"
)
// IsValid returns true if the conflict strategy is a recognized valid value.
func (c ConflictStrategy) IsValid() bool {
switch c {
case ConflictStrategySkip, ConflictStrategyOverwrite, ConflictStrategyFail, ConflictStrategyRename:
return true
default:
return false
}
}
// NewDefinitionApplyModuleCommand creates the `vela def apply-module` command
// to apply a Go module containing definitions to kubernetes
func NewDefinitionApplyModuleCommand(c common.Args, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "apply-module",
Short: "Apply all definitions from a Go module.",
Long: `Apply all definitions from a Go module to kubernetes cluster.
Supports both local paths and remote Go modules:
- Local path: ./my-definitions, /path/to/definitions
- Go module: github.com/myorg/definitions@v1.0.0
The module can contain a module.yaml file with metadata about the module,
including name, version, description, and minimum KubeVela version requirements.`,
Example: `# Apply definitions from a local directory
> vela def apply-module ./my-definitions
# Apply definitions from a Go module
> vela def apply-module github.com/myorg/definitions@v1.0.0
# Apply only component and trait definitions
> vela def apply-module ./my-definitions --types component,trait
# Apply with a name prefix to avoid conflicts
> vela def apply-module ./my-definitions --prefix myorg-
# Apply with conflict resolution strategy
> vela def apply-module ./my-definitions --conflict overwrite
# Dry-run to preview what would be applied
> vela def apply-module ./my-definitions --dry-run`,
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefModule,
types.TagCommandOrder: "2",
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dryRun, err := cmd.Flags().GetBool(FlagDryRun)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagDryRun)
}
namespace, err := cmd.Flags().GetString(FlagNamespace)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", Namespace)
}
version, err := cmd.Flags().GetString(FlagModuleVersion)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleVersion)
}
typesStr, err := cmd.Flags().GetString(FlagModuleTypes)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleTypes)
}
prefix, err := cmd.Flags().GetString(FlagModulePrefix)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModulePrefix)
}
conflictStr, err := cmd.Flags().GetString(FlagConflictStrategy)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagConflictStrategy)
}
conflict := ConflictStrategy(conflictStr)
if !conflict.IsValid() {
return errors.Errorf("invalid conflict strategy %q; valid values: skip, overwrite, fail, rename", conflictStr)
}
ignorePlacement, err := cmd.Flags().GetBool(FlagIgnorePlacement)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagIgnorePlacement)
}
skipHooks, err := cmd.Flags().GetBool(FlagSkipHooks)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagSkipHooks)
}
skipPreApply, err := cmd.Flags().GetBool(FlagSkipPreApply)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagSkipPreApply)
}
skipPostApply, err := cmd.Flags().GetBool(FlagSkipPostApply)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagSkipPostApply)
}
showStats, err := cmd.Flags().GetBool(FlagStats)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagStats)
}
var defTypes []string
if typesStr != "" {
defTypes = strings.Split(typesStr, ",")
for i := range defTypes {
defTypes[i] = strings.TrimSpace(defTypes[i])
}
}
return applyModule(ctx, c, streams, args[0], applyModuleOptions{
namespace: namespace,
version: version,
types: defTypes,
prefix: prefix,
conflict: conflict,
dryRun: dryRun,
ignorePlacement: ignorePlacement,
skipHooks: skipHooks,
skipPreApply: skipPreApply,
skipPostApply: skipPostApply,
showStats: showStats,
})
},
}
cmd.Flags().BoolP(FlagDryRun, "", false, "Preview what would be applied without making changes")
cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Namespace to apply definitions to")
cmd.Flags().StringP(FlagModuleVersion, "v", "", "Version of the module to apply (for remote modules)")
cmd.Flags().StringP(FlagModuleTypes, "t", "", "Comma-separated list of definition types to apply (component,trait,policy,workflow-step)")
cmd.Flags().StringP(FlagModulePrefix, "p", "", "Prefix to add to all definition names")
cmd.Flags().StringP(FlagConflictStrategy, "c", string(ConflictStrategyFail), "Conflict resolution strategy: skip, overwrite, fail, rename")
cmd.Flags().BoolP(FlagIgnorePlacement, "", false, "Ignore placement constraints and apply all definitions")
cmd.Flags().BoolP(FlagSkipHooks, "", false, "Skip all hooks (pre-apply and post-apply)")
cmd.Flags().BoolP(FlagSkipPreApply, "", false, "Skip pre-apply hooks only")
cmd.Flags().BoolP(FlagSkipPostApply, "", false, "Skip post-apply hooks only")
cmd.Flags().BoolP(FlagStats, "", false, "Show detailed timing and statistics")
return cmd
}
// applyModuleOptions contains options for applying a module
type applyModuleOptions struct {
namespace string
version string
types []string
prefix string
conflict ConflictStrategy
dryRun bool
ignorePlacement bool
skipHooks bool
skipPreApply bool
skipPostApply bool
showStats bool
}
// ApplyStats tracks statistics for module application
type ApplyStats struct {
// Timing
StartTime time.Time
ModuleLoadTime time.Duration
PreApplyHookTime time.Duration
DefApplyTime time.Duration
PostApplyHookTime time.Duration
TotalTime time.Duration
// Hook timing details
PreApplyHookDetails []HookTimingDetail
PostApplyHookDetails []HookTimingDetail
// Definition counts by type
Components int
Traits int
Policies int
WorkflowSteps int
// Definition counts by action
Created int
Updated int
Skipped int
Failed int
// Placement stats
PlacementEvaluated int
PlacementEligible int
PlacementSkipped int
// Hook stats
HookResourcesCreated int
HookResourcesUpdated int
OptionalHooksFailed int
}
// HookTimingDetail contains timing info for a single hook
type HookTimingDetail struct {
Name string
Duration time.Duration
Wait bool
}
// NewApplyStats creates a new ApplyStats and starts the timer
func NewApplyStats() *ApplyStats {
return &ApplyStats{
StartTime: time.Now(),
}
}
// applyModule loads and applies all definitions from a module
//
//nolint:gocyclo // Complex module apply logic with hooks, validation, and dry-run support
func applyModule(ctx context.Context, c common.Args, streams util.IOStreams, moduleRef string, opts applyModuleOptions) error {
// Initialize stats tracking
stats := NewApplyStats()
// Load module options
loadOpts := goloader.ModuleLoadOptions{
Version: opts.version,
Types: opts.types,
NamePrefix: opts.prefix,
ResolveDependencies: true,
}
if len(opts.types) == 0 {
loadOpts = goloader.DefaultModuleLoadOptions()
loadOpts.Version = opts.version
loadOpts.NamePrefix = opts.prefix
}
streams.Infof("Loading module from %s...\n", moduleRef)
// Load the module (with timing)
loadStart := time.Now()
module, err := goloader.LoadModule(ctx, moduleRef, loadOpts)
if err != nil {
return errors.Wrapf(err, "failed to load module from %s", moduleRef)
}
stats.ModuleLoadTime = time.Since(loadStart)
// Print module summary
streams.Infof("\n%s\n", module.Summary())
// Get kubernetes client
config, err := c.GetConfig()
if err != nil {
return errors.Wrap(err, "failed to get kubernetes config")
}
k8sClient, err := c.GetClient()
if err != nil {
return errors.Wrap(err, "failed to get kubernetes client")
}
// Get cluster labels for placement checking (unless ignoring or dry-run)
var clusterLabels map[string]string
var modulePlacement placement.PlacementSpec
checkPlacement := !opts.ignorePlacement && !opts.dryRun
if checkPlacement {
// Get module-level placement
if module.Metadata.Spec.Placement != nil {
modulePlacement = module.Metadata.Spec.Placement.ToPlacementSpec()
}
// Check if any placement constraints exist (module-level or definition-level)
hasAnyPlacement := !modulePlacement.IsEmpty()
if !hasAnyPlacement {
for _, result := range module.Definitions {
if result.Definition.Placement != nil && !result.Definition.Placement.IsEmpty() {
hasAnyPlacement = true
break
}
}
}
// Fetch cluster labels if there's any placement to check
if hasAnyPlacement {
var labelErr error
clusterLabels, labelErr = placement.GetClusterLabels(ctx, k8sClient)
if labelErr != nil {
streams.Infof("Warning: Could not fetch cluster labels: %v\n", labelErr)
streams.Infof("Placement constraints will not be enforced.\n\n")
checkPlacement = false
} else {
streams.Infof("Checking placement constraints...\n")
streams.Infof("Cluster labels: %s\n\n", placement.FormatClusterLabels(clusterLabels))
}
}
}
if opts.ignorePlacement {
streams.Infof("Warning: Ignoring placement constraints (--ignore-placement)\n\n")
}
// Execute pre-apply hooks
if !opts.skipHooks && !opts.skipPreApply && module.Metadata.Spec.Hooks.HasPreApply() {
hookExecutor := goloader.NewHookExecutor(k8sClient, module.Path, opts.namespace, opts.dryRun, streams)
hookStats, err := hookExecutor.ExecuteHooks(ctx, "pre-apply", module.Metadata.Spec.Hooks.PreApply)
if err != nil {
return errors.Wrap(err, "pre-apply hooks failed")
}
stats.PreApplyHookTime = hookStats.TotalDuration
stats.HookResourcesCreated += hookStats.ResourcesCreated
stats.HookResourcesUpdated += hookStats.ResourcesUpdated
stats.OptionalHooksFailed += hookStats.OptionalFailed
for _, detail := range hookStats.HookDetails {
stats.PreApplyHookDetails = append(stats.PreApplyHookDetails, HookTimingDetail{
Name: detail.Name,
Duration: detail.Duration,
Wait: detail.Wait,
})
}
}
// Track results
var failedDefs []string
defApplyStart := time.Now()
// Apply each definition
for _, result := range module.Definitions {
if result.Error != nil {
stats.Failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: %v", result.Definition.FilePath, result.Error))
continue
}
// Parse CUE to definition
def := pkgdef.Definition{}
if err := def.FromCUEString(result.CUE, config); err != nil {
stats.Failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: %v", result.Definition.Name, err))
continue
}
def.SetNamespace(opts.namespace)
// Apply name prefix if specified
if opts.prefix != "" {
def.SetName(opts.prefix + def.GetName())
}
// Check placement constraints (combine module-level and definition-level)
if checkPlacement {
// Get definition-level placement (if any)
var defPlacement placement.PlacementSpec
if result.Definition.Placement != nil {
defPlacement = result.Definition.Placement.ToPlacementSpec()
}
// Combine with module-level placement (definition overrides module)
effectivePlacement := placement.GetEffectivePlacement(modulePlacement, defPlacement)
if !effectivePlacement.IsEmpty() {
stats.PlacementEvaluated++
placementResult := placement.Evaluate(effectivePlacement, clusterLabels)
if !placementResult.Eligible {
streams.Infof(" ✗ %s %s: skipped (%s)\n", def.GetKind(), def.GetName(), placementResult.Reason)
stats.PlacementSkipped++
continue
}
stats.PlacementEligible++
streams.Infof(" ✓ %s %s: eligible\n", def.GetKind(), def.GetName())
}
}
// Track definition type
trackDefinitionType(stats, result.Definition.Type)
// Dry-run mode: just print the YAML
if opts.dryRun {
s, err := prettyYAMLMarshal(def.Object)
if err != nil {
stats.Failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: %v", result.Definition.Name, err))
continue
}
streams.Info(s)
streams.Info("---\n")
stats.Created++
continue
}
// Check if definition already exists
existingDef := pkgdef.Definition{}
existingDef.SetGroupVersionKind(def.GroupVersionKind())
err = k8sClient.Get(ctx, types2.NamespacedName{
Namespace: opts.namespace,
Name: def.GetName(),
}, &existingDef)
exists := err == nil
if err != nil && !errors2.IsNotFound(err) {
stats.Failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: %v", def.GetName(), err))
continue
}
// Handle conflicts based on strategy
if exists {
switch opts.conflict {
case ConflictStrategySkip:
streams.Infof("Skipping %s %s (already exists)\n", def.GetKind(), def.GetName())
stats.Skipped++
continue
case ConflictStrategyFail:
return errors.Errorf("definition %s %s already exists in namespace %s (use --conflict=overwrite to update)",
def.GetKind(), def.GetName(), opts.namespace)
case ConflictStrategyRename:
// Find a unique name
baseName := def.GetName()
for i := 1; ; i++ {
newName := fmt.Sprintf("%s-%d", baseName, i)
existingDef := pkgdef.Definition{}
existingDef.SetGroupVersionKind(def.GroupVersionKind())
err = k8sClient.Get(ctx, types2.NamespacedName{
Namespace: opts.namespace,
Name: newName,
}, &existingDef)
if errors2.IsNotFound(err) {
def.SetName(newName)
exists = false
break
}
if i > 100 {
return errors.Errorf("failed to find unique name for %s (tried %s-1 to %s-100)", baseName, baseName, baseName)
}
}
case ConflictStrategyOverwrite:
// Will update below
}
}
// Apply the definition
if exists {
// Update existing - preserve resourceVersion for optimistic concurrency
resourceVersion := existingDef.GetResourceVersion()
uid := existingDef.GetUID()
existingDef.Object = def.Object
existingDef.SetResourceVersion(resourceVersion)
existingDef.SetUID(uid)
existingDef.SetNamespace(opts.namespace)
if err = k8sClient.Update(ctx, &existingDef); err != nil {
stats.Failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: %v", def.GetName(), err))
continue
}
streams.Infof("%s %s updated in namespace %s\n", def.GetKind(), def.GetName(), opts.namespace)
stats.Updated++
} else {
// Create new
if err = k8sClient.Create(ctx, &def); err != nil {
stats.Failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: %v", def.GetName(), err))
continue
}
streams.Infof("%s %s created in namespace %s\n", def.GetKind(), def.GetName(), opts.namespace)
stats.Created++
}
}
stats.DefApplyTime = time.Since(defApplyStart)
// Execute post-apply hooks (only if we actually applied something or this is not dry-run)
if !opts.skipHooks && !opts.skipPostApply && module.Metadata.Spec.Hooks.HasPostApply() {
hookExecutor := goloader.NewHookExecutor(k8sClient, module.Path, opts.namespace, opts.dryRun, streams)
hookStats, err := hookExecutor.ExecuteHooks(ctx, "post-apply", module.Metadata.Spec.Hooks.PostApply)
if err != nil {
return errors.Wrap(err, "post-apply hooks failed")
}
stats.PostApplyHookTime = hookStats.TotalDuration
stats.HookResourcesCreated += hookStats.ResourcesCreated
stats.HookResourcesUpdated += hookStats.ResourcesUpdated
stats.OptionalHooksFailed += hookStats.OptionalFailed
for _, detail := range hookStats.HookDetails {
stats.PostApplyHookDetails = append(stats.PostApplyHookDetails, HookTimingDetail{
Name: detail.Name,
Duration: detail.Duration,
Wait: detail.Wait,
})
}
}
// Calculate total time
stats.TotalTime = time.Since(stats.StartTime)
// Print summary - basic stats are always shown
printBasicSummary(streams, stats, failedDefs, opts.dryRun)
// Print detailed stats if requested
if opts.showStats {
printDetailedStats(streams, stats)
}
if stats.Failed > 0 {
return errors.Errorf("%d definitions failed to apply", stats.Failed)
}
return nil
}
// trackDefinitionType increments the counter for the given definition type
func trackDefinitionType(stats *ApplyStats, defType string) {
switch defType {
case "component":
stats.Components++
case "trait":
stats.Traits++
case "policy":
stats.Policies++
case "workflow-step":
stats.WorkflowSteps++
}
}
// printBasicSummary prints a minimal summary that is always shown
func printBasicSummary(streams util.IOStreams, stats *ApplyStats, failedDefs []string, dryRun bool) {
streams.Infof("\n")
// Build a concise one-line summary
var parts []string
if dryRun {
if stats.Created > 0 {
parts = append(parts, fmt.Sprintf("would apply: %d", stats.Created))
}
} else {
if stats.Created > 0 {
parts = append(parts, fmt.Sprintf("created: %d", stats.Created))
}
if stats.Updated > 0 {
parts = append(parts, fmt.Sprintf("updated: %d", stats.Updated))
}
}
if stats.Skipped > 0 {
parts = append(parts, fmt.Sprintf("skipped: %d", stats.Skipped))
}
if stats.PlacementSkipped > 0 {
parts = append(parts, fmt.Sprintf("placement-skipped: %d", stats.PlacementSkipped))
}
if stats.Failed > 0 {
parts = append(parts, fmt.Sprintf("failed: %d", stats.Failed))
}
if dryRun {
if len(parts) > 0 {
streams.Infof("Dry-run complete: %s\n", strings.Join(parts, ", "))
} else {
streams.Infof("Dry-run complete: no definitions to apply\n")
}
} else {
if len(parts) > 0 {
streams.Infof("Complete: %s\n", strings.Join(parts, ", "))
} else {
streams.Infof("Complete: no definitions applied\n")
}
}
// Show failed definitions details
if stats.Failed > 0 {
for _, f := range failedDefs {
streams.Infof(" - %s\n", f)
}
}
}
// printDetailedStats prints detailed statistics when --stats flag is set
func printDetailedStats(streams util.IOStreams, stats *ApplyStats) {
streams.Infof("\n─────────────────────────────────────────\n")
streams.Infof("Detailed Statistics\n")
streams.Infof("─────────────────────────────────────────\n")
// Definition counts by type
hasTypes := stats.Components > 0 || stats.Traits > 0 || stats.Policies > 0 || stats.WorkflowSteps > 0
if hasTypes {
streams.Infof("\nDefinitions by type:\n")
if stats.Components > 0 {
streams.Infof(" Components: %d\n", stats.Components)
}
if stats.Traits > 0 {
streams.Infof(" Traits: %d\n", stats.Traits)
}
if stats.Policies > 0 {
streams.Infof(" Policies: %d\n", stats.Policies)
}
if stats.WorkflowSteps > 0 {
streams.Infof(" Workflow Steps: %d\n", stats.WorkflowSteps)
}
}
// Definition counts by action
streams.Infof("\nDefinitions by action:\n")
streams.Infof(" Created: %d\n", stats.Created)
streams.Infof(" Updated: %d\n", stats.Updated)
streams.Infof(" Skipped: %d\n", stats.Skipped)
streams.Infof(" Failed: %d\n", stats.Failed)
// Placement stats
if stats.PlacementEvaluated > 0 {
streams.Infof("\nPlacement:\n")
streams.Infof(" Evaluated: %d\n", stats.PlacementEvaluated)
streams.Infof(" Eligible: %d\n", stats.PlacementEligible)
streams.Infof(" Skipped: %d\n", stats.PlacementSkipped)
}
// Timing statistics
streams.Infof("\nTiming:\n")
streams.Infof(" Module loading: %s\n", formatDuration(stats.ModuleLoadTime))
if stats.PreApplyHookTime > 0 {
streams.Infof(" Pre-apply hooks: %s\n", formatDuration(stats.PreApplyHookTime))
for _, detail := range stats.PreApplyHookDetails {
suffix := ""
if detail.Wait {
suffix = " (wait)"
}
streams.Infof(" - %s: %s%s\n", detail.Name, formatDuration(detail.Duration), suffix)
}
}
streams.Infof(" Definition apply: %s\n", formatDuration(stats.DefApplyTime))
if stats.PostApplyHookTime > 0 {
streams.Infof(" Post-apply hooks: %s\n", formatDuration(stats.PostApplyHookTime))
for _, detail := range stats.PostApplyHookDetails {
suffix := ""
if detail.Wait {
suffix = " (wait)"
}
streams.Infof(" - %s: %s%s\n", detail.Name, formatDuration(detail.Duration), suffix)
}
}
streams.Infof(" ─────────────────\n")
streams.Infof(" Total: %s\n", formatDuration(stats.TotalTime))
// Throughput
totalApplied := stats.Created + stats.Updated
if totalApplied > 0 && stats.DefApplyTime > 0 {
throughput := float64(totalApplied) / stats.DefApplyTime.Seconds()
streams.Infof(" Throughput: %.1f definitions/sec\n", throughput)
}
// Hook resource stats
if stats.HookResourcesCreated > 0 || stats.HookResourcesUpdated > 0 || stats.OptionalHooksFailed > 0 {
streams.Infof("\nHook resources:\n")
streams.Infof(" Created: %d\n", stats.HookResourcesCreated)
streams.Infof(" Updated: %d\n", stats.HookResourcesUpdated)
if stats.OptionalHooksFailed > 0 {
streams.Infof(" Optional failed: %d\n", stats.OptionalHooksFailed)
}
}
}
// formatDuration formats a duration for display
func formatDuration(d time.Duration) string {
if d < time.Second {
return fmt.Sprintf("%dms", d.Milliseconds())
}
if d < time.Minute {
return fmt.Sprintf("%.1fs", d.Seconds())
}
return fmt.Sprintf("%.1fm", d.Minutes())
}
// NewDefinitionListModuleCommand creates the `vela def list-module` command
// to list definitions in a Go module without applying them
func NewDefinitionListModuleCommand(c common.Args, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "list-module",
Short: "List all definitions in a Go module.",
Long: `List all definitions in a Go module without applying them.
Supports both local paths and remote Go modules:
- Local path: ./my-definitions, /path/to/definitions
- Go module: github.com/myorg/definitions@v1.0.0`,
Example: `# List definitions in a local directory
> vela def list-module ./my-definitions
# List definitions in a remote Go module
> vela def list-module github.com/myorg/definitions@v1.0.0
# List only component definitions
> vela def list-module ./my-definitions --types component`,
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefModule,
types.TagCommandOrder: "3",
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
version, err := cmd.Flags().GetString(FlagModuleVersion)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleVersion)
}
typesStr, err := cmd.Flags().GetString(FlagModuleTypes)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleTypes)
}
var defTypes []string
if typesStr != "" {
defTypes = strings.Split(typesStr, ",")
for i := range defTypes {
defTypes[i] = strings.TrimSpace(defTypes[i])
}
}
return listModule(ctx, streams, args[0], listModuleOptions{
version: version,
types: defTypes,
})
},
}
cmd.Flags().StringP(FlagModuleVersion, "v", "", "Version of the module (for remote modules)")
cmd.Flags().StringP(FlagModuleTypes, "t", "", "Comma-separated list of definition types to list")
return cmd
}
// listModuleOptions contains options for listing module contents
type listModuleOptions struct {
version string
types []string
}
// listModule lists all definitions in a module
func listModule(ctx context.Context, streams util.IOStreams, moduleRef string, opts listModuleOptions) error {
// Load module options
loadOpts := goloader.DefaultModuleLoadOptions()
loadOpts.Version = opts.version
if len(opts.types) > 0 {
loadOpts.Types = opts.types
}
streams.Infof("Loading module from %s...\n", moduleRef)
// Load the module
module, err := goloader.LoadModule(ctx, moduleRef, loadOpts)
if err != nil {
return errors.Wrapf(err, "failed to load module from %s", moduleRef)
}
// Print module info
streams.Infof("\n%s\n", module.Summary())
// Print definitions in table format
if len(module.Definitions) > 0 {
streams.Infof("Definitions:\n")
w := tabwriter.NewWriter(streams.Out, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tTYPE\tFILE\tSTATUS")
for _, def := range module.Definitions {
status := "OK"
if def.Error != nil {
status = fmt.Sprintf("Error: %v", def.Error)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
def.Definition.Name,
def.Definition.Type,
def.Definition.FilePath,
status)
}
if err := w.Flush(); err != nil {
return err
}
}
return nil
}
// NewDefinitionValidateModuleCommand creates the `vela def validate-module` command
// to validate a Go module's definitions
func NewDefinitionValidateModuleCommand(c common.Args, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "validate-module",
Short: "Validate all definitions in a Go module.",
Long: `Validate all definitions in a Go module without applying them.
Checks:
- All Go definition files can be parsed
- All definitions generate valid CUE
- Module metadata is valid (if module.yaml exists)
- Minimum KubeVela version requirements (if specified)`,
Example: `# Validate definitions in a local directory
> vela def validate-module ./my-definitions
# Validate definitions in a remote Go module
> vela def validate-module github.com/myorg/definitions@v1.0.0`,
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefModule,
types.TagCommandOrder: "4",
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
version, err := cmd.Flags().GetString(FlagModuleVersion)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleVersion)
}
return validateModule(ctx, c, streams, args[0], version)
},
}
cmd.Flags().StringP(FlagModuleVersion, "v", "", "Version of the module (for remote modules)")
return cmd
}
// validateModule validates all definitions in a module
func validateModule(ctx context.Context, c common.Args, streams util.IOStreams, moduleRef, version string) error {
// Load module options
loadOpts := goloader.DefaultModuleLoadOptions()
loadOpts.Version = version
streams.Infof("Loading module from %s...\n", moduleRef)
// Load the module
module, err := goloader.LoadModule(ctx, moduleRef, loadOpts)
if err != nil {
return errors.Wrapf(err, "failed to load module from %s", moduleRef)
}
// Get kubernetes config for CUE validation
config, err := c.GetConfig()
if err != nil {
// Continue without config - some validation can still be done
streams.Infof("Warning: Could not get kubernetes config, some validations will be skipped\n")
}
// Collect validation errors
var validationErrors []string
// Validate each definition
for _, result := range module.Definitions {
if result.Error != nil {
validationErrors = append(validationErrors,
fmt.Sprintf("%s: failed to load: %v", result.Definition.FilePath, result.Error))
continue
}
// Validate CUE can be parsed (config can be nil - FromCUEString doesn't use it)
def := pkgdef.Definition{}
if err := def.FromCUEString(result.CUE, config); err != nil {
validationErrors = append(validationErrors,
fmt.Sprintf("%s: invalid CUE: %v", result.Definition.Name, err))
}
}
// Validate module metadata (pass current vela version for minVelaVersion check)
errs := goloader.ValidateModule(module, velaversion.VelaVersion)
for _, e := range errs {
validationErrors = append(validationErrors, e.Error())
}
// Print results
streams.Infof("\n%s\n", module.Summary())
if len(validationErrors) > 0 {
streams.Infof("Validation failed with %d error(s):\n", len(validationErrors))
for _, e := range validationErrors {
streams.Infof(" - %s\n", e)
}
return errors.Errorf("module validation failed with %d errors", len(validationErrors))
}
streams.Infof("Module validation passed.\n")
return nil
}
// FlagModuleName is the flag for module name
const FlagModuleName = "name"
// FlagModuleDesc is the flag for module description
const FlagModuleDesc = "desc"
// FlagGoModule is the flag for Go module path
const FlagGoModule = "go-module"
// FlagWithExamples is the flag to include example definitions
const FlagWithExamples = "with-examples"
// Scaffold flags for creating specific definitions
const (
// FlagComponents is the flag for component names to scaffold
FlagComponents = "components"
// FlagTraits is the flag for trait names to scaffold
FlagTraits = "traits"
// FlagPolicies is the flag for policy names to scaffold
FlagPolicies = "policies"
// FlagWorkflowSteps is the flag for workflow step names to scaffold
FlagWorkflowSteps = "workflowsteps"
)
// NewDefinitionInitModuleCommand creates the `vela def init-module` command
// to initialize a new definition module with proper directory structure
func NewDefinitionInitModuleCommand(_ common.Args, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "init-module",
Short: "Initialize a new definition module.",
Long: `Initialize a new Go-based definition module with proper directory structure.
Creates:
- module.yaml with metadata
- go.mod and go.sum files
- cmd/register/main.go entry point for registry loading
- components/, traits/, policies/, workflowsteps/ directories
- Scaffolded definition files (optional)
- README.md with documentation
This sets up a complete module that can be published as a Go module
and applied using 'vela def apply-module'.`,
Example: `# Initialize a new module - creates 'my-defs' directory automatically
> vela def init-module --name my-defs
# Initialize a module in a specific directory
> vela def init-module ./my-definitions
# Initialize with custom module name and Go module path
> vela def init-module ./my-defs --name my-defs --go-module github.com/myorg/my-defs
# Initialize with example definitions
> vela def init-module --name my-defs --with-examples
# Initialize with scaffolded component definitions
> vela def init-module --name my-defs --components webservice,worker,task
# Initialize with multiple definition types
> vela def init-module --name my-defs --components api,backend --traits autoscaler --policies topology
# Initialize in current directory (if no --name or path given)
> vela def init-module`,
Args: cobra.MaximumNArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefModule,
types.TagCommandOrder: "1",
},
RunE: func(cmd *cobra.Command, args []string) error {
moduleName, err := cmd.Flags().GetString(FlagModuleName)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleName)
}
// Determine target directory:
// 1. If path argument is given, use it
// 2. If --name is given but no path, create directory with that name
// 3. Otherwise, use current directory
var targetDir string
switch {
case len(args) > 0:
targetDir = args[0]
case moduleName != "":
targetDir = moduleName
default:
targetDir = "."
}
moduleDesc, err := cmd.Flags().GetString(FlagModuleDesc)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleDesc)
}
goModule, err := cmd.Flags().GetString(FlagGoModule)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagGoModule)
}
withExamples, err := cmd.Flags().GetBool(FlagWithExamples)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagWithExamples)
}
// Parse scaffold flags
componentsStr, err := cmd.Flags().GetString(FlagComponents)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagComponents)
}
traitsStr, err := cmd.Flags().GetString(FlagTraits)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagTraits)
}
policiesStr, err := cmd.Flags().GetString(FlagPolicies)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagPolicies)
}
workflowStepsStr, err := cmd.Flags().GetString(FlagWorkflowSteps)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagWorkflowSteps)
}
scaffold := scaffoldOptions{
components: parseCommaSeparated(componentsStr),
traits: parseCommaSeparated(traitsStr),
policies: parseCommaSeparated(policiesStr),
workflowsteps: parseCommaSeparated(workflowStepsStr),
}
return initModule(streams, targetDir, initModuleOptions{
name: moduleName,
description: moduleDesc,
goModule: goModule,
withExamples: withExamples,
scaffold: scaffold,
})
},
}
cmd.Flags().StringP(FlagModuleName, "n", "", "Name for the module (defaults to directory name)")
cmd.Flags().StringP(FlagModuleDesc, "d", "", "Description of the module")
cmd.Flags().StringP(FlagGoModule, "g", "", "Go module path (e.g., github.com/myorg/my-defs)")
cmd.Flags().BoolP(FlagWithExamples, "e", false, "Include example definitions")
cmd.Flags().String(FlagComponents, "", "Comma-separated component names to scaffold (e.g., webservice,worker)")
cmd.Flags().String(FlagTraits, "", "Comma-separated trait names to scaffold (e.g., autoscaler,ingress)")
cmd.Flags().String(FlagPolicies, "", "Comma-separated policy names to scaffold (e.g., topology,override)")
cmd.Flags().String(FlagWorkflowSteps, "", "Comma-separated workflow step names to scaffold (e.g., deploy,notify)")
return cmd
}
// parseCommaSeparated splits a comma-separated string into a slice of trimmed strings
func parseCommaSeparated(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
return result
}
// scaffoldOptions contains names of definitions to scaffold
type scaffoldOptions struct {
components []string
traits []string
policies []string
workflowsteps []string
}
// hasAny returns true if any scaffold options are specified
func (s scaffoldOptions) hasAny() bool {
return len(s.components) > 0 || len(s.traits) > 0 || len(s.policies) > 0 || len(s.workflowsteps) > 0
}
// initModuleOptions contains options for module initialization
type initModuleOptions struct {
name string
description string
goModule string
withExamples bool
scaffold scaffoldOptions
}
// initModule initializes a new definition module
func initModule(streams util.IOStreams, targetDir string, opts initModuleOptions) error {
// Create target directory if it doesn't exist
absPath, err := filepath.Abs(targetDir)
if err != nil {
return errors.Wrapf(err, "failed to resolve path %s", targetDir)
}
if err := os.MkdirAll(absPath, 0755); err != nil { //nolint:gosec // G301: Standard permissions for user-accessible module directory
return errors.Wrapf(err, "failed to create directory %s", absPath)
}
// Derive module name from directory if not specified
if opts.name == "" {
opts.name = filepath.Base(absPath)
}
// Derive Go module path if not specified
if opts.goModule == "" {
opts.goModule = "github.com/myorg/" + opts.name
}
streams.Infof("Initializing definition module in %s...\n", absPath)
// Create directory structure
dirs := []string{
"components",
"traits",
"policies",
"workflowsteps",
"cmd/register",
}
for _, dir := range dirs {
dirPath := filepath.Join(absPath, dir)
if err := os.MkdirAll(dirPath, 0755); err != nil { //nolint:gosec // G301: Standard permissions for module subdirectories
return errors.Wrapf(err, "failed to create directory %s", dirPath)
}
streams.Infof(" Created %s/\n", dir)
}
// Create doc.go files for each definition package
// This ensures go mod tidy works correctly by making each directory a valid Go package
packageDocs := map[string]string{
"components": `// Package components contains KubeVela ComponentDefinition implementations.
// Each component defines a workload type that can be used in Applications.
//
// To create a new component:
//
// func init() {
// defkit.Register(MyComponent())
// }
//
// func MyComponent() *defkit.ComponentDefinition {
// return defkit.NewComponent("my-component").
// Description("My component description").
// Workload("apps/v1", "Deployment").
// // ... configuration
// }
package components
`,
"traits": `// Package traits contains KubeVela TraitDefinition implementations.
// Traits modify or enhance components with additional capabilities.
//
// To create a new trait:
//
// func init() {
// defkit.Register(MyTrait())
// }
//
// func MyTrait() *defkit.TraitDefinition {
// return defkit.NewTrait("my-trait").
// Description("My trait description").
// AppliesToWorkloads("deployments.apps").
// // ... configuration
// }
package traits
`,
"policies": `// Package policies contains KubeVela PolicyDefinition implementations.
// Policies define application-level behaviors and constraints.
//
// To create a new policy:
//
// func init() {
// defkit.Register(MyPolicy())
// }
//
// func MyPolicy() *defkit.PolicyDefinition {
// return defkit.NewPolicy("my-policy").
// Description("My policy description").
// // ... configuration
// }
package policies
`,
"workflowsteps": `// Package workflowsteps contains KubeVela WorkflowStepDefinition implementations.
// Workflow steps define actions that can be executed in application workflows.
//
// To create a new workflow step:
//
// func init() {
// defkit.Register(MyStep())
// }
//
// func MyStep() *defkit.WorkflowStepDefinition {
// return defkit.NewWorkflowStep("my-step").
// Description("My workflow step description").
// // ... configuration
// }
package workflowsteps
`,
}
for pkg, doc := range packageDocs {
docPath := filepath.Join(absPath, pkg, "doc.go")
if err := os.WriteFile(docPath, []byte(doc), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write %s/doc.go", pkg)
}
}
// Create module.yaml
moduleYAML := generateModuleYAML(opts)
if err := os.WriteFile(filepath.Join(absPath, "module.yaml"), []byte(moduleYAML), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write module.yaml")
}
streams.Infof(" Created module.yaml\n")
// Create go.mod
goMod := generateGoMod(opts)
if err := os.WriteFile(filepath.Join(absPath, "go.mod"), []byte(goMod), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write go.mod")
}
streams.Infof(" Created go.mod\n")
// Create cmd/register/main.go - the registry entry point
registerMain := generateRegisterMain(opts)
if err := os.WriteFile(filepath.Join(absPath, "cmd/register/main.go"), []byte(registerMain), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write cmd/register/main.go")
}
streams.Infof(" Created cmd/register/main.go\n")
// Create README.md
readme := generateModuleReadme(opts)
if err := os.WriteFile(filepath.Join(absPath, "README.md"), []byte(readme), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write README.md")
}
streams.Infof(" Created README.md\n")
// Create scaffolded definitions if specified
if opts.scaffold.hasAny() {
if err := createScaffoldedDefinitions(streams, absPath, opts); err != nil {
return errors.Wrap(err, "failed to create scaffolded definitions")
}
}
// Create example definitions if requested
if opts.withExamples {
if err := createExampleDefinitions(streams, absPath, opts); err != nil {
return errors.Wrap(err, "failed to create example definitions")
}
}
// Create .gitignore
gitignore := `# Binaries
*.exe
*.dll
*.so
*.dylib
# Test binary
*.test
# Output
*.out
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
`
if err := os.WriteFile(filepath.Join(absPath, ".gitignore"), []byte(gitignore), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write .gitignore")
}
streams.Infof(" Created .gitignore\n")
streams.Infof("\nModule initialized successfully!\n\n")
streams.Infof("Next steps:\n")
streams.Infof(" 1. Add your definitions to the appropriate directories\n")
streams.Infof(" 2. Run 'go mod tidy' to download dependencies\n")
streams.Infof(" 3. Test locally: vela def apply-module %s --dry-run\n", targetDir)
streams.Infof(" 4. Validate: vela def validate-module %s\n", targetDir)
streams.Infof(" 5. List definitions: vela def list-module %s\n", targetDir)
return nil
}
// generateRegisterMain generates the cmd/register/main.go content
func generateRegisterMain(opts initModuleOptions) string {
return fmt.Sprintf(`/*
Copyright 2025 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 main is the entry point for the definition registry.
// It outputs all registered definitions as JSON for CLI consumption.
//
// Usage: go run ./cmd/register
//
// Each definition package (components, traits, policies, workflowsteps)
// registers its definitions via init() functions that call defkit.Register().
// Importing those packages triggers registration automatically.
package main
import (
"fmt"
"os"
"github.com/oam-dev/kubevela/pkg/definition/defkit"
// Import packages to trigger init() registration
_ "%s/components"
_ "%s/traits"
_ "%s/policies"
_ "%s/workflowsteps"
)
func main() {
output, err := defkit.ToJSON()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to serialize registry: %%v\n", err)
os.Exit(1)
}
fmt.Print(string(output))
}
`, opts.goModule, opts.goModule, opts.goModule, opts.goModule)
}
// createScaffoldedDefinitions creates scaffolded definition files
func createScaffoldedDefinitions(streams util.IOStreams, basePath string, opts initModuleOptions) error {
// Create component scaffolds
for _, name := range opts.scaffold.components {
content := generateComponentScaffold(name)
filename := toSnakeCase(name) + ".go"
if err := os.WriteFile(filepath.Join(basePath, "components", filename), []byte(content), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write components/%s", filename)
}
streams.Infof(" Created components/%s\n", filename)
}
// Create trait scaffolds
for _, name := range opts.scaffold.traits {
content := generateTraitScaffold(name)
filename := toSnakeCase(name) + ".go"
if err := os.WriteFile(filepath.Join(basePath, "traits", filename), []byte(content), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write traits/%s", filename)
}
streams.Infof(" Created traits/%s\n", filename)
}
// Create policy scaffolds
for _, name := range opts.scaffold.policies {
content := generatePolicyScaffold(name)
filename := toSnakeCase(name) + ".go"
if err := os.WriteFile(filepath.Join(basePath, "policies", filename), []byte(content), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write policies/%s", filename)
}
streams.Infof(" Created policies/%s\n", filename)
}
// Create workflowstep scaffolds
for _, name := range opts.scaffold.workflowsteps {
content := generateWorkflowStepScaffold(name)
filename := toSnakeCase(name) + ".go"
if err := os.WriteFile(filepath.Join(basePath, "workflowsteps", filename), []byte(content), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrapf(err, "failed to write workflowsteps/%s", filename)
}
streams.Infof(" Created workflowsteps/%s\n", filename)
}
return nil
}
// toSnakeCase converts a name to snake_case for filenames
func toSnakeCase(name string) string {
// Simple conversion: replace hyphens with underscores, lowercase
result := strings.ToLower(name)
result = strings.ReplaceAll(result, "-", "_")
return result
}
// toCamelCase converts a name to CamelCase for Go exported function names
func toCamelCase(name string) string {
parts := strings.FieldsFunc(name, func(r rune) bool {
return r == '-' || r == '_'
})
for i, part := range parts {
if len(part) > 0 {
parts[i] = strings.ToUpper(part[:1]) + strings.ToLower(part[1:])
}
}
return strings.Join(parts, "")
}
// toLowerCamelCase converts a name to lowerCamelCase for Go unexported function names
func toLowerCamelCase(name string) string {
camel := toCamelCase(name)
if len(camel) == 0 {
return camel
}
return strings.ToLower(camel[:1]) + camel[1:]
}
// generateComponentScaffold generates a component definition scaffold
func generateComponentScaffold(name string) string {
funcName := toCamelCase(name)
return fmt.Sprintf(`/*
Copyright 2025 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 components
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
func init() {
defkit.Register(%s())
}
// %s creates the %s component definition.
func %s() *defkit.ComponentDefinition {
// Define parameters using the defkit fluent API
// TODO: Add your component parameters here
return defkit.NewComponent("%s").
Description("TODO: Add description for %s component").
Workload("apps/v1", "Deployment").
// TODO: Add parameters and template
Template(%sTemplate)
}
// %sTemplate defines the template function for %s.
func %sTemplate(tpl *defkit.Template) {
// TODO: Implement component output logic
// Example:
// image := defkit.String("image")
// replicas := defkit.Int("replicas")
// deployment := defkit.NewResource("apps/v1", "Deployment").
// Set("spec.replicas", replicas).
// Set("spec.template.spec.containers[0].image", image)
// tpl.Output(deployment)
}
`, funcName, funcName, name, funcName, name, name, toLowerCamelCase(name), toLowerCamelCase(name), name, toLowerCamelCase(name))
}
// generateTraitScaffold generates a trait definition scaffold
func generateTraitScaffold(name string) string {
funcName := toCamelCase(name)
return fmt.Sprintf(`/*
Copyright 2025 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 traits
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
func init() {
defkit.Register(%s())
}
// %s creates the %s trait definition.
func %s() *defkit.TraitDefinition {
// Define parameters using the defkit fluent API
// TODO: Add your trait parameters here
return defkit.NewTrait("%s").
Description("TODO: Add description for %s trait").
AppliesTo("deployments.apps", "statefulsets.apps").
// TODO: Add parameters and template
Template(%sTemplate)
}
// %sTemplate defines the template function for %s.
func %sTemplate(tpl *defkit.Template) {
// TODO: Implement trait patch logic
// Example:
// labels := defkit.Object("labels")
// patch := defkit.NewPatch().
// SpreadIf(labels.IsSet(), "spec.template.metadata.labels", labels)
// tpl.PatchOutput(patch)
}
`, funcName, funcName, name, funcName, name, name, toLowerCamelCase(name), toLowerCamelCase(name), name, toLowerCamelCase(name))
}
// generatePolicyScaffold generates a policy definition scaffold
func generatePolicyScaffold(name string) string {
funcName := toCamelCase(name)
return fmt.Sprintf(`/*
Copyright 2025 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 policies
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
func init() {
defkit.Register(%s())
}
// %s creates the %s policy definition.
func %s() *defkit.PolicyDefinition {
// Define parameters using the defkit fluent API
// TODO: Add your policy parameters here
return defkit.NewPolicy("%s").
Description("TODO: Add description for %s policy")
// TODO: Add parameters and implementation
}
`, funcName, funcName, name, funcName, name, name)
}
// generateWorkflowStepScaffold generates a workflow step definition scaffold
func generateWorkflowStepScaffold(name string) string {
funcName := toCamelCase(name)
return fmt.Sprintf(`/*
Copyright 2025 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 workflowsteps
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
func init() {
defkit.Register(%s())
}
// %s creates the %s workflow step definition.
func %s() *defkit.WorkflowStepDefinition {
// Define parameters using the defkit fluent API
// TODO: Add your workflow step parameters here
return defkit.NewWorkflowStep("%s").
Description("TODO: Add description for %s workflow step")
// TODO: Add parameters and implementation
}
`, funcName, funcName, name, funcName, name, name)
}
// generateModuleYAML generates the module.yaml content
func generateModuleYAML(opts initModuleOptions) string {
desc := opts.description
if desc == "" {
desc = "KubeVela definition module"
}
return fmt.Sprintf(`# KubeVela Definition Module
# This file contains metadata about your definition module.
# See: https://kubevela.io/docs/platform-engineers/definition-module
#
# Note: Version is automatically derived from git tags.
# Use 'git tag v1.0.0' to set the version.
apiVersion: core.oam.dev/v1beta1
kind: DefinitionModule
metadata:
name: %s
spec:
description: %s
maintainers:
- name: Your Name
email: your.email@example.com
# Minimum KubeVela version required (optional)
# minVelaVersion: v1.9.0
# Categories for organization (optional)
categories:
- custom
# Dependencies on other modules (optional)
# dependencies:
# - module: github.com/other/module
# version: v1.0.0
# Placement constraints for cluster-aware deployment (optional)
# Definitions in this module will only be applied to clusters matching these conditions.
# See: https://kubevela.io/docs/platform-engineers/definition-placement
# placement:
# runOn:
# - key: provider
# operator: Eq
# values: ["aws"]
# - key: environment
# operator: In
# values: ["production", "staging"]
# notRunOn:
# - key: cluster-type
# operator: Eq
# values: ["vcluster"]
`, opts.name, desc)
}
// generateGoMod generates the go.mod content
func generateGoMod(opts initModuleOptions) string {
return fmt.Sprintf(`module %s
go 1.23.8
require github.com/oam-dev/kubevela v1.9.0
`, opts.goModule)
}
// generateModuleReadme generates the README.md content
func generateModuleReadme(opts initModuleOptions) string {
desc := opts.description
if desc == "" {
desc = "A collection of KubeVela X-Definitions."
}
return fmt.Sprintf(`# %s
%s
## Overview
This module contains Go-based KubeVela X-Definitions that can be applied to any KubeVela cluster.
## Directory Structure
- **components/** - ComponentDefinitions for workload types
- **traits/** - TraitDefinitions for operational behaviors
- **policies/** - PolicyDefinitions for application policies
- **workflowsteps/** - WorkflowStepDefinitions for delivery workflows
## Usage
### Apply all definitions
`+"```bash"+`
vela def apply-module %s
`+"```"+`
### List definitions
`+"```bash"+`
vela def list-module %s
`+"```"+`
### Validate definitions
`+"```bash"+`
vela def validate-module %s
`+"```"+`
### Apply with namespace
`+"```bash"+`
vela def apply-module %s --namespace my-namespace
`+"```"+`
### Dry-run (preview without applying)
`+"```bash"+`
vela def apply-module %s --dry-run
`+"```"+`
## Adding New Definitions
1. Create a new Go file in the appropriate directory
2. Add an init() function that registers your definition
3. Use the defkit package fluent API to define your component/trait/policy/workflow-step
4. Run `+"`go mod tidy`"+` to update dependencies
5. Validate with `+"`vela def validate-module .`"+`
Example component definition:
`+"```go"+`
package components
import "github.com/oam-dev/kubevela/pkg/definition/defkit"
func init() {
defkit.Register(MyComponent())
}
func MyComponent() *defkit.ComponentDefinition {
image := defkit.String("image").Required().Description("Container image")
replicas := defkit.Int("replicas").Default(1).Description("Number of replicas")
return defkit.NewComponent("my-component").
Description("My custom component").
Workload("apps/v1", "Deployment").
Params(image, replicas).
Template(myComponentTemplate)
}
func myComponentTemplate(tpl *defkit.Template) {
vela := defkit.VelaCtx()
image := defkit.String("image")
replicas := defkit.Int("replicas")
deployment := defkit.NewResource("apps/v1", "Deployment").
Set("spec.replicas", replicas).
Set("spec.selector.matchLabels[app.oam.dev/component]", vela.Name()).
Set("spec.template.spec.containers[0].name", vela.Name()).
Set("spec.template.spec.containers[0].image", image)
tpl.Output(deployment)
}
`+"```"+`
## Version
Version is automatically derived from git tags. Use semantic versioning tags (e.g., v1.0.0) to set the module version.
## License
Apache License 2.0
`, opts.name, desc, opts.goModule, opts.goModule, opts.goModule, opts.goModule, opts.goModule)
}
// createExampleDefinitions creates example definition files
// opts is kept for future use to customize example definitions based on module options
func createExampleDefinitions(streams util.IOStreams, basePath string, _ initModuleOptions) error {
// Example component - using correct defkit API
componentExample := `/*
Copyright 2025 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 components contains component definitions.
// This demonstrates how to create a simple component definition using the defkit package.
package components
import "github.com/oam-dev/kubevela/pkg/definition/defkit"
func init() {
defkit.Register(ExampleComponent())
}
// ExampleComponent creates a simple container-based component.
func ExampleComponent() *defkit.ComponentDefinition {
// Define parameters using the defkit fluent API
image := defkit.String("image").Required().Description("Container image to run")
port := defkit.Int("port").Default(80).Description("Container port")
replicas := defkit.Int("replicas").Default(1).Description("Number of replicas")
return defkit.NewComponent("example-component").
Description("An example component definition").
Workload("apps/v1", "Deployment").
CustomStatus(defkit.DeploymentStatus().Build()).
HealthPolicy(defkit.DeploymentHealth().Build()).
Params(image, port, replicas).
Template(exampleComponentTemplate)
}
// exampleComponentTemplate defines the template function for example-component.
func exampleComponentTemplate(tpl *defkit.Template) {
vela := defkit.VelaCtx()
image := defkit.String("image")
port := defkit.Int("port")
replicas := defkit.Int("replicas")
// Primary output: Deployment
deployment := defkit.NewResource("apps/v1", "Deployment").
Set("spec.replicas", replicas).
Set("spec.selector.matchLabels[app.oam.dev/component]", vela.Name()).
Set("spec.template.metadata.labels[app.oam.dev/name]", vela.AppName()).
Set("spec.template.metadata.labels[app.oam.dev/component]", vela.Name()).
Set("spec.template.spec.containers[0].name", vela.Name()).
Set("spec.template.spec.containers[0].image", image).
Set("spec.template.spec.containers[0].ports[0].containerPort", port)
tpl.Output(deployment)
}
`
if err := os.WriteFile(filepath.Join(basePath, "components", "example.go"), []byte(componentExample), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrap(err, "failed to write example component")
}
streams.Infof(" Created components/example.go\n")
// Example trait - using correct defkit API
traitExample := `/*
Copyright 2025 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 traits contains trait definitions.
// This demonstrates how to create a simple trait definition using the defkit package.
package traits
import "github.com/oam-dev/kubevela/pkg/definition/defkit"
func init() {
defkit.Register(ExampleLabels())
}
// ExampleLabels creates a simple labels trait.
func ExampleLabels() *defkit.TraitDefinition {
// Define parameters using the defkit fluent API
labels := defkit.StringKeyMap("labels").Description("Labels to add to the workload")
return defkit.NewTrait("example-labels").
Description("Adds custom labels to the workload").
AppliesTo("deployments.apps", "statefulsets.apps").
Params(labels).
Template(exampleLabelsTemplate)
}
// exampleLabelsTemplate defines the template function for example-labels.
func exampleLabelsTemplate(tpl *defkit.Template) {
labels := defkit.Object("labels")
// Patch output: add labels to pod template
patch := defkit.NewPatch().
SpreadIf(labels.IsSet(), "spec.template.metadata.labels", labels)
tpl.PatchOutput(patch)
}
`
if err := os.WriteFile(filepath.Join(basePath, "traits", "example.go"), []byte(traitExample), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
return errors.Wrap(err, "failed to write example trait")
}
streams.Infof(" Created traits/example.go\n")
return nil
}
// FlagOutputDir is the flag for output directory
const FlagOutputDir = "output"
// NewDefinitionGenModuleCommand creates the `vela def gen-module` command
// to generate CUE code from Go definitions in a module
func NewDefinitionGenModuleCommand(_ common.Args, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "gen-module",
Short: "Generate CUE code from Go definitions in a module.",
Long: `Generate CUE code from all Go definitions in a module.
This command loads a Go definition module, compiles all definitions to CUE,
and writes the generated CUE files to an output directory organized by type.
Output structure:
<output>/
components/
webservice.cue
worker.cue
traits/
scaler.cue
policies/
topology.cue
workflowsteps/
deploy.cue`,
Example: `# Generate CUE from a local module (output to ./cue-generated)
> vela def gen-module ./my-definitions
# Generate CUE to a specific output directory
> vela def gen-module ./my-definitions -o ./generated-cue
# Generate only specific definition types
> vela def gen-module ./my-definitions -o ./output --types component,trait
# Generate from a remote Go module
> vela def gen-module github.com/myorg/definitions@v1.0.0 -o ./output`,
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefModule,
types.TagCommandOrder: "5",
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
outputDir, err := cmd.Flags().GetString(FlagOutputDir)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagOutputDir)
}
version, err := cmd.Flags().GetString(FlagModuleVersion)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleVersion)
}
typesStr, err := cmd.Flags().GetString(FlagModuleTypes)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagModuleTypes)
}
var defTypes []string
if typesStr != "" {
defTypes = strings.Split(typesStr, ",")
for i := range defTypes {
defTypes[i] = strings.TrimSpace(defTypes[i])
}
}
return genModule(ctx, streams, args[0], genModuleOptions{
outputDir: outputDir,
version: version,
types: defTypes,
})
},
}
cmd.Flags().StringP(FlagOutputDir, "o", "cue-generated", "Output directory for generated CUE files")
cmd.Flags().StringP(FlagModuleVersion, "v", "", "Version of the module (for remote modules)")
cmd.Flags().StringP(FlagModuleTypes, "t", "", "Comma-separated list of definition types to generate (component,trait,policy,workflow-step)")
return cmd
}
// genModuleOptions contains options for generating CUE from a module
type genModuleOptions struct {
outputDir string
version string
types []string
}
// genModule loads a module and generates CUE files for all definitions
func genModule(ctx context.Context, streams util.IOStreams, moduleRef string, opts genModuleOptions) error {
// Load module options
loadOpts := goloader.ModuleLoadOptions{
Version: opts.version,
Types: opts.types,
ResolveDependencies: true,
}
if len(opts.types) == 0 {
loadOpts = goloader.DefaultModuleLoadOptions()
loadOpts.Version = opts.version
}
streams.Infof("Loading module from %s...\n", moduleRef)
// Load the module
module, err := goloader.LoadModule(ctx, moduleRef, loadOpts)
if err != nil {
return errors.Wrapf(err, "failed to load module from %s", moduleRef)
}
// Print module summary
streams.Infof("\n%s\n", module.Summary())
if len(module.Definitions) == 0 {
streams.Infof("No definitions found in module.\n")
return nil
}
// Resolve output directory
absOutputDir, err := filepath.Abs(opts.outputDir)
if err != nil {
return errors.Wrapf(err, "failed to resolve output path %s", opts.outputDir)
}
// Create output directory structure
typeDirs := map[string]string{
"component": "components",
"trait": "traits",
"policy": "policies",
"workflow-step": "workflowsteps",
}
for _, dir := range typeDirs {
dirPath := filepath.Join(absOutputDir, dir)
if err := os.MkdirAll(dirPath, 0755); err != nil { //nolint:gosec // G301: Standard permissions for module subdirectories
return errors.Wrapf(err, "failed to create directory %s", dirPath)
}
}
streams.Infof("Generating CUE files to %s...\n\n", absOutputDir)
// Track results
var generated, failed int
var failedDefs []string
// Generate CUE for each definition
for _, result := range module.Definitions {
if result.Error != nil {
failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: %v", result.Definition.FilePath, result.Error))
continue
}
// Determine output directory based on definition type
typeDir, ok := typeDirs[result.Definition.Type]
if !ok {
typeDir = result.Definition.Type + "s" // fallback
}
// Generate filename from definition name
filename := result.Definition.Name + ".cue"
outputPath := filepath.Join(absOutputDir, typeDir, filename)
// Write CUE content
if err := os.WriteFile(outputPath, []byte(result.CUE), 0644); err != nil { //nolint:gosec // G306: Standard permissions for source files
failed++
failedDefs = append(failedDefs, fmt.Sprintf("%s: failed to write: %v", result.Definition.Name, err))
continue
}
streams.Infof(" Generated %s/%s\n", typeDir, filename)
generated++
}
// Print summary
streams.Infof("\nGeneration complete:\n")
streams.Infof(" Generated: %d\n", generated)
if failed > 0 {
streams.Infof(" Failed: %d\n", failed)
for _, f := range failedDefs {
streams.Infof(" - %s\n", f)
}
return errors.Errorf("%d definitions failed to generate", failed)
}
streams.Infof("\nOutput directory: %s\n", absOutputDir)
return nil
}