diff --git a/go.mod b/go.mod index 04b37359a..2ada1fe96 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,7 @@ require ( github.com/wonderflow/cert-manager-api v1.0.4-0.20210304051430-e08aa76f6c5f github.com/xanzy/go-gitlab v0.83.0 github.com/xlab/treeprint v1.2.0 + go.uber.org/multierr v1.7.0 go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.6.0 golang.org/x/oauth2 v0.6.0 @@ -287,7 +288,6 @@ require ( go.opentelemetry.io/proto/otlp v0.19.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.7.0 // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect diff --git a/references/cli/def.go b/references/cli/def.go index 62f54536a..7adf5e020 100644 --- a/references/cli/def.go +++ b/references/cli/def.go @@ -38,6 +38,7 @@ import ( crossplane "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime" "github.com/pkg/errors" "github.com/spf13/cobra" + "go.uber.org/multierr" "gopkg.in/yaml.v3" errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -57,6 +58,8 @@ import ( "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" ) const ( @@ -89,6 +92,7 @@ func DefinitionCommandGroup(c common.Args, order string, ioStreams util.IOStream NewDefinitionGenDocCommand(c, ioStreams), NewCapabilityShowCommand(c, "", ioStreams), NewDefinitionGenAPICommand(c), + NewDefinitionGenCUECommand(c), ) return cmd } @@ -1138,3 +1142,72 @@ func NewDefinitionGenAPICommand(c common.Args) *cobra.Command { return cmd } + +// NewDefinitionGenCUECommand create the `vela def gen-cue` command to help user generate CUE schema from the go code +func NewDefinitionGenCUECommand(_ common.Args) *cobra.Command { + const ( + typeProvider = "provider" + ) + + var ( + typ string + output 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\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\n" + + "# Generate CUE schema for provider type with custom output path\n" + + "> vela def gen-cue -t provider -o /path/to/myprovider.cue /path/to/myprovider.go\n", + 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) + } + + if output == "" { + output = strings.TrimSuffix(file, filepath.Ext(file)) + ".cue" + } + f, err := os.Create(filepath.Clean(output)) + if err != nil { + return err + } + defer multierr.AppendInvoke(&rerr, multierr.Close(f)) + + switch typ { + case typeProvider: + return providergen.Generate(providergen.Options{ + File: file, + Writer: f, + 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().StringVarP(&output, "output", "o", "", "Output CUE file path, if not specified, the CUE file will be generated in the same directory") + cmd.Flags().StringToStringVar(&typeMap, "types", map[string]string{}, "Special types to generate, format: =[any|ellipsis]. e.g. --types=*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured=ellipsis") + + return cmd +} diff --git a/references/cli/def_test.go b/references/cli/def_test.go index 8f3cd0924..002ba1b93 100644 --- a/references/cli/def_test.go +++ b/references/cli/def_test.go @@ -179,6 +179,22 @@ func removeDir(dirname string, t *testing.T) { } } +func compareFile(t *testing.T, got, expected string) { + gotBytes, err := os.ReadFile(got) + if err != nil { + t.Fatalf("failed to read file %s: %v", got, err) + } + + expectedBytes, err := os.ReadFile(expected) + if err != nil { + t.Fatalf("failed to read file %s: %v", expected, err) + } + + if !bytes.Equal(gotBytes, expectedBytes) { + t.Errorf("got %s, expected %s", gotBytes, expectedBytes) + } +} + func TestNewDefinitionCommandGroup(t *testing.T) { cmd := DefinitionCommandGroup(common2.Args{}, "", util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) initCommand(cmd) @@ -655,3 +671,28 @@ func TestNewDefinitionGenAPICommand(t *testing.T) { t.Fatalf("unexpeced error when executing genapi command: %v", err) } } + +func TestNewDefinitionGenCUECommand(t *testing.T) { + c := initArgs() + cmd := NewDefinitionGenCUECommand(c) + initCommand(cmd) + + // re-use the provider testdata + providerPath := "../cuegen/generators/provider/testdata" + output := filepath.Join(t.TempDir(), "output.cue") + expected := filepath.Join(providerPath, "valid.cue") + + cmd.SetArgs([]string{ + "-t", "provider", + "-o", output, + "--types", "*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured=ellipsis", + "--types", "*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.UnstructuredList=ellipsis", + filepath.Join(providerPath, "valid.go"), + }) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpeced error when executing gencue command: %v", err) + } + + compareFile(t, output, expected) +} diff --git a/references/cuegen/convert.go b/references/cuegen/convert.go index 7033f3621..375d10147 100644 --- a/references/cuegen/convert.go +++ b/references/cuegen/convert.go @@ -88,7 +88,7 @@ func (g *Generator) convert(typ gotypes.Type) (cueast.Expr, error) { case TypeEllipsis: return &cueast.StructLit{Elts: []cueast.Decl{&cueast.Ellipsis{}}}, nil default: - return nil, fmt.Errorf("unsupported special cue type %d", t) + return nil, fmt.Errorf("unsupported special cue type: %v", t) } } diff --git a/references/cuegen/option.go b/references/cuegen/option.go index fc0eb7731..05bf828d0 100644 --- a/references/cuegen/option.go +++ b/references/cuegen/option.go @@ -19,13 +19,13 @@ package cuegen import goast "go/ast" // Type is a special cue type -type Type int +type Type string const ( // TypeAny converts go type to _(top value) in cue - TypeAny Type = iota + TypeAny Type = "any" // TypeEllipsis converts go type to {...} in cue - TypeEllipsis + TypeEllipsis Type = "ellipsis" ) type options struct {