kubevela/references/cli/def.go

1254 lines
50 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/workflow/pkg/cue/model/sets"
crossplane "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"gopkg.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/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"
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/definition/gen_sdk"
"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),
NewDefinitionDocGenCommand(c, ioStreams),
NewCapabilityShowCommand(c, "", ioStreams),
NewDefinitionGenAPICommand(c),
NewDefinitionGenCUECommand(c, ioStreams),
NewDefinitionGenDocCommand(c, ioStreams),
)
return cmd
}
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
}
// 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.",
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" +
"# 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),
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)
}
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 == "" {
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]
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 cue string")
}
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(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
}
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),
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'.",
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),
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),
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
}
// 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
func NewDefinitionRenderCommand(c common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "render DEFINITION.cue",
Short: "Render X-Definition.",
Long: "Render X-Definition with cue format into kubernetes YAML format. Could be used to check whether the cue format definition is working as expected. If a directory is used as input, all cue definitions in the directory will be rendered.",
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 my-webservice.cue and save it in my-webservice.yaml.\n" +
"> vela def render my-webservice.cue -o my-webservice.yaml" +
"# Command below will render all cue format definitions in the ./defs/cue/ directory and save the YAML objects in ./defs/yaml/.\n" +
"> vela def render ./defs/cue/ -o ./defs/yaml/",
Args: cobra.ExactArgs(1),
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)
}
render := func(inputFilename, outputFilename string) error {
cueBytes, err := utils.ReadRemoteOrLocalPath(inputFilename, false)
if err != nil {
return errors.Wrapf(err, "failed to get %s", args[0])
}
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
}
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 {
filename := filepath.Base(path)
fileSuffix := filepath.Ext(path)
if fileSuffix != CUEExtension {
return nil
}
inputFilenames = append(inputFilenames, path)
if output != "" {
outputFilenames = append(outputFilenames, filepath.Join(output, strings.ReplaceAll(filename, CUEExtension, YAMLExtension)))
} 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 YAML. 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 YAML file. For example, declaring author information.")
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",
Short: "Apply X-Definition.",
Long: "Apply X-Definition from local storage to kubernetes cluster. It will apply file to vela-system namespace by default.",
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 the local directory including all files(YAML and CUE definition) 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" +
"# 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" +
"# Apply a CUE from URL \n" +
"> vela def apply https://my-host-to-def/my-trait.cue --dry-run" +
"# Apply a CUE from stdin \n" +
"> vela def apply -",
Args: cobra.ExactArgs(1),
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
}
func defApplyAll(ctx context.Context, c common.Args, io util.IOStreams, namespace, path string, dryRun bool) error {
files, err := utils.LoadDataFromPath(ctx, path, utils.IsJSONYAMLorCUEFile)
if err != nil {
return errors.Wrapf(err, "failed to get from %s", path)
}
for _, f := range files {
result, err := defApplyOne(ctx, c, namespace, f.Path, f.Data, dryRun)
if err != nil {
return err
}
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 "", err
}
k8sClient, err := c.GetClient()
if err != nil {
return "", errors.Wrapf(err, "failed to get k8s client")
}
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 "", errors.Wrapf(err, "failed to parse YAML to definition")
}
if dryRun {
return "", 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 "", err
}
return 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 "", errors.Wrapf(err, "failed to parse CUE for definition")
}
def.SetNamespace(namespace)
}
if dryRun {
s, err := prettyYAMLMarshal(def.Object)
if err != nil {
return "", errors.Wrapf(err, "failed to marshal CRD into YAML")
}
return 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 "", errors.Wrapf(err, "failed to create new definition in kubernetes")
}
return fmt.Sprintf("%s %s created in namespace %s.\n", kind, def.GetName(), def.GetNamespace()), nil
}
return "", errors.Wrapf(err, "failed to check existence of target definition in kubernetes")
}
if err := oldDef.FromCUEString(string(defBytes), config); err != nil {
return "", errors.Wrapf(err, "failed to merge with existing definition")
}
if err = k8sClient.Update(ctx, &oldDef); err != nil {
return "", errors.Wrapf(err, "failed to update existing definition in kubernetes")
}
return fmt.Sprintf("%s %s in namespace %s updated.\n", oldDef.GetKind(), oldDef.GetName(), oldDef.GetNamespace()), 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),
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
}
// NewDefinitionValidateCommand create the `vela def vet` command to help user validate the definition
func NewDefinitionValidateCommand(c common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "vet DEFINITION.cue",
Short: "Validate X-Definition.",
Long: "Validate definition file by checking whether it has the valid cue format with fields set correctly\n" +
"* Currently, this command only checks the cue format. This function is still working in progress and we will support more functional validation mechanism in the future.",
Example: "# Command below will validate the my-def.cue file.\n" +
"> vela def vet my-def.cue\n" +
"# Validate every cue file provided\n" +
"> vela def vet my-def1.cue my-def2.cue my-def3.cue\n" +
"# Validate every cue file in the specified directories" +
"> vela def vet ./test1/ ./test2/",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
for _, arg := range args {
files, err := utils.LoadDataFromPath(cmd.Context(), arg, utils.IsCUEFile)
if err != nil {
return errors.Wrapf(err, "failed to get file from %s", arg)
}
for _, file := range files {
validateRes, err := validateSingleCueFile(file.Path, file.Data, c)
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "%s", validateRes)
}
}
return nil
},
}
return cmd
}
func validateSingleCueFile(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
}
// 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",
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",
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",
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
}