From 41181fbc399a191ae04413f827b926fd5dcb66e2 Mon Sep 17 00:00:00 2001 From: TonyAdo <71679464+adohe@users.noreply.github.com> Date: Sun, 14 Apr 2024 18:34:57 +0800 Subject: [PATCH] feat: adjust generate cmd behavior according to latest discussion (#1034) --- pkg/cmd/apply/options.go | 2 +- pkg/cmd/apply/options_test.go | 1 + pkg/cmd/build/builders/doc.go | 2 - pkg/cmd/generate/generate.go | 145 ++++++++++++++++-------- pkg/cmd/generate/generator/generator.go | 27 ++++- pkg/cmd/preview/options.go | 2 +- pkg/cmd/preview/options_test.go | 1 + 7 files changed, 124 insertions(+), 56 deletions(-) delete mode 100644 pkg/cmd/build/builders/doc.go diff --git a/pkg/cmd/apply/options.go b/pkg/cmd/apply/options.go index b56d2bfa..4e6e61cd 100644 --- a/pkg/cmd/apply/options.go +++ b/pkg/cmd/apply/options.go @@ -82,7 +82,7 @@ func (o *Options) Run() error { if len(o.IntentFile) != 0 { spec, err = generate.SpecFromFile(o.IntentFile) } else { - spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, true) + spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, nil, true) } if err != nil { return err diff --git a/pkg/cmd/apply/options_test.go b/pkg/cmd/apply/options_test.go index 86cdc82d..6ae8962e 100644 --- a/pkg/cmd/apply/options_test.go +++ b/pkg/cmd/apply/options_test.go @@ -83,6 +83,7 @@ func mockGenerateSpecWithSpinner() { project *apiv1.Project, stack *apiv1.Stack, workspace *apiv1.Workspace, + parameters map[string]string, noStyle bool, ) (*apiv1.Spec, error) { return &apiv1.Spec{Resources: []apiv1.Resource{sa1, sa2, sa3}}, nil diff --git a/pkg/cmd/build/builders/doc.go b/pkg/cmd/build/builders/doc.go deleted file mode 100644 index de801fe8..00000000 --- a/pkg/cmd/build/builders/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package intent contains functions to build the Intent in operation commands. It should only be used in `pkg/cmd` -package builders diff --git a/pkg/cmd/generate/generate.go b/pkg/cmd/generate/generate.go index 12b541f9..d2a1a97e 100644 --- a/pkg/cmd/generate/generate.go +++ b/pkg/cmd/generate/generate.go @@ -1,3 +1,17 @@ +// Copyright 2024 KusionStack 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 generate import ( @@ -5,39 +19,34 @@ import ( "fmt" "io" "os" + "strings" "github.com/pterm/pterm" "github.com/spf13/cobra" yamlv3 "gopkg.in/yaml.v3" "k8s.io/cli-runtime/pkg/genericiooptions" - "kcl-lang.io/kpm/pkg/api" v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" - "kusionstack.io/kusion/pkg/backend" "kusionstack.io/kusion/pkg/cmd/generate/generator" "kusionstack.io/kusion/pkg/cmd/generate/run" "kusionstack.io/kusion/pkg/cmd/meta" cmdutil "kusionstack.io/kusion/pkg/cmd/util" - "kusionstack.io/kusion/pkg/engine/spec" "kusionstack.io/kusion/pkg/util/i18n" "kusionstack.io/kusion/pkg/util/pretty" ) var ( generateLong = i18n.T(` - Generate versioned Spec of target Stack. - - The command must be executed in a Stack or by specifying a Stack directory with the -w flag.`) + This command generates Spec resources with given values, then write the resulting Spec resources to specific output file or stdout. + + The nearest parent folder containing a stack.yaml file is loaded from the project in the current directory.`) generateExample = i18n.T(` - # Generate spec with working directory - kusion generate -w /path/to/stack + # Generate and write Spec resources to specific output file + kusion generate -o /tmp/spec.yaml # Generate spec with custom workspace - kusion generate -w /path/to/stack --workspace dev - - # Generate spec with custom backend - kusion generate -w /path/to/stack --backend oss`) + kusion generate -o /tmp/spec.yaml --workspace dev`) ) // GenerateFlags directly reflect the information that CLI is gathering via flags. They will be converted to @@ -45,9 +54,11 @@ var ( // // This structure reduces the transformation to wiring and makes the logic itself easy to unit test. type GenerateFlags struct { - WorkDir string MetaFlags *meta.MetaFlags + Output string + Values []string + genericiooptions.IOStreams } @@ -55,9 +66,10 @@ type GenerateFlags struct { type GenerateOptions struct { *meta.MetaOptions - WorkDir string + Output string + Values []string - SpecStorage spec.Storage + genericiooptions.IOStreams } // NewGenerateFlags returns a default GenerateFlags @@ -73,8 +85,8 @@ func NewCmdGenerate(ioStreams genericiooptions.IOStreams) *cobra.Command { flags := NewGenerateFlags(ioStreams) cmd := &cobra.Command{ - Use: "generate (-w DIRECTORY)", - Short: "Generate versioned Spec of target Stack", + Use: "generate", + Short: "Generate and print the resulting Spec resources of target Stack", Long: generateLong, Example: generateExample, RunE: func(cmd *cobra.Command, args []string) (err error) { @@ -97,38 +109,24 @@ func (flags *GenerateFlags) AddFlags(cmd *cobra.Command) { // bind flag structs flags.MetaFlags.AddFlags(cmd) - cmd.Flags().StringVarP(&flags.WorkDir, "workdir", "w", flags.WorkDir, i18n.T("The working directory for generate (default is current dir where executed).")) + cmd.Flags().StringVarP(&flags.Output, "output", "o", flags.Output, i18n.T("File to write generated Spec resources to")) + cmd.Flags().StringArrayVar(&flags.Values, "set", []string{}, i18n.T("Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")) } // ToOptions converts from CLI inputs to runtime inputs. func (flags *GenerateFlags) ToOptions() (*GenerateOptions, error) { - // If working directory not specified, use current dir where executed - workDir := flags.WorkDir - if len(workDir) == 0 { - workDir, _ = os.Getwd() - } - // Convert meta options metaOptions, err := flags.MetaFlags.ToOptions() if err != nil { return nil, err } - // Get target spec storage - specStorage, err := backend.NewSpecStorage( - *flags.MetaFlags.Backend, - metaOptions.RefProject.Name, - metaOptions.RefStack.Name, - metaOptions.RefWorkspace.Name, - ) - if err != nil { - return nil, err - } - o := &GenerateOptions{ - WorkDir: workDir, MetaOptions: metaOptions, - SpecStorage: specStorage, + Output: flags.Output, + Values: flags.Values, + + IOStreams: flags.IOStreams, } return o, nil @@ -140,20 +138,51 @@ func (o *GenerateOptions) Validate(cmd *cobra.Command, args []string) error { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) } + for _, value := range o.Values { + if parts := strings.SplitN(value, "=", 2); len(parts) != 2 { + return cmdutil.UsageErrorf(cmd, "value %s is invalid format", value) + } + } + return nil } // Run executes the `generate` command. func (o *GenerateOptions) Run() error { - versionedSpec, err := GenerateSpecWithSpinner(o.RefProject, o.RefStack, o.RefWorkspace, true) + // build parameters + parameters := o.buildParameters() + + // call default generator to generate Spec + spec, err := GenerateSpecWithSpinner(o.RefProject, o.RefStack, o.RefWorkspace, parameters, true) if err != nil { return err } - return o.SpecStorage.Apply(versionedSpec) + + // write Spec to output file or a writer + return write(spec, o.Output, o.Out) } -// GenerateSpecWithSpinner calls generator to generate versioned Spec. Add a method wrapper for testing purposes. -func GenerateSpecWithSpinner(project *v1.Project, stack *v1.Stack, workspace *v1.Workspace, noStyle bool) (*v1.Spec, error) { +// buildParameters builds parameters with given values. +func (o *GenerateOptions) buildParameters() map[string]string { + parameters := make(map[string]string) + + for _, value := range o.Values { + parts := strings.SplitN(value, "=", 2) + parameters[parts[0]] = parts[1] + } + + return parameters +} + +// GenerateSpecWithSpinner calls generator to generate versioned Spec. +// Add a method wrapper for testing purposes. +func GenerateSpecWithSpinner( + project *v1.Project, + stack *v1.Stack, + workspace *v1.Workspace, + parameters map[string]string, + noStyle bool, +) (*v1.Spec, error) { // Construct generator instance defaultGenerator := &generator.DefaultGenerator{ Project: project, @@ -162,12 +191,6 @@ func GenerateSpecWithSpinner(project *v1.Project, stack *v1.Stack, workspace *v1 Runner: &run.KPMRunner{}, } - kclPkg, err := api.GetKclPackage(stack.Path) - if err != nil { - return nil, err - } - defaultGenerator.KclPkg = kclPkg - var sp *pterm.SpinnerPrinter if noStyle { fmt.Printf("Generating Spec in the Stack %s...\n", stack.Name) @@ -179,7 +202,7 @@ func GenerateSpecWithSpinner(project *v1.Project, stack *v1.Stack, workspace *v1 // style means color and prompt here. Currently, sp will be nil only when o.NoStyle is true style := !noStyle && sp != nil - versionedSpec, err := defaultGenerator.Generate(stack.Path, nil) + versionedSpec, err := defaultGenerator.Generate(stack.Path, parameters) if err != nil { if style { sp.Fail() @@ -216,3 +239,29 @@ func SpecFromFile(filePath string) (*v1.Spec, error) { } return i, nil } + +// write writes Spec resources to a file or a writer. +func write(spec *v1.Spec, output string, out io.Writer) error { + specStr, err := yamlv3.Marshal(spec) + if err != nil { + return err + } + + switch { + case output == "": + _, err := fmt.Fprintln(out, specStr) + return err + default: + return dumpToFile(string(specStr), output) + } +} + +func dumpToFile(specStr string, filepath string) error { + f, err := os.Create(filepath) + if err != nil { + return fmt.Errorf("opening file for writing Spec: %w", err) + } + defer f.Close() + _, err = f.WriteString(specStr + "\n") + return err +} diff --git a/pkg/cmd/generate/generator/generator.go b/pkg/cmd/generate/generator/generator.go index 5019b0d3..8e24f30d 100644 --- a/pkg/cmd/generate/generator/generator.go +++ b/pkg/cmd/generate/generator/generator.go @@ -1,3 +1,17 @@ +// Copyright 2024 KusionStack 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 generator import ( @@ -21,7 +35,7 @@ import ( ) // Generator is an interface for things that can generate versioned Spec from -// configuration code under current working directory and given input parameters. +// configuration code under current working directory with given input parameters. type Generator interface { // Generate creates versioned Intent given working directory and set of parameters Generate(workDir string, params map[string]string) (*v1.Spec, error) @@ -32,8 +46,8 @@ type DefaultGenerator struct { Project *v1.Project Stack *v1.Stack Workspace *v1.Workspace - Runner run.CodeRunner - KclPkg *api.KclPackage + + Runner run.CodeRunner } // Generate versioned Spec with target code runner. @@ -61,11 +75,16 @@ func (g *DefaultGenerator) Generate(workDir string, params map[string]string) (* return nil, err } + kclPkg, err := api.GetKclPackage(g.Stack.Path) + if err != nil { + return nil, err + } + builder := &builders.AppsConfigBuilder{ Workspace: g.Workspace, Apps: apps, } - return builder.Build(g.KclPkg, g.Project, g.Stack) + return builder.Build(kclPkg, g.Project, g.Stack) } // copyDependentModules copies dependent Kusion modules' generators to destination. diff --git a/pkg/cmd/preview/options.go b/pkg/cmd/preview/options.go index 85321c20..76810b7e 100644 --- a/pkg/cmd/preview/options.go +++ b/pkg/cmd/preview/options.go @@ -139,7 +139,7 @@ func (o *Options) Run() error { if len(o.IntentFile) != 0 { spec, err = generate.SpecFromFile(o.IntentFile) } else { - spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, true) + spec, err = generate.GenerateSpecWithSpinner(currentProject, currentStack, currentWorkspace, nil, true) } if err != nil { return err diff --git a/pkg/cmd/preview/options_test.go b/pkg/cmd/preview/options_test.go index a5cbb697..de352f94 100644 --- a/pkg/cmd/preview/options_test.go +++ b/pkg/cmd/preview/options_test.go @@ -232,6 +232,7 @@ func mockGenerateIntentWithSpinner() { project *apiv1.Project, stack *apiv1.Stack, workspace *apiv1.Workspace, + parameters map[string]string, noStyle bool, ) (*apiv1.Spec, error) { return &apiv1.Spec{Resources: []apiv1.Resource{sa1, sa2, sa3}}, nil