diff --git a/pkg/cmd/apply/options.go b/pkg/cmd/apply/options.go index 8ef60f45..b56d2bfa 100644 --- a/pkg/cmd/apply/options.go +++ b/pkg/cmd/apply/options.go @@ -58,7 +58,7 @@ func (o *Options) Run() error { } // parse project and stack of work directory - currentProject, currentStack, err := project.DetectProjectAndStack(o.Options.WorkDir) + currentProject, currentStack, err := project.DetectProjectAndStackFrom(o.Options.WorkDir) if err != nil { return err } diff --git a/pkg/cmd/apply/options_test.go b/pkg/cmd/apply/options_test.go index 5c3a08ce..86cdc82d 100644 --- a/pkg/cmd/apply/options_test.go +++ b/pkg/cmd/apply/options_test.go @@ -71,7 +71,7 @@ var ( ) func mockPatchDetectProjectAndStack() *mockey.Mocker { - return mockey.Mock(project.DetectProjectAndStack).To(func(stackDir string) (*apiv1.Project, *apiv1.Stack, error) { + return mockey.Mock(project.DetectProjectAndStackFrom).To(func(stackDir string) (*apiv1.Project, *apiv1.Stack, error) { proj.Path = stackDir stack.Path = stackDir return proj, stack, nil diff --git a/pkg/cmd/destroy/options.go b/pkg/cmd/destroy/options.go index 03b99682..13e1d483 100644 --- a/pkg/cmd/destroy/options.go +++ b/pkg/cmd/destroy/options.go @@ -52,7 +52,7 @@ func (o *Options) Run() error { // listen for interrupts or the SIGTERM signal signals.HandleInterrupt() // parse project and stack of work directory - proj, stack, err := project.DetectProjectAndStack(o.Options.WorkDir) + proj, stack, err := project.DetectProjectAndStackFrom(o.Options.WorkDir) if err != nil { return err } diff --git a/pkg/cmd/destroy/options_test.go b/pkg/cmd/destroy/options_test.go index dda9a457..318d41be 100644 --- a/pkg/cmd/destroy/options_test.go +++ b/pkg/cmd/destroy/options_test.go @@ -80,7 +80,7 @@ var ( ) func mockDetectProjectAndStack() { - mockey.Mock(project.DetectProjectAndStack).To(func(stackDir string) (*apiv1.Project, *apiv1.Stack, error) { + mockey.Mock(project.DetectProjectAndStackFrom).To(func(stackDir string) (*apiv1.Project, *apiv1.Stack, error) { proj.Path = stackDir stack.Path = stackDir return proj, stack, nil diff --git a/pkg/cmd/generate/generate.go b/pkg/cmd/generate/generate.go index 2ad4c588..cf5533f1 100644 --- a/pkg/cmd/generate/generate.go +++ b/pkg/cmd/generate/generate.go @@ -16,9 +16,9 @@ import ( "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/project" "kusionstack.io/kusion/pkg/util/i18n" "kusionstack.io/kusion/pkg/util/pretty" ) @@ -46,8 +46,7 @@ var ( // This structure reduces the transformation to wiring and makes the logic itself easy to unit test. type GenerateFlags struct { WorkDir string - Backend string - Workspace string + MetaFlags *meta.MetaFlags genericiooptions.IOStreams } @@ -56,17 +55,15 @@ type GenerateFlags struct { type GenerateOptions struct { WorkDir string - Project *v1.Project - Stack *v1.Stack - Workspace *v1.Workspace + MetaOptions *meta.MetaOptions SpecStorage spec.Storage - Generator generator.Generator } // NewGenerateFlags returns a default GenerateFlags func NewGenerateFlags(streams genericiooptions.IOStreams) *GenerateFlags { return &GenerateFlags{ + MetaFlags: meta.NewMetaFlags(), IOStreams: streams, } } @@ -97,9 +94,10 @@ func NewCmdGenerate(ioStreams genericiooptions.IOStreams) *cobra.Command { // AddFlags registers flags for a cli. 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.Backend, "backend", "", flags.Backend, i18n.T("The backend to use, supports 'local', 'oss' and 's3'.")) - cmd.Flags().StringVarP(&flags.Workspace, "workspace", "", flags.Workspace, i18n.T("The name of target workspace to operate in.")) } // ToOptions converts from CLI inputs to runtime inputs. @@ -110,35 +108,27 @@ func (flags *GenerateFlags) ToOptions() (*GenerateOptions, error) { workDir, _ = os.Getwd() } - // Parse project and currentStack of work directory - currentProject, currentStack, err := project.DetectProjectAndStack(workDir) - if err != nil { - return nil, err - } - - // Get current workspace from backend - workspaceStorage, err := backend.NewWorkspaceStorage(flags.Backend) - if err != nil { - return nil, err - } - currentWorkspace, err := workspaceStorage.Get(flags.Workspace) + // Convert meta options + metaOptions, err := flags.MetaFlags.ToOptions() if err != nil { return nil, err } // Get target spec storage - specStorage, err := backend.NewSpecStorage(flags.Backend, currentProject.Name, currentStack.Name, flags.Workspace) + 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, - Project: currentProject, - Stack: currentStack, - Workspace: currentWorkspace, + MetaOptions: metaOptions, SpecStorage: specStorage, - Generator: nil, } return o, nil @@ -155,11 +145,16 @@ func (o *GenerateOptions) Validate(cmd *cobra.Command, args []string) error { // Run executes the `generate` command. func (o *GenerateOptions) Run() error { - spec, err := GenerateSpecWithSpinner(o.Project, o.Stack, o.Workspace, true) + versionedSpec, err := GenerateSpecWithSpinner( + o.MetaOptions.RefProject, + o.MetaOptions.RefStack, + o.MetaOptions.RefWorkspace, + true, + ) if err != nil { return err } - return o.SpecStorage.Apply(spec) + return o.SpecStorage.Apply(versionedSpec) } // GenerateSpecWithSpinner calls generator to generate versioned Spec. Add a method wrapper for testing purposes. @@ -189,7 +184,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 - spec, err := defaultGenerator.Generate(stack.Path, nil) + versionedSpec, err := defaultGenerator.Generate(stack.Path, nil) if err != nil { if style { sp.Fail() @@ -206,7 +201,7 @@ func GenerateSpecWithSpinner(project *v1.Project, stack *v1.Stack, workspace *v1 fmt.Println() } - return spec, nil + return versionedSpec, nil } func SpecFromFile(filePath string) (*v1.Spec, error) { diff --git a/pkg/cmd/meta/meta.go b/pkg/cmd/meta/meta.go new file mode 100644 index 00000000..45cbd40f --- /dev/null +++ b/pkg/cmd/meta/meta.go @@ -0,0 +1,99 @@ +// 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 meta + +import ( + "github.com/spf13/cobra" + + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" + "kusionstack.io/kusion/pkg/backend" + "kusionstack.io/kusion/pkg/project" + "kusionstack.io/kusion/pkg/util/i18n" +) + +// MetaFlags directly reflect the information that CLI is gathering via flags. They will be converted to +// MetaOptions, which reflect the runtime requirements for the command. +// +// This structure reduces the transformation to wiring and makes the logic itself easy to unit test. +type MetaFlags struct { + Application *string + Workspace *string + + Backend *string +} + +// MetaOptions are the meta-options that are available on all or most commands. +type MetaOptions struct { + // RefProject references the project for this CLI invocation. + RefProject *v1.Project + + // RefStack referenced the stack for this CLI invocation + RefStack *v1.Stack + + // RefWorkspace referenced the workspace for this CLI invocation + RefWorkspace *v1.Workspace +} + +// NewMetaFlags provides default flags and values for use in other commands. +func NewMetaFlags() *MetaFlags { + application := "" + workspace := "" + backend := "" + + return &MetaFlags{ + Application: &application, + Workspace: &workspace, + Backend: &backend, + } +} + +// AddFlags registers flags for a cli. +func (f *MetaFlags) AddFlags(cmd *cobra.Command) { + if f.Workspace != nil { + cmd.Flags().StringVarP(f.Workspace, "workspace", "", *f.Workspace, i18n.T("The name of target workspace to operate in.")) + } + if f.Backend != nil { + cmd.Flags().StringVarP(f.Backend, "backend", "", *f.Backend, i18n.T("The backend to use, supports 'local', 'oss' and 's3'.")) + } +} + +// ToOptions converts MetaFlags to MetaOptions. +func (f *MetaFlags) ToOptions() (*MetaOptions, error) { + opts := &MetaOptions{} + + // Parse project and currentStack of work directory + refProject, refStack, err := project.DetectProjectAndStacks() + if err != nil { + return nil, err + } + + opts.RefProject = refProject + opts.RefStack = refStack + + // Get current workspace from backend + if f.Backend != nil && f.Workspace != nil { + workspaceStorage, err := backend.NewWorkspaceStorage(*f.Backend) + if err != nil { + return nil, err + } + refWorkspace, err := workspaceStorage.Get(*f.Workspace) + if err != nil { + return nil, err + } + opts.RefWorkspace = refWorkspace + } + + return opts, nil +} diff --git a/pkg/cmd/preview/options.go b/pkg/cmd/preview/options.go index 30fbe7e6..85321c20 100644 --- a/pkg/cmd/preview/options.go +++ b/pkg/cmd/preview/options.go @@ -115,7 +115,7 @@ func (o *Options) Run() error { } // Parse project and currentStack of work directory - currentProject, currentStack, err := project.DetectProjectAndStack(o.WorkDir) + currentProject, currentStack, err := project.DetectProjectAndStackFrom(o.WorkDir) if err != nil { return err } diff --git a/pkg/cmd/preview/options_test.go b/pkg/cmd/preview/options_test.go index 04df5c2e..a5cbb697 100644 --- a/pkg/cmd/preview/options_test.go +++ b/pkg/cmd/preview/options_test.go @@ -220,7 +220,7 @@ func newSA(name string) apiv1.Resource { } func mockDetectProjectAndStack() { - mockey.Mock(project.DetectProjectAndStack).To(func(stackDir string) (*apiv1.Project, *apiv1.Stack, error) { + mockey.Mock(project.DetectProjectAndStackFrom).To(func(stackDir string) (*apiv1.Project, *apiv1.Stack, error) { proj.Path = stackDir stack.Path = stackDir return proj, stack, nil diff --git a/pkg/project/paths.go b/pkg/project/paths.go index 928f2c92..45110377 100644 --- a/pkg/project/paths.go +++ b/pkg/project/paths.go @@ -27,8 +27,19 @@ const ( StackFile = "stack.yaml" ) -// DetectProjectAndStack try to get stack and project from given path -func DetectProjectAndStack(stackDir string) (p *v1.Project, s *v1.Stack, err error) { +// DetectProjectAndStacks locates the closest project and stack from the current working directory, +// or an error if not found. +func DetectProjectAndStacks() (*v1.Project, *v1.Stack, error) { + dir, err := os.Getwd() + if err != nil { + return nil, nil, err + } + + return DetectProjectAndStackFrom(dir) +} + +// DetectProjectAndStackFrom try to get stack and project from given path +func DetectProjectAndStackFrom(stackDir string) (p *v1.Project, s *v1.Stack, err error) { stackDir, err = filepath.Abs(stackDir) if err != nil { return nil, nil, err diff --git a/pkg/project/paths_test.go b/pkg/project/paths_test.go index 3044fefd..e82bc4d6 100644 --- a/pkg/project/paths_test.go +++ b/pkg/project/paths_test.go @@ -205,17 +205,17 @@ func TestDetectProjectAndStack(t *testing.T) { for _, tt := range tests { mockey.PatchConvey(tt.name, t, func() { tt.preRun() - project, stack, err := DetectProjectAndStack(tt.args.stackDir) + project, stack, err := DetectProjectAndStackFrom(tt.args.stackDir) tt.postRun() if (err != nil) != tt.wantErr { - t.Errorf("DetectProjectAndStack() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("DetectProjectAndStackFrom() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(project, tt.project) { - t.Errorf("DetectProjectAndStack() got = %v, want %v", project, tt.project) + t.Errorf("DetectProjectAndStackFrom() got = %v, want %v", project, tt.project) } if !reflect.DeepEqual(stack, tt.stack) { - t.Errorf("DetectProjectAndStack() gosuccess = %v, want %v", stack, tt.stack) + t.Errorf("DetectProjectAndStackFrom() gosuccess = %v, want %v", stack, tt.stack) } }) }