Files
kubevela/references/cli/def.go
2026-04-16 10:25:28 -07:00

2113 lines
79 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 cli
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/encoding/gocode/gocodec"
"github.com/kubevela/pkg/util/slices"
"github.com/kubevela/workflow/pkg/cue/model/sets"
crossplane "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.yaml.in/yaml/v3"
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/apimachinery/pkg/runtime/serializer/json"
types2 "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
commontype "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/cue/upgrade"
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/definition/gen_sdk"
"github.com/oam-dev/kubevela/pkg/definition/goloader"
"github.com/oam-dev/kubevela/pkg/utils"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/filters"
"github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/cuegen"
providergen "github.com/oam-dev/kubevela/references/cuegen/generators/provider"
"github.com/oam-dev/kubevela/references/docgen"
)
const (
// HelmChartNamespacePlaceholder is used as a placeholder for rendering definitions into helm chart format
HelmChartNamespacePlaceholder = "###HELM_NAMESPACE###"
// HelmChartFormatEnvName is the name of the environment variable to enable render helm chart format YAML
HelmChartFormatEnvName = "AS_HELM_CHART"
)
// DefinitionCommandGroup create the command group for `vela def` command to manage definitions
func DefinitionCommandGroup(c common.Args, order string, ioStreams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "def",
Short: "Manage definitions.",
Long: "Manage X-Definitions for extension.",
Annotations: map[string]string{
types.TagCommandOrder: order,
types.TagCommandType: types.TypeExtension,
},
}
cmd.SetOut(ioStreams.Out)
cmd.AddCommand(
NewDefinitionGetCommand(c),
NewDefinitionListCommand(c),
NewDefinitionEditCommand(c),
NewDefinitionRenderCommand(c),
NewDefinitionApplyCommand(c, ioStreams),
NewDefinitionDelCommand(c),
NewDefinitionInitCommand(c),
NewDefinitionValidateCommand(c),
NewDefinitionUpgradeCommand(c, ioStreams),
NewDefinitionDocGenCommand(c, ioStreams),
NewCapabilityShowCommand(c, "", ioStreams),
NewDefinitionGenAPICommand(c),
NewDefinitionGenCUECommand(c, ioStreams),
NewDefinitionGenDocCommand(c, ioStreams),
// Module commands for Go definition modules
NewDefinitionInitModuleCommand(c, ioStreams),
NewDefinitionApplyModuleCommand(c, ioStreams),
NewDefinitionListModuleCommand(c, ioStreams),
NewDefinitionValidateModuleCommand(c, ioStreams),
NewDefinitionGenModuleCommand(c, ioStreams),
)
// Set custom help function for grouped output
cmd.SetHelpFunc(defHelpFunc)
return cmd
}
// defHelpFunc provides grouped help output for the def command
func defHelpFunc(cmd *cobra.Command, args []string) {
// If this is being called for a subcommand (not the root "def" command),
// use the standard cobra help output
if cmd.Name() != "def" {
// Use standard help template for subcommands
cmd.Println(cmd.Long)
if cmd.Example != "" {
cmd.Println()
cmd.Println("Examples:")
cmd.Println(cmd.Example)
}
cmd.Println()
cmd.Println("Usage:")
cmd.Printf(" %s\n", cmd.UseLine())
cmd.Println()
localFlags := cmd.LocalFlags().FlagUsages()
if localFlags != "" {
cmd.Println("Flags:")
cmd.Print(localFlags)
}
inheritedFlags := cmd.InheritedFlags().FlagUsages()
if inheritedFlags != "" {
cmd.Println()
cmd.Println("Global Flags:")
cmd.Print(inheritedFlags)
}
return
}
if len(args) > 0 {
// If args provided, find and show help for subcommand
foundCmd, _, err := cmd.Find(args)
if foundCmd != nil && err == nil {
_ = foundCmd.Help()
return
}
}
cmd.Println(cmd.Long)
cmd.Println()
cmd.Println("Usage:")
cmd.Printf(" %s [command]\n", cmd.UseLine())
cmd.Println()
// Print commands grouped by type
for _, t := range []string{types.TypeDefManagement, types.TypeDefGeneration, types.TypeDefModule} {
PrintDefHelpByTag(cmd, cmd.Commands(), t)
}
cmd.Println("Flags:")
cmd.Println(" -h, --help help for def")
cmd.Println()
cmd.Println("Global Flags:")
cmd.Println(" -V, --verbosity Level number for the log level verbosity")
cmd.Println(" -y, --yes Assume yes for all user prompts")
cmd.Println()
cmd.Printf("Use \"%s [command] --help\" for more information about a command.\n", cmd.CommandPath())
}
// PrintDefHelpByTag prints commands grouped by their TagCommandType annotation
func PrintDefHelpByTag(cmd *cobra.Command, all []*cobra.Command, tag string) {
table := newUITable()
table.MaxColWidth = 80
var pl []Printable
for _, c := range all {
if c.Hidden || c.IsAdditionalHelpTopicCommand() {
continue
}
if val, ok := c.Annotations[types.TagCommandType]; ok && val == tag {
pl = append(pl, NewPrintable(c, c.Short))
}
}
if len(pl) == 0 {
return
}
slices.Sort(pl, func(i, j Printable) bool { return i.Order < j.Order })
cmd.Println(tag + ":")
for _, v := range pl {
table.AddRow(fmt.Sprintf(" %-18s", v.Use), v.Desc)
}
cmd.Println(table.String())
cmd.Println()
}
func getPrompt(cmd *cobra.Command, reader *bufio.Reader, description string, prompt string, validate func(string) error) (string, error) {
cmd.Printf("%s", description)
for {
cmd.Printf("%s", prompt)
resp, err := reader.ReadString('\n')
resp = strings.TrimSpace(resp)
if err != nil {
return "", errors.Wrapf(err, "failed to read user response")
}
if validate == nil {
return resp, nil
}
err = validate(resp)
if err != nil {
cmd.Println(err)
} else {
return resp, nil
}
}
}
// nolint:staticcheck
func buildTemplateFromYAML(templateYAML string, def *pkgdef.Definition) error {
templateYAMLBytes, err := utils.ReadRemoteOrLocalPath(templateYAML, false)
if err != nil {
return errors.Wrapf(err, "failed to get template YAML file %s", templateYAML)
}
yamlStrings := regexp.MustCompile(`\n---[^\n]*\n`).Split(string(templateYAMLBytes), -1)
templateObject := map[string]interface{}{
process.OutputFieldName: map[string]interface{}{},
process.OutputsFieldName: map[string]interface{}{},
process.ParameterFieldName: map[string]interface{}{},
}
kind := def.GetKind()
for index, yamlString := range yamlStrings {
var yamlObject map[string]interface{}
if err = yaml.Unmarshal([]byte(yamlString), &yamlObject); err != nil {
return errors.Wrapf(err, "failed to unmarshal template yaml file")
}
if index == 0 && kind != v1beta1.TraitDefinitionKind {
templateObject[process.OutputFieldName] = yamlObject
} else {
name, _, _ := unstructured.NestedString(yamlObject, "metadata", "name")
if name == "" {
name = fmt.Sprintf("output-%d", index)
}
templateObject[process.OutputsFieldName].(map[string]interface{})[name] = yamlObject
}
}
codec := gocodec.New(cuecontext.New(), &gocodec.Config{})
val, err := codec.Decode(templateObject)
if err != nil {
return errors.Wrapf(err, "failed to decode template into cue")
}
templateString, err := sets.ToString(val)
if err != nil {
return errors.Wrapf(err, "failed to encode template cue string")
}
err = unstructured.SetNestedField(def.Object, templateString, pkgdef.DefinitionTemplateKeys...)
if err != nil {
return errors.Wrapf(err, "failed to merge template cue string")
}
return nil
}
// FlagLang is the flag for specifying the language of a definition
const FlagLang = "lang"
// NewDefinitionInitCommand create the `vela def init` command to help user initialize a definition locally
func NewDefinitionInitCommand(_ common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "init DEF_NAME",
Short: "Init a new definition",
Long: "Init a new definition with given arguments or interactively\n* We support parsing a single YAML file (like kubernetes objects) into the cue-style template. \n" +
"However, we do not support variables in YAML file currently, which prevents users from directly feeding files like helm chart directly. \n" +
"We may introduce such features in the future.\n\n" +
"Use --lang go to generate a Go-based definition using the defkit package.",
Example: "# Command below initiate an empty TraitDefinition named my-ingress\n" +
"> vela def init my-ingress -t trait --desc \"My ingress trait definition.\" > ./my-ingress.cue\n" +
"# Command below initiate a definition named my-def interactively and save it to ./my-def.cue\n" +
"> vela def init my-def -i --output ./my-def.cue\n" +
"# Command below initiate a ComponentDefinition named my-webservice with the template parsed from ./template.yaml.\n" +
"> vela def init my-webservice -i --template-yaml ./template.yaml\n" +
"# Command below initiate a Go-based ComponentDefinition\n" +
"> vela def init my-webservice -t component --lang go -o ./my-webservice.go\n" +
"# Initiate a Terraform ComponentDefinition named vswitch from Github for Alibaba Cloud.\n" +
"> vela def init vswitch --type component --provider alibaba --desc xxx --git https://github.com/kubevela-contrib/terraform-modules.git --path alibaba/vswitch\n" +
"# Initiate a Terraform ComponentDefinition named redis from local file for AWS.\n" +
"> vela def init redis --type component --provider aws --desc \"Terraform configuration for AWS Redis\" --local redis.tf",
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "1",
},
RunE: func(cmd *cobra.Command, args []string) error {
var defStr string
definitionType, err := cmd.Flags().GetString(FlagType)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagType)
}
alias, err := cmd.Flags().GetString(FlagAlias)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagAlias)
}
desc, err := cmd.Flags().GetString(FlagDescription)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagDescription)
}
templateYAML, err := cmd.Flags().GetString(FlagTemplateYAML)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagTemplateYAML)
}
output, err := cmd.Flags().GetString(FlagOutput)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagOutput)
}
interactive, err := cmd.Flags().GetBool(FlagInteractive)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagInteractive)
}
lang, err := cmd.Flags().GetString(FlagLang)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagLang)
}
if interactive {
reader := bufio.NewReader(cmd.InOrStdin())
if definitionType == "" {
if definitionType, err = getPrompt(cmd, reader, "Please choose one definition type from the following values: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", ")+"\n", "> Definition type: ", func(resp string) error {
if _, ok := pkgdef.DefinitionTypeToKind[resp]; !ok {
return errors.New("invalid definition type")
}
return nil
}); err != nil {
return err
}
}
if desc == "" {
if desc, err = getPrompt(cmd, reader, "", "> Definition description: ", nil); err != nil {
return err
}
}
if templateYAML == "" && lang != "go" {
if templateYAML, err = getPrompt(cmd, reader, "Please enter the location the template YAML file to build definition. Leave it empty to generate default template.\n", "> Definition template filename: ", func(resp string) error {
if resp == "" {
return nil
}
_, err = os.Stat(resp)
return err
}); err != nil {
return err
}
}
if output == "" {
if output, err = getPrompt(cmd, reader, "Please enter the output location of the generated definition. Leave it empty to print definition to stdout.\n", "> Definition output filename: ", nil); err != nil {
return err
}
}
}
kind, ok := pkgdef.DefinitionTypeToKind[definitionType]
if !ok {
return errors.New("invalid definition type")
}
name := args[0]
// Handle Go-based definitions
if lang == "go" {
defStr, err = generateGoDefinition(name, definitionType, desc)
if err != nil {
return errors.Wrapf(err, "failed to generate Go definition")
}
} else {
provider, err := cmd.Flags().GetString(FlagProvider)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagProvider)
}
if provider != "" {
defStr, err = generateTerraformTypedComponentDefinition(cmd, name, kind, provider, desc)
if err != nil {
return errors.Wrapf(err, "failed to generate Terraform typed component definition")
}
} else {
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
def.SetGVK(kind)
def.SetName(name)
def.SetAnnotations(map[string]string{
pkgdef.DescriptionKey: desc,
pkgdef.AliasKey: alias,
})
def.SetLabels(map[string]string{})
def.Object["spec"] = pkgdef.GetDefinitionDefaultSpec(def.GetKind())
if templateYAML != "" {
if err = buildTemplateFromYAML(templateYAML, &def); err != nil {
return err
}
}
defStr, err = def.ToCUEString()
if err != nil {
return errors.Wrapf(err, "failed to generate cue string")
}
}
}
if output != "" {
if err = os.WriteFile(path.Clean(output), []byte(defStr), 0600); err != nil {
return errors.Wrapf(err, "failed to write definition into %s", output)
}
cmd.Printf("Definition written to %s\n", output)
} else if _, err = cmd.OutOrStdout().Write([]byte(defStr + "\n")); err != nil {
return errors.Wrapf(err, "failed to write out definition")
}
return nil
},
}
cmd.Flags().StringP(FlagType, "t", "", "Specify the type of the new definition. Valid types: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", "))
cmd.Flags().StringP(FlagDescription, "d", "", "Specify the description of the new definition.")
cmd.Flags().StringP(FlagAlias, "a", "", "Specify the alias of the new definition.")
cmd.Flags().StringP(FlagTemplateYAML, "f", "", "Specify the template yaml file that definition will use to build the schema. If empty, a default template for the given definition type will be used.")
cmd.Flags().StringP(FlagOutput, "o", "", "Specify the output path of the generated definition. If empty, the definition will be printed in the console.")
cmd.Flags().BoolP(FlagInteractive, "i", false, "Specify whether use interactive process to help generate definitions.")
cmd.Flags().StringP(FlagLang, "l", "cue", "Specify the language of the definition. Valid options: cue, go")
cmd.Flags().StringP(FlagProvider, "p", "", "Specify which provider the cloud resource definition belongs to. Only `alibaba`, `aws`, `azure`, `gcp`, `baidu`, `tencent`, `elastic`, `ucloud`, `vsphere` are supported.")
cmd.Flags().StringP(FlagGit, "", "", "Specify which git repository the configuration(HCL) is stored in. Valid when --provider/-p is set.")
cmd.Flags().StringP(FlagLocal, "", "", "Specify the local path of the configuration(HCL) file. Valid when --provider/-p is set.")
cmd.Flags().StringP(FlagPath, "", "", "Specify which path the configuration(HCL) is stored in the Git repository. Valid when --git is set.")
return cmd
}
// generateGoDefinition generates a Go-based definition scaffold
func generateGoDefinition(name, definitionType, desc string) (string, error) {
// Convert name to Go-friendly format (PascalCase)
funcName := toPascalCase(name)
switch definitionType {
case "component":
return generateGoComponentDefinition(name, funcName, desc), nil
case "trait":
return generateGoTraitDefinition(name, funcName, desc), nil
case "policy":
return generateGoPolicyDefinition(name, funcName, desc), nil
case "workflow-step":
return generateGoWorkflowStepDefinition(name, funcName, desc), nil
default:
return "", errors.Errorf("unsupported definition type for Go: %s", definitionType)
}
}
// toPascalCase converts a kebab-case or snake_case string to PascalCase
func toPascalCase(s string) string {
// Split by common separators
parts := strings.FieldsFunc(s, func(r rune) bool {
return r == '-' || r == '_'
})
var result strings.Builder
for _, part := range parts {
if len(part) > 0 {
result.WriteString(strings.ToUpper(part[:1]))
result.WriteString(strings.ToLower(part[1:]))
}
}
return result.String()
}
func generateGoComponentDefinition(name, funcName, desc string) 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
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
// %sComponent creates the %s component definition.
func %sComponent() *defkit.ComponentDefinition {
// Define parameters
image := defkit.String("image").
Required().
Description("Container image to use")
// Create the component definition
return defkit.NewComponent("%s").
Description(%q).
Workload("apps/v1", "Deployment").
Params(image).
Template(func(tpl *defkit.Template) {
// Access runtime context
vela := defkit.VelaCtx()
// Create the Deployment resource
deployment := defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", vela.Name()).
Set("metadata.labels.app", vela.Name()).
Set("spec.selector.matchLabels.app", vela.Name()).
Set("spec.template.metadata.labels.app", vela.Name()).
Set("spec.template.spec.containers[0].name", vela.Name()).
Set("spec.template.spec.containers[0].image", image)
// Set as primary output
tpl.Output(deployment)
})
}
func main() {
// Print the CUE definition
component := %sComponent()
println(component.ToCue())
}
`, funcName, name, funcName, name, desc, funcName)
}
func generateGoTraitDefinition(name, funcName, desc string) 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
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
// %sTrait creates the %s trait definition.
func %sTrait() *defkit.TraitDefinition {
// Define parameters
replicas := defkit.Int("replicas").
Default(1).
Description("Number of replicas")
// Create the trait definition
return defkit.NewTrait("%s").
Description(%q).
AppliesTo("deployments.apps", "statefulsets.apps").
Params(replicas).
Template(func(tpl *defkit.Template) {
// Access the context
vela := defkit.VelaCtx()
// Patch the workload
patch := defkit.NewPatch().
Set("spec.replicas", replicas)
tpl.Patch(patch)
_ = vela // use vela context as needed
})
}
func main() {
// Print the CUE definition
trait := %sTrait()
println(trait.ToCue())
}
`, funcName, name, funcName, name, desc, funcName)
}
func generateGoPolicyDefinition(name, funcName, desc string) 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
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
// %sPolicy creates the %s policy definition.
func %sPolicy() *defkit.PolicyDefinition {
// Define parameters
enabled := defkit.Bool("enabled").
Default(true).
Description("Whether to enable this policy")
// Create the policy definition
return defkit.NewPolicy("%s").
Description(%q).
Params(enabled).
Template(func(tpl *defkit.Template) {
// Policy logic here
_ = enabled
})
}
func main() {
// Print the CUE definition
policy := %sPolicy()
println(policy.ToCue())
}
`, funcName, name, funcName, name, desc, funcName)
}
func generateGoWorkflowStepDefinition(name, funcName, desc string) 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
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
// %sWorkflowStep creates the %s workflow step definition.
func %sWorkflowStep() *defkit.WorkflowStepDefinition {
// Define parameters
message := defkit.String("message").
Default("Hello").
Description("Message to use in the workflow step")
// Create the workflow step definition
return defkit.NewWorkflowStep("%s").
Description(%q).
Params(message).
Template(func(tpl *defkit.Template) {
// Workflow step logic here
_ = message
})
}
func main() {
// Print the CUE definition
step := %sWorkflowStep()
println(step.ToCue())
}
`, funcName, name, funcName, name, desc, funcName)
}
func generateTerraformTypedComponentDefinition(cmd *cobra.Command, name, kind, provider, desc string) (string, error) {
if kind != v1beta1.ComponentDefinitionKind {
return "", errors.New("provider is only valid when the type of the definition is component")
}
switch provider {
case "aws", "azure", "alibaba", "tencent", "gcp", "baidu", "elastic", "ucloud", "vsphere", "huawei":
var terraform *commontype.Terraform
git, err := cmd.Flags().GetString(FlagGit)
if err != nil {
return "", errors.Wrapf(err, "failed to get `%s`", FlagGit)
}
local, err := cmd.Flags().GetString(FlagLocal)
if err != nil {
return "", errors.Wrapf(err, "failed to get `%s`", FlagLocal)
}
if git != "" && local != "" {
return "", errors.New("only one of --git and --local can be set")
}
gitPath, err := cmd.Flags().GetString(FlagPath)
if err != nil {
return "", errors.Wrapf(err, "failed to get `%s`", FlagPath)
}
if git != "" {
if !strings.HasPrefix(git, "https://") || !strings.HasSuffix(git, ".git") {
return "", errors.Errorf("invalid git url: %s", git)
}
terraform = &commontype.Terraform{
Configuration: git,
Type: "remote",
Path: gitPath,
}
} else if local != "" {
hcl, err := os.ReadFile(filepath.Clean(local))
if err != nil {
return "", errors.Wrapf(err, "failed to read Terraform configuration from file %s", local)
}
terraform = &commontype.Terraform{
Configuration: string(hcl),
}
}
def := v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1beta1",
Kind: "ComponentDefinition",
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s", provider, name),
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
"definition.oam.dev/description": desc,
},
Labels: map[string]string{
"type": "terraform",
},
},
Spec: v1beta1.ComponentDefinitionSpec{
Workload: commontype.WorkloadTypeDescriptor{
Definition: commontype.WorkloadGVK{
APIVersion: "terraform.core.oam.dev/v1beta2",
Kind: "Configuration",
},
},
Schematic: &commontype.Schematic{
Terraform: terraform,
},
},
}
if provider != "alibaba" {
def.Spec.Schematic.Terraform.ProviderReference = &crossplane.Reference{
Name: provider,
Namespace: "default",
}
}
var out bytes.Buffer
err = json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{Yaml: true}).Encode(&def, &out)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal component definition")
}
return out.String(), nil
default:
return "", errors.Errorf("Provider `%s` is not supported. Only `alibaba`, `aws`, `azure`, `gcp`, `baidu`, `tencent`, `elastic`, `ucloud`, `vsphere` are supported.", provider)
}
}
func getSingleDefinition(cmd *cobra.Command, definitionName string, client client.Client, definitionType string, namespace string) (*pkgdef.Definition, error) {
definitions, err := pkgdef.SearchDefinition(client, definitionType, namespace, filters.ByName(definitionName))
if err != nil {
return nil, err
}
if len(definitions) == 0 {
return nil, fmt.Errorf("definition not found")
}
if len(definitions) > 1 {
table := newUITable()
table.AddRow("NAME", "TYPE", "NAMESPACE", "DESCRIPTION")
for _, definition := range definitions {
desc := ""
if annotations := definition.GetAnnotations(); annotations != nil {
desc = annotations[pkgdef.DescriptionKey]
}
table.AddRow(definition.GetName(), definition.GetKind(), definition.GetNamespace(), desc)
}
cmd.Println(table)
return nil, fmt.Errorf("found %d definitions, please specify which one to select with more arguments", len(definitions))
}
return &pkgdef.Definition{Unstructured: definitions[0]}, nil
}
// getDefRevs will search for DefinitionRevisions with specified conditions.
// Check SearchDefinitionRevisions for details.
func getDefRevs(ctx context.Context, client client.Client, ns, defTypeStr, defName string, rev int64) ([]v1beta1.DefinitionRevision, error) {
defType, ok := pkgdef.StringToDefinitionType[defTypeStr]
// Empty definition type is intentionally allowed, to allow the user to match all definition types
if defTypeStr != "" && !ok {
return nil, fmt.Errorf("%s is not a valid type. Valid types are %v", defTypeStr, reflect.ValueOf(pkgdef.StringToDefinitionType).MapKeys())
}
return pkgdef.SearchDefinitionRevisions(ctx, client, ns, defName, defType, rev)
}
// printDefRevs will print DefinitionRevisions
func printDefRevs(ctx context.Context, cmd *cobra.Command, client client.Client, ns, defTypeStr, defName string) error {
revs, err := getDefRevs(ctx, client, ns, defTypeStr, defName, 0)
if err != nil {
return err
}
table := newUITable()
table.AddRow("NAME", "REVISION", "TYPE", "HASH")
for _, rev := range revs {
table.AddRow(defName, rev.Spec.Revision, rev.Spec.DefinitionType, rev.Spec.RevisionHash)
}
cmd.Println(table)
return nil
}
// NewDefinitionGetCommand create the `vela def get` command to get definition from k8s
func NewDefinitionGetCommand(c common.Args) *cobra.Command {
var listRevisions bool
var targetRevision string
cmd := &cobra.Command{
Use: "get NAME",
Short: "Get definition",
Long: "Get definition from kubernetes cluster",
Example: "# Command below will get the ComponentDefinition(or other definitions if exists) of webservice in all namespaces\n" +
"> vela def get webservice\n" +
"# Command below will get the TraitDefinition of annotations in namespace vela-system\n" +
"> vela def get annotations --type trait --namespace vela-system",
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "2",
},
RunE: func(cmd *cobra.Command, args []string) error {
definitionType, err := cmd.Flags().GetString(FlagType)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagType)
}
namespace, err := cmd.Flags().GetString(FlagNamespace)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", Namespace)
}
k8sClient, err := c.GetClient()
if err != nil {
return errors.Wrapf(err, "failed to get k8s client")
}
if listRevisions {
return printDefRevs(context.Background(), cmd, k8sClient, namespace, definitionType, args[0])
}
var def *pkgdef.Definition
// Get history Definition from DefinitionRevisions
if targetRevision != "" {
// "v1", "1", both need to work
targetRevision = strings.TrimPrefix(targetRevision, "v")
ver, err := strconv.Atoi(targetRevision)
if err != nil {
return fmt.Errorf("invalid version: %w", err)
}
// Get the user-specified revision.
revs, err := getDefRevs(context.Background(), k8sClient, namespace, definitionType, args[0], int64(ver))
if err != nil {
return err
}
if len(revs) == 0 {
return fmt.Errorf("no %s with revision %s found in namespace %s", args[0], targetRevision, namespace)
}
// Now we have at least one DefinitionRevision (typically it will only be one).
// They all fit user's conditions. We will use the first one.
// Extract Definition from DefinitionRevision that we just got.
def, err = pkgdef.GetDefinitionFromDefinitionRevision(&revs[0])
if err != nil {
return err
}
} else {
def, err = getSingleDefinition(cmd, args[0], k8sClient, definitionType, namespace)
if err != nil {
return err
}
}
cueString, err := def.ToCUEString()
if err != nil {
return errors.Wrapf(err, "failed to get cue format definition")
}
if _, err = cmd.OutOrStdout().Write([]byte(cueString + "\n")); err != nil {
return errors.Wrapf(err, "failed to write out cue string")
}
return nil
},
}
cmd.Flags().StringP(FlagType, "t", "", "Specify which definition type to get. If empty, all types will be searched. Valid types: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", "))
cmd.Flags().BoolVarP(&listRevisions, "revisions", "", false, "List revisions of the specified definition.")
cmd.Flags().StringVarP(&targetRevision, "revision", "r", "", "Get the specified version of a definition.")
cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.")
return cmd
}
// NewDefinitionDocGenCommand create the `vela def doc-gen` command to generate documentation of definitions
func NewDefinitionDocGenCommand(c common.Args, ioStreams util.IOStreams) *cobra.Command {
var docPath, location, i18nPath string
cmd := &cobra.Command{
Use: "doc-gen NAME",
Short: "Generate documentation for definitions",
Long: "Generate documentation for definitions",
Example: "1. Generate documentation for ComponentDefinition webservice:\n" +
"> vela def doc-gen webservice -n vela-system\n" +
"2. Generate documentation for local CUE Definition file webservice.cue:\n" +
"> vela def doc-gen webservice.cue\n" +
"3. Generate documentation for local Cloud Resource Definition YAML alibaba-vpc.yaml:\n" +
"> vela def doc-gen alibaba-vpc.yaml\n",
Deprecated: "This command has been replaced by 'vela show' or 'vela def show'.",
Annotations: map[string]string{
types.TagCommandType: types.TypeDefGeneration,
types.TagCommandOrder: "1",
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("please specify definition name, cue file or a cloud resource definition yaml")
}
namespace, err := cmd.Flags().GetString(FlagNamespace)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", Namespace)
}
return ShowReferenceMarkdown(context.Background(), c, ioStreams, args[0], docPath, location, i18nPath, namespace, 0)
},
}
cmd.Flags().StringVarP(&docPath, "path", "p", "", "Specify the path for of the doc generated from definition.")
cmd.Flags().StringVarP(&location, "location", "l", "", "specify the location for of the doc generated from definition, now supported options 'zh', 'en'. ")
cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.")
cmd.Flags().StringVarP(&i18nPath, "i18n", "", "https://kubevela.io/reference-i18n.json", "specify the location for of the doc generated from definition, now supported options 'zh', 'en'. ")
return cmd
}
// NewDefinitionListCommand create the `vela def list` command to list definition from k8s
func NewDefinitionListCommand(c common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List definitions.",
Long: "List definitions in kubernetes cluster.",
Example: "# Command below will list all definitions in all namespaces\n" +
"> vela def list\n" +
"# Command below will list all definitions in the vela-system namespace\n" +
"> vela def get annotations --type trait --namespace vela-system",
Args: cobra.ExactArgs(0),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "3",
},
RunE: func(cmd *cobra.Command, args []string) error {
definitionType, err := cmd.Flags().GetString(FlagType)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagType)
}
namespace, err := cmd.Flags().GetString(FlagNamespace)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", Namespace)
}
addonName, err := cmd.Flags().GetString("from")
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", "from")
}
k8sClient, err := c.GetClient()
if err != nil {
return errors.Wrapf(err, "failed to get k8s client")
}
definitions, err := pkgdef.SearchDefinition(k8sClient,
definitionType,
namespace,
filters.ByOwnerAddon(addonName))
if err != nil {
return err
}
if len(definitions) == 0 {
cmd.Println("No definition found.")
return nil
}
// Determine if there is a definition in the list from some addons
// This is used to tell if we want the SOURCE-ADDON column
showSourceAddon := false
for _, def := range definitions {
ownerRef := def.GetOwnerReferences()
if len(ownerRef) > 0 && strings.HasPrefix(ownerRef[0].Name, addonutil.AddonAppPrefix) {
showSourceAddon = true
break
}
}
table := newUITable()
// We only include SOURCE-ADDON if there is at least one definition from an addon
if showSourceAddon {
table.AddRow("NAME", "TYPE", "NAMESPACE", "SOURCE-ADDON", "DESCRIPTION")
} else {
table.AddRow("NAME", "TYPE", "NAMESPACE", "DESCRIPTION")
}
for _, definition := range definitions {
desc := ""
if annotations := definition.GetAnnotations(); annotations != nil {
desc = annotations[pkgdef.DescriptionKey]
}
// Do not show SOURCE-ADDON column
if !showSourceAddon {
table.AddRow(definition.GetName(), definition.GetKind(), definition.GetNamespace(), desc)
continue
}
sourceAddon := ""
if len(definition.GetOwnerReferences()) > 0 {
sourceAddon = strings.TrimPrefix(definition.GetOwnerReferences()[0].Name, "addon-")
}
table.AddRow(definition.GetName(), definition.GetKind(), definition.GetNamespace(), sourceAddon, desc)
}
cmd.Println(table)
return nil
},
}
cmd.Flags().StringP(FlagType, "t", "", "Specify which definition type to list. If empty, all types will be searched. Valid types: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", "))
cmd.Flags().String("from", "", "Filter definitions by which addon installed them.")
cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.")
return cmd
}
// NewDefinitionEditCommand create the `vela def edit` command to help user edit remote definitions
func NewDefinitionEditCommand(c common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "edit NAME",
Short: "Edit X-Definition.",
Long: "Edit X-Definition in kubernetes. If type and namespace are not specified, the command will automatically search all possible results.\n" +
"By default, this command will use the vi editor and can be altered by setting EDITOR environment variable.",
Example: "# Command below will edit the ComponentDefinition (and other definitions if exists) of webservice in kubernetes\n" +
"> vela def edit webservice\n" +
"# Command below will edit the TraitDefinition of ingress in vela-system namespace\n" +
"> vela def edit ingress --type trait --namespace vela-system",
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "4",
},
RunE: func(cmd *cobra.Command, args []string) error {
definitionType, err := cmd.Flags().GetString(FlagType)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagType)
}
namespace, err := cmd.Flags().GetString(FlagNamespace)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", Namespace)
}
config, err := c.GetConfig()
if err != nil {
return err
}
k8sClient, err := c.GetClient()
if err != nil {
return errors.Wrapf(err, "failed to get k8s client")
}
def, err := getSingleDefinition(cmd, args[0], k8sClient, definitionType, namespace)
if err != nil {
return err
}
cueString, err := def.ToCUEString()
if err != nil {
return errors.Wrapf(err, "failed to get cue format definition")
}
cleanup := func(filePath string) {
if err := os.Remove(filePath); err != nil {
cmd.PrintErrf("failed to remove file %s: %v", filePath, err)
}
}
filename := fmt.Sprintf("vela-def-%d", time.Now().UnixNano())
tempFilePath := filepath.Join(os.TempDir(), filename+CUEExtension)
if err := os.WriteFile(tempFilePath, []byte(cueString), 0600); err != nil {
return errors.Wrapf(err, "failed to write temporary file")
}
defer cleanup(tempFilePath)
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
scriptFilePath := filepath.Join(os.TempDir(), filename+".sh")
if err := os.WriteFile(scriptFilePath, []byte(editor+" "+tempFilePath), 0600); err != nil {
return errors.Wrapf(err, "failed to write temporary script file")
}
defer cleanup(scriptFilePath)
editCmd := exec.Command("sh", path.Clean(scriptFilePath)) //nolint:gosec
editCmd.Stdin = os.Stdin
editCmd.Stdout = os.Stdout
editCmd.Stderr = os.Stderr
if err = editCmd.Run(); err != nil {
return errors.Wrapf(err, "failed to run editor %s at path %s", editor, scriptFilePath)
}
newBuf, err := os.ReadFile(path.Clean(tempFilePath))
if err != nil {
return errors.Wrapf(err, "failed to read temporary file %s", tempFilePath)
}
if cueString == string(newBuf) {
cmd.Printf("definition unchanged\n")
return nil
}
if err := def.FromCUEString(string(newBuf), config); err != nil {
return errors.Wrapf(err, "failed to load edited cue string")
}
if err := k8sClient.Update(context.Background(), def); err != nil {
return errors.Wrapf(err, "failed to apply changes to kubernetes")
}
cmd.Printf("Definition edited successfully.\n")
return nil
},
}
cmd.Flags().StringP(FlagType, "t", "", "Specify which definition type to get. If empty, all types will be searched. Valid types: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", "))
cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.")
return cmd
}
func prettyYAMLMarshal(obj map[string]interface{}) (string, error) {
var b bytes.Buffer
encoder := yaml.NewEncoder(&b)
encoder.SetIndent(2)
err := encoder.Encode(&obj)
if err != nil {
return "", err
}
return b.String(), nil
}
// FlagFormat is the flag for specifying the output format
const FlagFormat = "format"
// NewDefinitionRenderCommand create the `vela def render` command to help user render definition cue file into k8s YAML file, if used without kubernetes environment, set IGNORE_KUBE_CONFIG=true
//
//nolint:gocyclo // CLI command handling multiple input/output formats and file types
func NewDefinitionRenderCommand(c common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "render DEFINITION.cue|DEFINITION.go",
Short: "Render X-Definition.",
Long: "Render X-Definition with CUE or Go format into kubernetes YAML format (default) or CUE format. Could be used to check whether the definition is working as expected. " +
"If a directory is used as input, all CUE and Go definitions in the directory will be rendered.\n\n" +
"Go definitions use the defkit package to define components, traits, policies, and workflow steps in Go code. " +
"The Go file must import defkit and export functions that return definition types (e.g., *defkit.ComponentDefinition).\n\n" +
"Use --format cue to output the raw CUE template instead of YAML. This is useful for inspecting the generated CUE from Go definitions.",
Example: "# Command below will render my-webservice.cue into YAML format and print it out.\n" +
"> vela def render my-webservice.cue\n" +
"# Command below will render a Go definition into YAML format.\n" +
"> vela def render my-webservice.go\n" +
"# Command below will render a Go definition into CUE format.\n" +
"> vela def render my-webservice.go --format cue\n" +
"# Command below will render my-webservice.cue and save it in my-webservice.yaml.\n" +
"> vela def render my-webservice.cue -o my-webservice.yaml\n" +
"# Command below will render all CUE and Go definitions in the ./defs/ directory and save the YAML objects in ./defs/yaml/.\n" +
"> vela def render ./defs/ -o ./defs/yaml/\n" +
"# Command below will render all CUE and Go definitions in ./defs/ and save CUE output in ./defs/cue/.\n" +
"> vela def render ./defs/ -o ./defs/cue/ --format cue",
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "5",
},
RunE: func(cmd *cobra.Command, args []string) error {
output, err := cmd.Flags().GetString(FlagOutput)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagOutput)
}
message, err := cmd.Flags().GetString(FlagMessage)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagMessage)
}
outputFormat, err := cmd.Flags().GetString(FlagFormat)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagFormat)
}
// Validate output format
if outputFormat != "yaml" && outputFormat != "cue" {
return errors.Errorf("invalid format %q, must be 'yaml' or 'cue'", outputFormat)
}
// renderCUE renders a CUE definition file to YAML or CUE output
renderCUE := func(inputFilename, outputFilename string) error {
cueBytes, err := utils.ReadRemoteOrLocalPath(inputFilename, false)
if err != nil {
return errors.Wrapf(err, "failed to get %s", args[0])
}
// If format is cue, just output the raw CUE content
if outputFormat == "cue" {
cueStr := string(cueBytes)
if outputFilename == "" {
cmd.Printf("--- %s ---\n%s\n", filepath.Base(inputFilename), cueStr)
} else {
if message != "" {
cueStr = "// " + strings.ReplaceAll(message, "{{INPUT_FILENAME}}", filepath.Base(inputFilename)) + "\n" + cueStr
}
if err := os.WriteFile(outputFilename, []byte(cueStr), 0600); err != nil {
return errors.Wrapf(err, "failed to write CUE format definition to file %s", outputFilename)
}
}
return nil
}
config, err := c.GetConfig()
if err != nil {
klog.Infof("ignore kubernetes cluster, unable to get kubeconfig: %s", err.Error())
}
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
if err := def.FromCUEString(string(cueBytes), config); err != nil {
return errors.Wrapf(err, "failed to parse CUE")
}
helmChartFormatEnv := strings.ToLower(os.Getenv(HelmChartFormatEnvName))
if helmChartFormatEnv == "true" {
def.SetNamespace(HelmChartNamespacePlaceholder)
} else if helmChartFormatEnv == "system" {
def.SetNamespace(types.DefaultKubeVelaNS)
}
if len(def.GetLabels()) == 0 {
def.SetLabels(nil)
}
s, err := prettyYAMLMarshal(def.Object)
if err != nil {
return errors.Wrapf(err, "failed to marshal CRD into YAML")
}
s = strings.ReplaceAll(s, "'"+HelmChartNamespacePlaceholder+"'", "{{ include \"systemDefinitionNamespace\" . }}") + "\n"
if outputFilename == "" {
s = fmt.Sprintf("--- %s ---\n%s", filepath.Base(inputFilename), s)
cmd.Print(s)
} else {
if message != "" {
s = "# " + strings.ReplaceAll(message, "{{INPUT_FILENAME}}", filepath.Base(inputFilename)) + "\n" + s
}
s = "# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.\n" + s
if err := os.WriteFile(outputFilename, []byte(s), 0600); err != nil {
return errors.Wrapf(err, "failed to write YAML format definition to file %s", outputFilename)
}
}
return nil
}
// renderGo renders a Go definition file to YAML or CUE output
renderGo := func(inputFilename, outputFilename string) error {
results, err := goloader.LoadFromFile(inputFilename)
if err != nil {
return errors.Wrapf(err, "failed to load Go definition from %s", inputFilename)
}
// If format is cue, output the raw generated CUE content
if outputFormat == "cue" {
var allCUE strings.Builder
for _, result := range results {
if result.Error != nil {
return errors.Wrapf(result.Error, "failed to generate CUE for %s", result.Definition.FunctionName)
}
if outputFilename == "" {
allCUE.WriteString(fmt.Sprintf("--- %s (%s) ---\n%s\n", filepath.Base(inputFilename), result.Definition.Name, result.CUE))
} else {
allCUE.WriteString(result.CUE)
if len(results) > 1 {
allCUE.WriteString("\n---\n")
}
}
}
if outputFilename == "" {
cmd.Print(allCUE.String())
} else {
content := allCUE.String()
if message != "" {
content = "// " + strings.ReplaceAll(message, "{{INPUT_FILENAME}}", filepath.Base(inputFilename)) + "\n" + content
}
content = "// Code generated by KubeVela templates. DO NOT EDIT. Please edit the original Go file.\n" + content
if err := os.WriteFile(outputFilename, []byte(content), 0600); err != nil {
return errors.Wrapf(err, "failed to write CUE format definition to file %s", outputFilename)
}
}
return nil
}
config, err := c.GetConfig()
if err != nil {
klog.Infof("ignore kubernetes cluster, unable to get kubeconfig: %s", err.Error())
}
var allYAML strings.Builder
for _, result := range results {
if result.Error != nil {
return errors.Wrapf(result.Error, "failed to generate CUE for %s", result.Definition.FunctionName)
}
// Convert generated CUE to Definition
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
if err := def.FromCUEString(result.CUE, config); err != nil {
return errors.Wrapf(err, "failed to parse generated CUE for %s", result.Definition.FunctionName)
}
helmChartFormatEnv := strings.ToLower(os.Getenv(HelmChartFormatEnvName))
if helmChartFormatEnv == "true" {
def.SetNamespace(HelmChartNamespacePlaceholder)
} else if helmChartFormatEnv == "system" {
def.SetNamespace(types.DefaultKubeVelaNS)
}
if len(def.GetLabels()) == 0 {
def.SetLabels(nil)
}
s, err := prettyYAMLMarshal(def.Object)
if err != nil {
return errors.Wrapf(err, "failed to marshal CRD into YAML for %s", result.Definition.FunctionName)
}
s = strings.ReplaceAll(s, "'"+HelmChartNamespacePlaceholder+"'", "{{ include \"systemDefinitionNamespace\" . }}") + "\n"
if outputFilename == "" {
allYAML.WriteString(fmt.Sprintf("--- %s (%s) ---\n%s", filepath.Base(inputFilename), result.Definition.Name, s))
} else {
allYAML.WriteString(s)
if len(results) > 1 {
allYAML.WriteString("---\n")
}
}
}
if outputFilename == "" {
cmd.Print(allYAML.String())
} else {
content := allYAML.String()
if message != "" {
content = "# " + strings.ReplaceAll(message, "{{INPUT_FILENAME}}", filepath.Base(inputFilename)) + "\n" + content
}
content = "# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original Go file.\n" + content
if err := os.WriteFile(outputFilename, []byte(content), 0600); err != nil {
return errors.Wrapf(err, "failed to write YAML format definition to file %s", outputFilename)
}
}
return nil
}
// render dispatches to the appropriate renderer based on file extension
render := func(inputFilename, outputFilename string) error {
ext := filepath.Ext(inputFilename)
switch ext {
case GoExtension:
// Skip test files
if strings.HasSuffix(inputFilename, "_test.go") {
return nil
}
return renderGo(inputFilename, outputFilename)
case CUEExtension:
return renderCUE(inputFilename, outputFilename)
default:
return errors.Errorf("unsupported file extension %s, expected .cue or .go", ext)
}
}
inputFilenames := []string{args[0]}
outputFilenames := []string{output}
fi, err := os.Stat(args[0])
if err != nil {
return errors.Wrapf(err, "failed to get input %s", args[0])
}
if fi.IsDir() {
inputFilenames = []string{}
outputFilenames = []string{}
err := filepath.Walk(args[0], func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
filename := filepath.Base(path)
fileSuffix := filepath.Ext(path)
// Determine output extension based on format
outputExt := YAMLExtension
if outputFormat == "cue" {
outputExt = CUEExtension
}
// Handle CUE files
if fileSuffix == CUEExtension {
inputFilenames = append(inputFilenames, path)
if output != "" {
outputFilenames = append(outputFilenames, filepath.Join(output, strings.ReplaceAll(filename, CUEExtension, outputExt)))
} else {
outputFilenames = append(outputFilenames, "")
}
return nil
}
// Handle Go files (skip test files)
if fileSuffix == GoExtension && !strings.HasSuffix(filename, "_test.go") {
// Check if this is a defkit definition file
isDefFile, checkErr := goloader.IsGoDefinitionFile(path)
if checkErr != nil {
klog.Warningf("failed to check if %s is a Go definition file: %v", path, checkErr)
return nil
}
if isDefFile {
inputFilenames = append(inputFilenames, path)
if output != "" {
outputFilenames = append(outputFilenames, filepath.Join(output, strings.ReplaceAll(filename, GoExtension, outputExt)))
} else {
outputFilenames = append(outputFilenames, "")
}
}
}
return nil
})
if err != nil {
return errors.Wrapf(err, "failed to read directory %s", args[0])
}
}
var renderErrs []error
for i, inputFilename := range inputFilenames {
renderErr := render(inputFilename, outputFilenames[i])
if renderErr != nil {
wrappedRenderErr := errors.Wrapf(renderErr, "failed to render %s", inputFilename)
if _, writeErr := fmt.Fprintln(cmd.ErrOrStderr(), wrappedRenderErr); writeErr != nil {
renderErrs = append(renderErrs, errors.Wrapf(writeErr, "failed to write to stderr for %s", inputFilename))
}
renderErrs = append(renderErrs, wrappedRenderErr)
}
}
if len(renderErrs) > 0 {
return fmt.Errorf("rendering failed for %d file(s)", len(renderErrs))
}
return nil
},
}
cmd.Flags().StringP(FlagOutput, "o", "", "Specify the output path of the rendered definition. If empty, the definition will be printed in the console. If input is a directory, the output path is expected to be a directory as well.")
cmd.Flags().StringP(FlagMessage, "", "", "Specify the header message of the generated file. For example, declaring author information.")
cmd.Flags().StringP(FlagFormat, "f", "yaml", "Specify the output format: 'yaml' (default) for Kubernetes YAML, or 'cue' for raw CUE template output.")
return cmd
}
// NewDefinitionApplyCommand create the `vela def apply` command to help user apply local definitions to k8s
func NewDefinitionApplyCommand(c common.Args, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "apply DEFINITION.cue|DEFINITION.go",
Short: "Apply X-Definition.",
Long: "Apply X-Definition from local storage to kubernetes cluster. It will apply file to vela-system namespace by default.\n\n" +
"Supports CUE, YAML, and Go definition files. Go definitions use the defkit package to define components, traits, policies, and workflow steps.",
Example: "# Command below will apply the local my-webservice.cue file to kubernetes vela-system namespace\n" +
"> vela def apply my-webservice.cue\n" +
"# Apply a Go definition file to kubernetes vela-system namespace\n" +
"> vela def apply my-webservice.go\n" +
"# Apply the local directory including all files (YAML, CUE, and Go definitions) to kubernetes vela-system namespace\n" +
"> vela def apply def/\n" +
"# Command below will apply the ./defs/my-trait.cue file to kubernetes default namespace\n" +
"> vela def apply ./defs/my-trait.cue --namespace default\n" +
"# Command below will convert the ./defs/my-trait.cue file to kubernetes CRD object and print it without applying it to kubernetes\n" +
"> vela def apply ./defs/my-trait.cue --dry-run\n" +
"# Apply a CUE from URL \n" +
"> vela def apply https://my-host-to-def/my-trait.cue --dry-run\n" +
"# Apply a CUE from stdin \n" +
"> vela def apply -",
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "6",
},
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)
}
if len(args) < 1 {
return errors.New("you must specify the definition path, directory or URL")
}
return defApplyAll(ctx, c, streams, namespace, args[0], dryRun)
},
}
cmd.Flags().BoolP(FlagDryRun, "", false, "only build definition from CUE into CRB object without applying it to kubernetes clusters")
cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.")
return cmd
}
// isDefinitionFile checks if the path is a definition file (JSON, YAML, CUE, or Go)
func isDefinitionFile(path string) bool {
// Check for standard definition file types
if utils.IsJSONYAMLorCUEFile(path) {
return true
}
// Check for Go definition files (skip test files)
if strings.HasSuffix(path, GoExtension) && !strings.HasSuffix(path, "_test.go") {
isDefFile, err := goloader.IsGoDefinitionFile(path)
if err != nil {
klog.Warningf("failed to check if %s is a Go definition file: %v", path, err)
return false
}
return isDefFile
}
return false
}
func defApplyAll(ctx context.Context, c common.Args, io util.IOStreams, namespace, path string, dryRun bool) error {
files, err := utils.LoadDataFromPath(ctx, path, isDefinitionFile)
if err != nil {
return errors.Wrapf(err, "failed to get from %s", path)
}
for _, f := range files {
results, err := defApplyOne(ctx, c, namespace, f.Path, f.Data, dryRun)
if err != nil {
return err
}
for _, result := range results {
io.Infonln(result)
}
}
return nil
}
func defApplyOne(ctx context.Context, c common.Args, namespace, defpath string, defBytes []byte, dryRun bool) ([]string, error) {
config, err := c.GetConfig()
if err != nil {
return nil, err
}
k8sClient, err := c.GetClient()
if err != nil {
return nil, errors.Wrapf(err, "failed to get k8s client")
}
// Handle Go definition files
if strings.HasSuffix(defpath, GoExtension) {
return defApplyGoFile(ctx, c, k8sClient, config, namespace, defpath, dryRun)
}
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
switch {
case strings.HasSuffix(defpath, YAMLExtension) || strings.HasSuffix(defpath, YMLExtension):
// In this case, it's not in cue format, it's a yaml
if err = def.FromYAML(defBytes); err != nil {
return nil, errors.Wrapf(err, "failed to parse YAML to definition")
}
if dryRun {
return nil, errors.New("dry-run will render CUE to YAML, while the input is already in yaml")
}
// YAML won't validate or format CUE schematic
op, err := utils.CreateOrUpdate(ctx, k8sClient, &def)
if err != nil {
return nil, err
}
return []string{fmt.Sprintf("%s %s in namespace %s %s.\n", def.GetKind(), def.GetName(), def.GetNamespace(), op)}, nil
default:
if err := def.FromCUEString(string(defBytes), config); err != nil {
return nil, errors.Wrapf(err, "failed to parse CUE for definition")
}
def.SetNamespace(namespace)
}
if dryRun {
s, err := prettyYAMLMarshal(def.Object)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal CRD into YAML")
}
return []string{s}, nil
}
oldDef := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
oldDef.SetGroupVersionKind(def.GroupVersionKind())
err = k8sClient.Get(ctx, types2.NamespacedName{
Namespace: def.GetNamespace(),
Name: def.GetName(),
}, &oldDef)
if err != nil {
if errors2.IsNotFound(err) {
kind := def.GetKind()
if err = k8sClient.Create(ctx, &def); err != nil {
return nil, errors.Wrapf(err, "failed to create new definition in kubernetes")
}
return []string{fmt.Sprintf("%s %s created in namespace %s.\n", kind, def.GetName(), def.GetNamespace())}, nil
}
return nil, errors.Wrapf(err, "failed to check existence of target definition in kubernetes")
}
if err := oldDef.FromCUEString(string(defBytes), config); err != nil {
return nil, errors.Wrapf(err, "failed to merge with existing definition")
}
if err = k8sClient.Update(ctx, &oldDef); err != nil {
return nil, errors.Wrapf(err, "failed to update existing definition in kubernetes")
}
return []string{fmt.Sprintf("%s %s in namespace %s updated.\n", oldDef.GetKind(), oldDef.GetName(), oldDef.GetNamespace())}, nil
}
// defApplyGoFile handles applying Go definition files
// Args is kept for future use (e.g., Schema for type registration, DiscoveryClient for API discovery)
func defApplyGoFile(ctx context.Context, _ common.Args, k8sClient client.Client, config *rest.Config, namespace, defpath string, dryRun bool) ([]string, error) {
results, err := goloader.LoadFromFile(defpath)
if err != nil {
return nil, errors.Wrapf(err, "failed to load Go definition from %s", defpath)
}
var outputs []string
for _, result := range results {
if result.Error != nil {
return nil, errors.Wrapf(result.Error, "failed to generate CUE for %s", result.Definition.FunctionName)
}
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
if err := def.FromCUEString(result.CUE, config); err != nil {
return nil, errors.Wrapf(err, "failed to parse generated CUE for %s", result.Definition.FunctionName)
}
def.SetNamespace(namespace)
if dryRun {
s, err := prettyYAMLMarshal(def.Object)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal CRD into YAML for %s", result.Definition.FunctionName)
}
outputs = append(outputs, s)
continue
}
oldDef := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
oldDef.SetGroupVersionKind(def.GroupVersionKind())
err = k8sClient.Get(ctx, types2.NamespacedName{
Namespace: def.GetNamespace(),
Name: def.GetName(),
}, &oldDef)
if err != nil {
if errors2.IsNotFound(err) {
kind := def.GetKind()
if err = k8sClient.Create(ctx, &def); err != nil {
return nil, errors.Wrapf(err, "failed to create new definition in kubernetes")
}
outputs = append(outputs, fmt.Sprintf("%s %s created in namespace %s.\n", kind, def.GetName(), def.GetNamespace()))
continue
}
return nil, errors.Wrapf(err, "failed to check existence of target definition in kubernetes")
}
if err := oldDef.FromCUEString(result.CUE, config); err != nil {
return nil, errors.Wrapf(err, "failed to merge with existing definition")
}
if err = k8sClient.Update(ctx, &oldDef); err != nil {
return nil, errors.Wrapf(err, "failed to update existing definition in kubernetes")
}
outputs = append(outputs, fmt.Sprintf("%s %s in namespace %s updated.\n", oldDef.GetKind(), oldDef.GetName(), oldDef.GetNamespace()))
}
return outputs, nil
}
// NewDefinitionDelCommand create the `vela def del` command to help user delete existing definitions conveniently
func NewDefinitionDelCommand(c common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "del DEFINITION_NAME",
Short: "Delete X-Definition.",
Long: "Delete X-Definition in kubernetes cluster.",
Example: "# Command below will delete TraitDefinition of annotations in default namespace\n" +
"> vela def del annotations -t trait -n default",
Args: cobra.ExactArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "7",
},
RunE: func(cmd *cobra.Command, args []string) error {
definitionType, err := cmd.Flags().GetString(FlagType)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", FlagType)
}
namespace, err := cmd.Flags().GetString(FlagNamespace)
if err != nil {
return errors.Wrapf(err, "failed to get `%s`", Namespace)
}
k8sClient, err := c.GetClient()
if err != nil {
return errors.Wrapf(err, "failed to get k8s client")
}
def, err := getSingleDefinition(cmd, args[0], k8sClient, definitionType, namespace)
if err != nil {
return err
}
desc := def.GetAnnotations()[pkgdef.DescriptionKey]
toDelete := false
_, err = getPrompt(cmd, bufio.NewReader(cmd.InOrStdin()),
fmt.Sprintf("Are you sure to delete the following definition in namespace %s?\n", def.GetNamespace())+
fmt.Sprintf("%s %s: %s\n", def.GetKind(), def.GetName(), desc),
"[yes|no] > ",
func(resp string) error {
switch strings.ToLower(resp) {
case "yes":
toDelete = true
case "y":
toDelete = true
case "no":
toDelete = false
case "n":
toDelete = false
default:
return errors.New("invalid input")
}
return nil
})
if err != nil {
return err
}
if !toDelete {
return nil
}
if err := k8sClient.Delete(context.Background(), def); err != nil {
return errors.Wrapf(err, "failed to delete %s %s in namespace %s", def.GetKind(), def.GetName(), def.GetNamespace())
}
cmd.Printf("%s %s in namespace %s deleted.\n", def.GetKind(), def.GetName(), def.GetNamespace())
return nil
},
}
cmd.Flags().StringP(FlagType, "t", "", "Specify the definition type of target. Valid types: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", "))
cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.")
return cmd
}
// isCUEorGoDefinitionFile checks if a file is a CUE file or a Go definition file
func isCUEorGoDefinitionFile(path string) bool {
if utils.IsCUEFile(path) {
return true
}
if strings.HasSuffix(path, GoExtension) && !strings.HasSuffix(path, "_test.go") {
isDefFile, err := goloader.IsGoDefinitionFile(path)
if err != nil {
return false
}
return isDefFile
}
return false
}
func NewDefinitionValidateCommand(c common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "vet DEFINITION.cue|DEFINITION.go",
Short: "Validate X-Definition.",
Long: "Validate definition file by checking whether it has the valid CUE or Go format with fields set correctly.\n" +
"Supports both CUE files and Go definition files using the defkit package.\n" +
"* For CUE files, this command checks the CUE syntax and validates the definition structure.\n" +
"* For Go files, it verifies the Go syntax and ensures the generated CUE is valid.",
Example: "# Command below will validate the my-def.cue file.\n" +
"> vela def vet my-def.cue\n" +
"# Validate a Go definition file.\n" +
"> vela def vet my-def.go\n" +
"# Validate every CUE and Go definition file provided\n" +
"> vela def vet my-def1.cue my-def2.go my-def3.cue\n" +
"# Validate every CUE and Go definition file in the specified directories\n" +
"> vela def vet ./test1/ ./test2/",
Args: cobra.MinimumNArgs(1),
Annotations: map[string]string{
types.TagCommandType: types.TypeDefManagement,
types.TagCommandOrder: "8",
},
RunE: func(cmd *cobra.Command, args []string) error {
for _, arg := range args {
files, err := utils.LoadDataFromPath(cmd.Context(), arg, isCUEorGoDefinitionFile)
if err != nil {
return errors.Wrapf(err, "failed to get file from %s", arg)
}
for _, file := range files {
validateRes, err := validateDefinitionFile(file.Path, file.Data, c)
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "%s", validateRes)
}
}
return nil
},
}
return cmd
}
func validateDefinitionFile(fileName string, fileData []byte, c common.Args) (string, error) {
// Handle Go definition files
if strings.HasSuffix(fileName, GoExtension) {
return validateGoDefinitionFile(fileName, c)
}
// Handle CUE files
return validateCueFile(fileName, fileData, c)
}
func validateCueFile(fileName string, fileData []byte, c common.Args) (string, error) {
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
config, err := c.GetConfig()
if err != nil {
klog.Infof("ignore kubernetes cluster, unable to get kubeconfig: %s", err.Error())
}
if err := def.FromCUEString(string(fileData), config); err != nil {
return "", errors.Wrapf(err, "failed to parse CUE: %s", fileName)
}
return fmt.Sprintf("Validation %s succeed.\n", fileName), nil
}
func validateGoDefinitionFile(fileName string, c common.Args) (string, error) {
// Load and generate CUE from the Go file
results, err := goloader.LoadFromFile(fileName)
if err != nil {
return "", errors.Wrapf(err, "failed to load Go definition: %s", fileName)
}
config, err := c.GetConfig()
if err != nil {
klog.Infof("ignore kubernetes cluster, unable to get kubeconfig: %s", err.Error())
}
var validatedDefs []string
for _, result := range results {
if result.Error != nil {
return "", errors.Wrapf(result.Error, "failed to generate CUE for %s in %s", result.Definition.FunctionName, fileName)
}
// Validate the generated CUE
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
if err := def.FromCUEString(result.CUE, config); err != nil {
return "", errors.Wrapf(err, "generated CUE is invalid for %s in %s", result.Definition.FunctionName, fileName)
}
validatedDefs = append(validatedDefs, result.Definition.Name)
}
if len(validatedDefs) == 1 {
return fmt.Sprintf("Validation %s succeed (definition: %s).\n", fileName, validatedDefs[0]), nil
}
return fmt.Sprintf("Validation %s succeed (definitions: %s).\n", fileName, strings.Join(validatedDefs, ", ")), nil
}
// NewDefinitionGenAPICommand create the `vela def gen-api` command to help user generate Go code from the definition
func NewDefinitionGenAPICommand(c common.Args) *cobra.Command {
meta := gen_sdk.GenMeta{}
var languageArgs []string
cmd := &cobra.Command{
Use: "gen-api DEFINITION.cue",
Short: "Generate SDK from X-Definition.",
Long: "Generate SDK from X-definition file.\n" +
"* This command leverage openapi-generator project. Therefore demands \"docker\" exist in PATH\n" +
"* Currently, this function is still working in progress and not all formats of parameter in X-definition are supported yet.",
Example: "# Generate SDK for golang with scaffold initialized\n" +
"> vela def gen-api --init --language go -f /path/to/def -o /path/to/sdk\n" +
"# Generate incremental definition files to existing sdk directory\n" +
"> vela def gen-api --language go -f /path/to/def -o /path/to/sdk\n" +
"# Generate definitions to a sub-module\n" +
"> vela def gen-api --language go -f /path/to/def -o /path/to/sdk --submodule --api-dir path/relative/to/output --language-args arg1=val1,arg2=val2\n",
Annotations: map[string]string{
types.TagCommandType: types.TypeDefGeneration,
types.TagCommandOrder: "2",
},
RunE: func(cmd *cobra.Command, args []string) error {
err := meta.Init(c, languageArgs)
if err != nil {
return err
}
err = meta.CreateScaffold()
if err != nil {
return err
}
err = meta.PrepareGeneratorAndTemplate()
if err != nil {
return err
}
err = meta.Run(cmd.Context())
if err != nil {
return err
}
return nil
},
}
cmd.Flags().StringVarP(&meta.Output, "output", "o", "./apis", "Output directory path")
cmd.Flags().StringVar(&meta.APIDirectory, "api-dir", "", "API directory path to put definition API files, relative to output directory. Default value: go: pkg/apis")
cmd.Flags().BoolVar(&meta.IsSubModule, "submodule", false, "Whether the generated code is a submodule of the project. If set, the directory specified by `api-dir` will be treated as a submodule of the project")
cmd.Flags().StringVarP(&meta.Package, "package", "p", gen_sdk.PackagePlaceHolder, "Package name of generated code")
cmd.Flags().StringVarP(&meta.Lang, "language", "g", "go", "Language to generate code. Valid languages: go")
cmd.Flags().StringVarP(&meta.Template, "template", "t", "", "Template file path, if not specified, the default template will be used")
cmd.Flags().StringSliceVarP(&meta.File, "file", "f", nil, "File name of definitions, can be specified multiple times, or use comma to separate multiple files. If directory specified, all files found recursively in the directory will be used")
cmd.Flags().BoolVar(&meta.InitSDK, "init", false, "Init the whole SDK project, if not set, only the API file will be generated")
cmd.Flags().BoolVarP(&meta.Verbose, "verbose", "v", false, "Print verbose logs")
var langArgsDescStr string
for lang, args := range gen_sdk.LangArgsRegistry {
langArgsDescStr += lang + ": \n"
for key, arg := range args {
langArgsDescStr += fmt.Sprintf("\t%s: %s(default: %s)\n", key, arg.Name, arg.Default)
}
}
cmd.Flags().StringSliceVar(&languageArgs, "language-args", []string{},
fmt.Sprintf("language-specific arguments to pass to the go generator, available options: \n%s", langArgsDescStr),
)
return cmd
}
const (
genTypeProvider = "provider"
)
// NewDefinitionGenCUECommand create the `vela def gen-cue` command to help user generate CUE schema from the go code
func NewDefinitionGenCUECommand(_ common.Args, streams util.IOStreams) *cobra.Command {
var (
typ string
typeMap map[string]string
nullable bool
)
cmd := &cobra.Command{
Use: "gen-cue [flags] SOURCE.go",
Args: cobra.ExactArgs(1),
Short: "Generate CUE schema from Go code.",
Long: "Generate CUE schema from Go code.\n" +
"* This command provide a way to generate CUE schema from Go code,\n" +
"* Which can be used to keep consistency between Go code and CUE schema automatically.\n",
Example: "# Generate CUE schema for provider type\n" +
"> vela def gen-cue -t provider /path/to/myprovider.go > /path/to/myprovider.cue\n" +
"# Generate CUE schema for provider type with custom types\n" +
"> vela def gen-cue -t provider --types *k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured=ellipsis /path/to/myprovider.go > /path/to/myprovider.cue",
Annotations: map[string]string{
types.TagCommandType: types.TypeDefGeneration,
types.TagCommandOrder: "3",
},
RunE: func(cmd *cobra.Command, args []string) (rerr error) {
// convert map[string]string to map[string]cuegen.Type
newTypeMap := make(map[string]cuegen.Type, len(typeMap))
for k, v := range typeMap {
newTypeMap[k] = cuegen.Type(v)
}
file := args[0]
if !strings.HasSuffix(file, ".go") {
return fmt.Errorf("invalid file %s, must be a go file", file)
}
switch typ {
case genTypeProvider:
return providergen.Generate(providergen.Options{
File: file,
Writer: streams.Out,
Types: newTypeMap,
Nullable: nullable,
})
default:
return fmt.Errorf("invalid type %s", typ)
}
},
}
cmd.Flags().StringVarP(&typ, "type", "t", "", "Type of the definition to generate. Valid types: [provider]")
cmd.Flags().BoolVar(&nullable, "nullable", false, "Whether to generate null enum for pointer type")
cmd.Flags().StringToStringVar(&typeMap, "types", map[string]string{}, "Special types to generate, format: <package+struct>=[any|ellipsis]. e.g. --types=*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured=ellipsis")
return cmd
}
// NewDefinitionGenDocCommand create the `vela def gen-doc` command to generate documentation of definitions
func NewDefinitionGenDocCommand(_ common.Args, streams util.IOStreams) *cobra.Command {
var typ string
cmd := &cobra.Command{
Use: "gen-doc [flags] SOURCE.cue...",
Args: cobra.MinimumNArgs(1),
Short: "Generate documentation for non component, trait, policy and workflow definitions",
Long: "Generate documentation for non component, trait, policy and workflow definitions",
Example: "1. Generate documentation for provider definitions\n" +
"> vela def gen-doc -t provider provider1.cue provider2.cue > provider.md",
Annotations: map[string]string{
types.TagCommandType: types.TypeDefGeneration,
types.TagCommandOrder: "4",
},
RunE: func(cmd *cobra.Command, args []string) error {
readers := make([]io.Reader, 0, len(args))
for _, arg := range args {
if !strings.HasSuffix(arg, CUEExtension) {
return fmt.Errorf("invalid file %s, must be a cue file", arg)
}
f, err := os.ReadFile(filepath.Clean(arg))
if err != nil {
return fmt.Errorf("read file %s: %w", arg, err)
}
readers = append(readers, bytes.NewReader(f))
}
switch typ {
case genTypeProvider:
return docgen.GenerateProvidersMarkdown(cmd.Context(), readers, streams.Out)
default:
return fmt.Errorf("invalid type %s", typ)
}
},
}
cmd.Flags().StringVarP(&typ, "type", "t", "", "Type of the definition to generate. Valid types: [provider]")
return cmd
}
// NewDefinitionUpgradeCommand create the `vela def upgrade` command to help user upgrade CUE templates for version compatibility
func NewDefinitionUpgradeCommand(c common.Args, ioStreams util.IOStreams) *cobra.Command {
var (
outputFile string
targetVersion string
checkOnly bool
quiet bool
)
cmd := &cobra.Command{
Use: "upgrade DEFINITION_FILE",
Short: "Upgrade CUE definition for version compatibility",
Long: "Upgrade CUE definition files to be compatible with a specific KubeVela version.\n" +
"This command automatically applies all necessary upgrades to ensure your definitions work with the target KubeVela version.\n" +
"If no version is specified, upgrades to the current CLI version.\n\n" +
"Currently supported upgrades:\n" +
"• List concatenation syntax compatibility\n" +
"• Import statement management\n" +
"• Template syntax modernization",
Example: "# Validate if definition needs upgrading (exit code 1 if upgrade needed)\n" +
"vela def upgrade my-definition.cue --validate\n\n" +
"# Silent validation for scripting (only exit code)\n" +
"vela def upgrade my-definition.cue --validate --quiet\n\n" +
"# Upgrade definition for current KubeVela version\n" +
"vela def upgrade my-definition.cue\n\n" +
"# Upgrade and save to specific file\n" +
"vela def upgrade my-definition.cue -o upgraded-definition.cue\n\n" +
"# Upgrade for specific KubeVela version\n" +
"vela def upgrade my-definition.cue --target-version=v1.11",
Annotations: map[string]string{
types.TagCommandType: types.TypeDefGeneration,
types.TagCommandOrder: "5",
},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
sourceFile := args[0]
// Read the source file
content, err := os.ReadFile(sourceFile) //nolint:gosec
if err != nil {
return fmt.Errorf("failed to read source file %s: %w", sourceFile, err)
}
// Prepare target version (strip 'v' prefix if present for consistency)
version := strings.TrimPrefix(targetVersion, "v")
// Check-only mode
if checkOnly {
var needsUpgrade bool
var reasons []string
if version != "" {
needsUpgrade, reasons, err = upgrade.RequiresUpgrade(string(content), version)
} else {
needsUpgrade, reasons, err = upgrade.RequiresUpgrade(string(content))
}
if err != nil {
return fmt.Errorf("failed to check upgrade requirements: %w", err)
}
if needsUpgrade {
if !quiet {
fmt.Fprintf(ioStreams.Out, "✗ Definition %s requires upgrade:\n", sourceFile)
for _, reason := range reasons {
fmt.Fprintf(ioStreams.Out, " - %s\n", reason)
}
}
os.Exit(1) // Non-zero exit code for scripts
} else if !quiet {
fmt.Fprintf(ioStreams.Out, "✓ Definition %s is up to date\n", sourceFile)
}
return nil
}
// Apply upgrades
var upgradedContent string
if version != "" {
upgradedContent, err = upgrade.Upgrade(string(content), version)
} else {
// Use default version (current KubeVela)
upgradedContent, err = upgrade.Upgrade(string(content))
}
if err != nil {
return fmt.Errorf("failed to upgrade CUE template: %w", err)
}
// Determine output destination
if outputFile != "" {
// Write to specified output file
if err := os.WriteFile(outputFile, []byte(upgradedContent), 0600); err != nil { //nolint:gosec
return fmt.Errorf("failed to write output file %s: %w", outputFile, err)
}
fmt.Fprintf(ioStreams.Out, "Successfully upgraded %s and saved to %s\n", sourceFile, outputFile)
} else {
// Write to stdout
fmt.Fprint(ioStreams.Out, upgradedContent)
}
return nil
},
}
cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file path. If not specified, outputs to stdout.")
cmd.Flags().StringVar(&targetVersion, "target-version", "", "Target KubeVela version (e.g., --target-version=v1.11). If not specified, uses current CLI version.")
cmd.Flags().BoolVar(&checkOnly, "validate", false, "Validate if definition needs upgrading without making changes (exit code 1 if upgrade required)")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Suppress output in validate mode, only return exit code")
return cmd
}