Skip to content

Commit

Permalink
feat: add kusion release show command (#1265)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoangndst authored Aug 19, 2024
1 parent f2ee452 commit c29980d
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 1 deletion.
2 changes: 1 addition & 1 deletion pkg/cmd/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewCmdRel(streams genericiooptions.IOStreams) *cobra.Command {
Run: cmdutil.DefaultSubCommandRun(streams.ErrOut),
}

cmd.AddCommand(NewCmdUnlock(streams), NewCmdList(streams))
cmd.AddCommand(NewCmdUnlock(streams), NewCmdList(streams), NewCmdShow(streams))

return cmd
}
232 changes: 232 additions & 0 deletions pkg/cmd/release/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package rel

import (
"encoding/json"
"fmt"

"gopkg.in/yaml.v3"

v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"

"k8s.io/cli-runtime/pkg/genericiooptions"

"k8s.io/kubectl/pkg/util/templates"

"kusionstack.io/kusion/pkg/backend"
"kusionstack.io/kusion/pkg/engine/release"
"kusionstack.io/kusion/pkg/project"
"kusionstack.io/kusion/pkg/util/i18n"

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

"github.com/spf13/cobra"
)

var (
showShort = i18n.T("Show details of a release of the current or specified stack")

showLong = i18n.T(`
Show details of a release of the current or specified stack.
This command displays detailed information about a release of the current project in the current or a specified workspace
`)

showExample = i18n.T(`
# Show details of the latest release of the current project in the current workspace
kusion release show
# Show details of a specific release of the current project in the current workspace
kusion release show --revision=1
# Show details of a specific release of the specified project in the specified workspace
kusion release show --revision=1 --project=hoangndst --workspace=dev
# Show details of the latest release with specified backend
kusion release show --backend=local
# Show details of the latest release with specified output format
kusion release show --output=json
`)
)

const jsonOutput = "json"

// ShowFlags reflects the information that CLI is gathering via flags,
// which will be converted into ShowOptions.
type ShowFlags struct {
Revision *uint64
Project *string
Workspace *string
Backend *string
Output string
}

// ShowOptions defines the configuration parameters for the `kusion release show` command.
type ShowOptions struct {
Revision *uint64
Project *string
Workspace *string
ReleaseStorage release.Storage
Output string
}

// NewShowFlags returns a default ShowFlags.
func NewShowFlags(_ genericiooptions.IOStreams) *ShowFlags {
revision := uint64(0)
workspace := ""
projectName := ""
backendName := ""
output := ""
return &ShowFlags{
Revision: &revision,
Project: &projectName,
Workspace: &workspace,
Backend: &backendName,
Output: output,
}
}

// NewCmdShow creates the `kusion release show` command.
func NewCmdShow(streams genericiooptions.IOStreams) *cobra.Command {
flags := NewShowFlags(streams)

cmd := &cobra.Command{
Use: "show",
Short: showShort,
Long: templates.LongDesc(showLong),
Example: templates.Examples(showExample),
RunE: func(cmd *cobra.Command, args []string) (err error) {
o, err := flags.ToOptions()
defer cmdutil.RecoverErr(&err)
cmdutil.CheckErr(err)
cmdutil.CheckErr(o.Validate(cmd, args))
cmdutil.CheckErr(o.Run())

return
},
}

flags.AddFlags(cmd)

return cmd
}

// AddFlags adds flags for a ShowOptions struct to the specified command.
func (f *ShowFlags) AddFlags(cmd *cobra.Command) {
if f.Revision != nil {
cmd.Flags().Uint64VarP(f.Revision, "revision", "", 0, i18n.T("The revision number of the release"))
}
if f.Project != nil {
cmd.Flags().StringVarP(f.Project, "project", "", "", i18n.T("The project name"))
}
if f.Workspace != nil {
cmd.Flags().StringVarP(f.Workspace, "workspace", "", "", i18n.T("The workspace name"))
}
if f.Backend != nil {
cmd.Flags().StringVarP(f.Backend, "backend", "", "", i18n.T("The backend to use, supports 'local', 'oss' and 's3'"))
}
cmd.Flags().StringVarP(&f.Output, "output", "o", f.Output, i18n.T("Specify the output format"))
}

// ToOptions converts ShowFlags to ShowOptions.
func (f *ShowFlags) ToOptions() (*ShowOptions, error) {
var storageBackend backend.Backend
var err error
if f.Backend != nil && *f.Backend != "" {
storageBackend, err = backend.NewBackend(*f.Backend)
if err != nil {
return nil, err
}
} else {
storageBackend, err = backend.NewBackend("")
if err != nil {
return nil, err
}
}

workspaceName := ""
projectName := ""

workspaceStorage, err := storageBackend.WorkspaceStorage()
if err != nil {
return nil, err
}
if f.Workspace != nil && *f.Workspace != "" {
refWorkspace, err := workspaceStorage.Get(*f.Workspace)
if err != nil {
return nil, err
}
workspaceName = refWorkspace.Name
} else {
currentWorkspace, err := workspaceStorage.GetCurrent()
if err != nil {
return nil, err
}
workspaceName = currentWorkspace
}

if f.Project != nil && *f.Project != "" {
projectName = *f.Project
} else {
currentProject, _, err := project.DetectProjectAndStacks()
if err != nil {
return nil, err
}
projectName = currentProject.Name
}
storage, err := storageBackend.ReleaseStorage(projectName, workspaceName)
if err != nil {
return nil, err
}

return &ShowOptions{
Revision: f.Revision,
Output: f.Output,
Project: &projectName,
Workspace: &workspaceName,
ReleaseStorage: storage,
}, nil
}

