Skip to content

Commit

Permalink
feat: add workspace cli (#718)
Browse files Browse the repository at this point in the history
  • Loading branch information
healthjyk committed Dec 25, 2023
1 parent 61f7798 commit a91a525
Show file tree
Hide file tree
Showing 28 changed files with 1,235 additions and 5 deletions.
5 changes: 4 additions & 1 deletion pkg/cmd/cmd.go
Expand Up @@ -10,11 +10,13 @@ import (

"kusionstack.io/kusion/pkg/cmd/apply"
"kusionstack.io/kusion/pkg/cmd/build"
cmdinit "kusionstack.io/kusion/pkg/cmd/init"
"kusionstack.io/kusion/pkg/cmd/workspace"

// we need to import compile pkg to keep the compile command available
"kusionstack.io/kusion/pkg/cmd/compile" //nolint:staticcheck
"kusionstack.io/kusion/pkg/cmd/deps"
"kusionstack.io/kusion/pkg/cmd/destroy"
cmdinit "kusionstack.io/kusion/pkg/cmd/init"
"kusionstack.io/kusion/pkg/cmd/preview"
"kusionstack.io/kusion/pkg/cmd/version"
"kusionstack.io/kusion/pkg/util/i18n"
Expand Down Expand Up @@ -90,6 +92,7 @@ func NewKusionctlCmd(o KusionctlOptions) *cobra.Command {
{
Message: "Configuration Commands:",
Commands: []*cobra.Command{
workspace.NewCmd(),
cmdinit.NewCmdInit(),
compile.NewCmdCompile(),
build.NewCmdBuild(),
Expand Down
43 changes: 43 additions & 0 deletions pkg/cmd/workspace/cmd.go
@@ -0,0 +1,43 @@
package workspace

import (
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/templates"

"kusionstack.io/kusion/pkg/cmd/workspace/create"
"kusionstack.io/kusion/pkg/cmd/workspace/del"
"kusionstack.io/kusion/pkg/cmd/workspace/list"
"kusionstack.io/kusion/pkg/cmd/workspace/show"
"kusionstack.io/kusion/pkg/cmd/workspace/update"
"kusionstack.io/kusion/pkg/util/i18n"
)

func NewCmd() *cobra.Command {
var (
short = i18n.T(`Workspace is a logical concept representing a target that stacks will be deployed to`)

long = i18n.T(`
Workspace is a logical concept representing a target that stacks will be deployed to.
Workspace is managed by platform engineers, which contains a set of configurations that application developers do not want or should not concern, and is reused by multiple stacks belonging to different projects.`)
)

cmd := &cobra.Command{
Use: "workspace",
Short: short,
Long: templates.LongDesc(long),
SilenceErrors: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
}

createCmd := create.NewCmd()
updateCmd := update.NewCmd()
showCmd := show.NewCmd()
listCmd := list.NewCmd()
delCmd := del.NewCmd()
cmd.AddCommand(createCmd, updateCmd, showCmd, listCmd, delCmd)

return cmd
}
14 changes: 14 additions & 0 deletions pkg/cmd/workspace/cmd_test.go
@@ -0,0 +1,14 @@
package workspace

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewCmd(t *testing.T) {
t.Run("successfully get workspace help", func(t *testing.T) {
cmd := NewCmd()
assert.NotNil(t, cmd)
})
}
44 changes: 44 additions & 0 deletions pkg/cmd/workspace/create/cmd.go
@@ -0,0 +1,44 @@
package create

import (
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/templates"

"kusionstack.io/kusion/pkg/cmd/util"
"kusionstack.io/kusion/pkg/util/i18n"
)

func NewCmd() *cobra.Command {
var (
short = i18n.T(`Create a new workspace`)

long = i18n.T(`
This command creates a workspace with specified name and configuration file, where the file must be in the YAML format.`)

example = i18n.T(`
# Create a new workspace
kusion workspace create dev -f dev.yaml`)
)

o := NewOptions()
cmd := &cobra.Command{
Use: "create",
Short: short,
Long: templates.LongDesc(long),
Example: templates.Examples(example),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) (err error) {
defer util.RecoverErr(&err)
if err != nil {
return err
}
util.CheckErr(o.Complete(args))
util.CheckErr(o.Validate())
util.CheckErr(o.Run())
return
},
}

cmd.Flags().StringVarP(&o.FilePath, "file", "f", "", i18n.T("the path of workspace configuration file"))
return cmd
}
25 changes: 25 additions & 0 deletions pkg/cmd/workspace/create/cmd_test.go
@@ -0,0 +1,25 @@
package create

import (
"testing"

"github.com/bytedance/mockey"
"github.com/stretchr/testify/assert"
)

func TestNewCmd(t *testing.T) {
t.Run("successfully create workspace", func(t *testing.T) {
mockey.PatchConvey("mock cmd", t, func() {
mockey.Mock((*Options).Complete).To(func(o *Options, args []string) error {
o.Name = "dev"
o.FilePath = "dev.yaml"
return nil
}).Build()
mockey.Mock((*Options).Run).Return(nil).Build()

cmd := NewCmd()
err := cmd.Execute()
assert.Nil(t, err)
})
})
}
49 changes: 49 additions & 0 deletions pkg/cmd/workspace/create/options.go
@@ -0,0 +1,49 @@
package create

import (
"fmt"

"kusionstack.io/kusion/pkg/cmd/workspace/util"
"kusionstack.io/kusion/pkg/workspace"
)

type Options struct {
Name string
FilePath string
}

func NewOptions() *Options {
return &Options{}
}

func (o *Options) Complete(args []string) error {
name, err := util.GetNameFromArgs(args)
if err != nil {
return err
}
o.Name = name
return nil
}

func (o *Options) Validate() error {
if err := util.ValidateName(o.Name); err != nil {
return err
}
if err := util.ValidateFilePath(o.FilePath); err != nil {
return err
}
return nil
}

func (o *Options) Run() error {
ws, err := util.GetValidWorkspaceFromFile(o.FilePath, o.Name)
if err != nil {
return err
}

if err = workspace.CreateWorkspaceByDefaultOperator(ws); err != nil {
return err
}
fmt.Printf("create workspace %s successfully\n", o.Name)
return nil
}
117 changes: 117 additions & 0 deletions pkg/cmd/workspace/create/options_test.go
@@ -0,0 +1,117 @@
package create

import (
"reflect"
"testing"

"github.com/bytedance/mockey"
"github.com/stretchr/testify/assert"

v1 "kusionstack.io/kusion/pkg/apis/core/v1"
"kusionstack.io/kusion/pkg/cmd/workspace/util"
"kusionstack.io/kusion/pkg/workspace"
)

func TestOptions_Complete(t *testing.T) {
testcases := []struct {
name string
args []string
success bool
expectedOpts *Options
}{
{
name: "successfully complete options",
args: []string{"dev"},
success: true,
expectedOpts: &Options{Name: "dev"},
},
{
name: "complete field invalid args",
args: []string{"dev", "prod"},
success: false,
expectedOpts: nil,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
opts := NewOptions()
err := opts.Complete(tc.args)
assert.Equal(t, tc.success, err == nil)
if tc.success {
assert.True(t, reflect.DeepEqual(opts, tc.expectedOpts))
}
})
}
}

func TestOptions_Validate(t *testing.T) {
testcases := []struct {
name string
opts *Options
success bool
}{
{
name: "valid options",
opts: &Options{
Name: "dev",
FilePath: "dev.yaml",
},
success: true,
},
{
name: "invalid options empty name",
opts: &Options{
FilePath: "dev.yaml",
},
success: false,
},
{
name: "invalid options empty file path",
opts: &Options{
Name: "dev",
},
success: false,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := tc.opts.Validate()
assert.Equal(t, tc.success, err == nil)
})
}
}

func TestOptions_Run(t *testing.T) {
testcases := []struct {
name string
opts *Options
success bool
}{
{
name: "successfully run",
opts: &Options{
Name: "dev",
FilePath: "dev.yaml",
},
success: true,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
mockey.PatchConvey("mock create workspace", t, func() {
mockey.Mock(util.GetValidWorkspaceFromFile).
Return(&v1.Workspace{Name: "dev"}, nil).
Build()
mockey.Mock(workspace.CreateWorkspaceByDefaultOperator).
Return(nil).
Build()

err := tc.opts.Run()
assert.Equal(t, tc.success, err == nil)
})
})
}
}
42 changes: 42 additions & 0 deletions pkg/cmd/workspace/del/cmd.go
@@ -0,0 +1,42 @@
package del

import (
"github.com/spf13/cobra"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"

"kusionstack.io/kusion/pkg/cmd/util"
)

func NewCmd() *cobra.Command {
var (
short = i18n.T(`Delete a workspace`)

long = i18n.T(`
This command deletes a specified workspace.`)

example = i18n.T(`
# Delete a workspace
kusion workspace delete dev`)
)

o := NewOptions()
cmd := &cobra.Command{
Use: "delete",
Short: short,
Long: templates.LongDesc(long),
Example: templates.Examples(example),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) (err error) {
defer util.RecoverErr(&err)
if err != nil {
return err
}
util.CheckErr(o.Complete(args))
util.CheckErr(o.Validate())
util.CheckErr(o.Run())
return
},
}
return cmd
}
24 changes: 24 additions & 0 deletions pkg/cmd/workspace/del/cmd_test.go
@@ -0,0 +1,24 @@
package del

import (
"testing"

"github.com/bytedance/mockey"
"github.com/stretchr/testify/assert"
)

func TestNewCmd(t *testing.T) {
t.Run("successfully delete workspace", func(t *testing.T) {
mockey.PatchConvey("mock cmd", t, func() {
mockey.Mock((*Options).Complete).To(func(o *Options, args []string) error {
o.Name = "dev"
return nil
}).Build()
mockey.Mock((*Options).Run).Return(nil).Build()

cmd := NewCmd()
err := cmd.Execute()
assert.Nil(t, err)
})
})
}

0 comments on commit a91a525

Please sign in to comment.