// Validate checks the provided options for the `kusion release show` command.
func (o *ShowOptions) Validate(cmd *cobra.Command, args []string) error {
if len(args) != 0 {
return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
}

return nil
}

// Run executes the `kusion release show` command.
func (o *ShowOptions) Run() (err error) {
var rel *v1.Release
if o.Revision != nil && *o.Revision != 0 {
rel, err = o.ReleaseStorage.Get(*o.Revision)
if err != nil {
fmt.Printf("No release found for revision %d of project: %s, workspace: %s\n",
*o.Revision, *o.Project, *o.Workspace)
return err
}
} else {
rel, err = o.ReleaseStorage.Get(o.ReleaseStorage.GetLatestRevision())
if err != nil {
fmt.Printf("No release found for project: %s, workspace: %s\n",
*o.Project, *o.Workspace)
return err
}
}
if o.Output == jsonOutput {
data, err := json.MarshalIndent(rel, "", " ")
if err != nil {
return err
}
fmt.Println(string(data))
} else {
data, err := yaml.Marshal(rel)
if err != nil {
return err
}
fmt.Println(string(data))
}
return nil
}
132 changes: 132 additions & 0 deletions pkg/cmd/release/show_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package rel

import (
"fmt"
"testing"

"github.com/bytedance/mockey"
"github.com/stretchr/testify/assert"
"k8s.io/cli-runtime/pkg/genericiooptions"
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
"kusionstack.io/kusion/pkg/backend"
"kusionstack.io/kusion/pkg/engine/release"
"kusionstack.io/kusion/pkg/project"
"kusionstack.io/kusion/pkg/workspace"
)

func TestShowFlags_ToOptions(t *testing.T) {
streams := genericiooptions.IOStreams{}

f := NewShowFlags(streams)

t.Run("Successful Option Creation", func(t *testing.T) {
mockey.PatchConvey("mock detect project and stack", t, func() {
mockey.Mock(project.DetectProjectAndStackFrom).Return(&v1.Project{
Name: "mock-project",
}, &v1.Stack{
Name: "mock-stack",
}, nil).Build()
_, err := f.ToOptions()
assert.NoError(t, err)
})
})

t.Run("Failed Option Creation Due to Invalid Backend", func(t *testing.T) {
s := "invalid-backend"
f.Backend = &s
_, err := f.ToOptions()
assert.Error(t, err)
})
}

func TestShowOptions_Validate(t *testing.T) {
opts := &ShowOptions{}
streams := genericiooptions.IOStreams{}
cmd := NewCmdShow(streams)

t.Run("Valid Args", func(t *testing.T) {
err := opts.Validate(cmd, []string{})
assert.NoError(t, err)
})

t.Run("Invalid Args", func(t *testing.T) {
err := opts.Validate(cmd, []string{"invalid-args"})
assert.Error(t, err)
})
}

func TestShowOptions_Run(t *testing.T) {
revisions := uint64(1)
projectName := "mock-project"
workspaceName := "mock-workspace"
opts := &ShowOptions{
Revision: &revisions,
Project: &projectName,
Workspace: &workspaceName,
ReleaseStorage: &fakeStorageShow{},
}

t.Run("Successfully show the latest release", func(t *testing.T) {
mockey.PatchConvey("mock release getter", t, func() {
mockey.Mock((*fakeStorageShow).Get).
Return(&v1.Release{
Project: "mock-project",
Workspace: "mock-workspace",
Revision: 1,
}, nil).Build()

err := opts.Run()
assert.NoError(t, err)
})
})

t.Run("Failed to show the latest release", func(t *testing.T) {
mockey.PatchConvey("mock release getter", t, func() {
mockey.Mock((*fakeStorageShow).Get).
Return(nil, fmt.Errorf("release does not exist")).Build()

err := opts.Run()
assert.ErrorContains(t, err, "release does not exist")
})
})
}

var _ backend.Backend = (*fakeBackendShow)(nil)

type fakeBackendShow struct{}

func (f *fakeBackendShow) WorkspaceStorage() (workspace.Storage, error) {
return nil, nil
}

func (f *fakeBackendShow) ReleaseStorage(_, _ string) (release.Storage, error) {
return nil, nil
}

var _ release.Storage = (*fakeStorageShow)(nil)

type fakeStorageShow struct{}

func (f *fakeStorageShow) Get(_ uint64) (*v1.Release, error) {
return nil, nil
}

func (f *fakeStorageShow) GetRevisions() []uint64 {
return nil
}

func (f *fakeStorageShow) GetStackBoundRevisions(_ string) []uint64 {
return nil
}

func (f *fakeStorageShow) GetLatestRevision() uint64 {
return 0
}

func (f *fakeStorageShow) Create(_ *v1.Release) error {
return nil
}

func (f *fakeStorageShow) Update(_ *v1.Release) error {
return nil
}

0 comments on commit c29980d

Please sign in to comment.