Skip to content

Commit

Permalink
refact: preview&apply use the same cmd pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
adohe committed Apr 12, 2024
1 parent fbd5ece commit 559f823
Show file tree
Hide file tree
Showing 9 changed files with 1,206 additions and 1,441 deletions.
468 changes: 438 additions & 30 deletions pkg/cmd/apply/apply.go

Large diffs are not rendered by default.

320 changes: 316 additions & 4 deletions pkg/cmd/apply/apply_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,327 @@
package apply

import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/AlecAivazis/survey/v2"
"github.com/bytedance/mockey"
"github.com/stretchr/testify/assert"

apiv1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
internalv1 "kusionstack.io/kusion/pkg/apis/internal.kusion.io/v1"
v1 "kusionstack.io/kusion/pkg/apis/status/v1"
"kusionstack.io/kusion/pkg/backend/storages"
"kusionstack.io/kusion/pkg/cmd/generate"
"kusionstack.io/kusion/pkg/cmd/meta"
"kusionstack.io/kusion/pkg/cmd/preview"
"kusionstack.io/kusion/pkg/engine"
"kusionstack.io/kusion/pkg/engine/operation"
"kusionstack.io/kusion/pkg/engine/operation/models"
"kusionstack.io/kusion/pkg/engine/runtime"
"kusionstack.io/kusion/pkg/engine/runtime/kubernetes"
statestorages "kusionstack.io/kusion/pkg/engine/state/storages"
workspacestorages "kusionstack.io/kusion/pkg/workspace/storages"
)

var (
proj = &apiv1.Project{
Name: "testdata",
}
stack = &apiv1.Stack{
Name: "dev",
}
workspace = &apiv1.Workspace{
Name: "default",
}
)

func TestApplyCommandRun(t *testing.T) {
t.Run("validate error", func(t *testing.T) {
cmd := NewCmdApply()
err := cmd.Execute()
func NewApplyOptions() *ApplyOptions {
storageBackend := storages.NewLocalStorage(&internalv1.BackendLocalConfig{
Path: filepath.Join("", "state.yaml"),
})
return &ApplyOptions{
PreviewOptions: &preview.PreviewOptions{
MetaOptions: &meta.MetaOptions{
RefProject: proj,
RefStack: stack,
RefWorkspace: workspace,
StorageBackend: storageBackend,
},
Operator: "",
Detail: false,
All: false,
NoStyle: false,
Output: "",
IgnoreFields: nil,
},
}
}

func TestApplyOptionsRun(t *testing.T) {
/*mockey.PatchConvey("Detail is true", t, func() {
mockGenerateSpecWithSpinner()
mockPatchNewKubernetesRuntime()
mockPatchOperationPreview()
mockStateStorage()
o := NewApplyOptions()
o.Detail = true
o.All = true
o.NoStyle = true
err := o.Run()
assert.Nil(t, err)
})*/

mockey.PatchConvey("DryRun is true", t, func() {
mockGenerateSpecWithSpinner()
mockPatchNewKubernetesRuntime()
mockPatchOperationPreview()
mockStateStorage()
mockOperationApply(models.Success)

o := NewApplyOptions()
o.DryRun = true
mockPromptOutput("yes")
err := o.Run()
assert.Nil(t, err)
})
}

func mockGenerateSpecWithSpinner() {
mockey.Mock(generate.GenerateSpecWithSpinner).To(func(
project *apiv1.Project,
stack *apiv1.Stack,
workspace *apiv1.Workspace,
noStyle bool,
) (*apiv1.Spec, error) {
return &apiv1.Spec{Resources: []apiv1.Resource{sa1, sa2, sa3}}, nil
}).Build()
}

func mockPatchNewKubernetesRuntime() *mockey.Mocker {
return mockey.Mock(kubernetes.NewKubernetesRuntime).To(func() (runtime.Runtime, error) {
return &fakerRuntime{}, nil
}).Build()
}

func mockWorkspaceStorage() {

Check failure on line 112 in pkg/cmd/apply/apply_test.go

View workflow job for this annotation

GitHub Actions / Golang Lint

func `mockWorkspaceStorage` is unused (unused)
mockey.Mock((*storages.LocalStorage).WorkspaceStorage).Return(&workspacestorages.LocalStorage{}, nil).Build()
mockey.Mock((*workspacestorages.LocalStorage).Get).Return(&apiv1.Workspace{}, nil).Build()
}

var _ runtime.Runtime = (*fakerRuntime)(nil)

type fakerRuntime struct{}

func (f *fakerRuntime) Import(_ context.Context, request *runtime.ImportRequest) *runtime.ImportResponse {
return &runtime.ImportResponse{Resource: request.PlanResource}
}

func (f *fakerRuntime) Apply(_ context.Context, request *runtime.ApplyRequest) *runtime.ApplyResponse {
return &runtime.ApplyResponse{
Resource: request.PlanResource,
Status: nil,
}
}

func (f *fakerRuntime) Read(_ context.Context, request *runtime.ReadRequest) *runtime.ReadResponse {
if request.PlanResource.ResourceKey() == "fake-id" {
return &runtime.ReadResponse{
Resource: nil,
Status: nil,
}
}
return &runtime.ReadResponse{
Resource: request.PlanResource,
Status: nil,
}
}

func (f *fakerRuntime) Delete(_ context.Context, _ *runtime.DeleteRequest) *runtime.DeleteResponse {
return nil
}

func (f *fakerRuntime) Watch(_ context.Context, _ *runtime.WatchRequest) *runtime.WatchResponse {
return nil
}

func mockPatchOperationPreview() *mockey.Mocker {
return mockey.Mock((*operation.PreviewOperation).Preview).To(func(
*operation.PreviewOperation,
*operation.PreviewRequest,
) (rsp *operation.PreviewResponse, s v1.Status) {
return &operation.PreviewResponse{
Order: &models.ChangeOrder{
StepKeys: []string{sa1.ID, sa2.ID, sa3.ID},
ChangeSteps: map[string]*models.ChangeStep{
sa1.ID: {
ID: sa1.ID,
Action: models.Create,
From: &sa1,
},
sa2.ID: {
ID: sa2.ID,
Action: models.UnChanged,
From: &sa2,
},
sa3.ID: {
ID: sa3.ID,
Action: models.Undefined,
From: &sa1,
},
},
},
}, nil
}).Build()
}

func mockStateStorage() {
mockey.Mock((*storages.LocalStorage).WorkspaceStorage).Return(&workspacestorages.LocalStorage{}, nil).Build()
}

const (
apiVersion = "v1"
kind = "ServiceAccount"
namespace = "test-ns"
)

var (
sa1 = newSA("sa1")
sa2 = newSA("sa2")
sa3 = newSA("sa3")
)

func newSA(name string) apiv1.Resource {
return apiv1.Resource{
ID: engine.BuildID(apiVersion, kind, namespace, name),
Type: "Kubernetes",
Attributes: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"name": name,
"namespace": namespace,
},
},
}
}

func TestApply(t *testing.T) {
stateStorage := statestorages.NewLocalStorage(filepath.Join("", "state.yaml"))
mockey.PatchConvey("dry run", t, func() {
planResources := &apiv1.Spec{Resources: []apiv1.Resource{sa1}}
order := &models.ChangeOrder{
StepKeys: []string{sa1.ID},
ChangeSteps: map[string]*models.ChangeStep{
sa1.ID: {
ID: sa1.ID,
Action: models.Create,
From: sa1,
},
},
}
changes := models.NewChanges(proj, stack, order)
o := &ApplyOptions{}
o.DryRun = true
err := Apply(o, stateStorage, planResources, changes, os.Stdout)
assert.Nil(t, err)
})
mockey.PatchConvey("apply success", t, func() {
mockOperationApply(models.Success)
o := &ApplyOptions{}
planResources := &apiv1.Spec{Resources: []apiv1.Resource{sa1, sa2}}
order := &models.ChangeOrder{
StepKeys: []string{sa1.ID, sa2.ID},
ChangeSteps: map[string]*models.ChangeStep{
sa1.ID: {
ID: sa1.ID,
Action: models.Create,
From: &sa1,
},
sa2.ID: {
ID: sa2.ID,
Action: models.UnChanged,
From: &sa2,
},
},
}
changes := models.NewChanges(proj, stack, order)

err := Apply(o, stateStorage, planResources, changes, os.Stdout)
assert.Nil(t, err)
})
mockey.PatchConvey("apply failed", t, func() {
mockOperationApply(models.Failed)

o := &ApplyOptions{}
planResources := &apiv1.Spec{Resources: []apiv1.Resource{sa1}}
order := &models.ChangeOrder{
StepKeys: []string{sa1.ID},
ChangeSteps: map[string]*models.ChangeStep{
sa1.ID: {
ID: sa1.ID,
Action: models.Create,
From: &sa1,
},
},
}
changes := models.NewChanges(proj, stack, order)

err := Apply(o, stateStorage, planResources, changes, os.Stdout)
assert.NotNil(t, err)
})
}

func mockOperationApply(res models.OpResult) {
mockey.Mock((*operation.ApplyOperation).Apply).To(
func(o *operation.ApplyOperation, request *operation.ApplyRequest) (*operation.ApplyResponse, v1.Status) {
var err error
if res == models.Failed {
err = errors.New("mock error")
}
for _, r := range request.Intent.Resources {
// ing -> $res
o.MsgCh <- models.Message{
ResourceID: r.ResourceKey(),
OpResult: "",
OpErr: nil,
}
o.MsgCh <- models.Message{
ResourceID: r.ResourceKey(),
OpResult: res,
OpErr: err,
}
}
close(o.MsgCh)
if res == models.Failed {
return nil, v1.NewErrorStatus(err)
}
return &operation.ApplyResponse{}, nil
}).Build()
}

func TestPrompt(t *testing.T) {
mockey.PatchConvey("prompt error", t, func() {
mockey.Mock(survey.AskOne).Return(errors.New("mock error")).Build()
_, err := prompt()
assert.NotNil(t, err)
})

mockey.PatchConvey("prompt yes", t, func() {
mockPromptOutput("yes")
_, err := prompt()
assert.Nil(t, err)
})
}

func mockPromptOutput(res string) {
mockey.Mock(survey.AskOne).To(func(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
reflect.ValueOf(response).Elem().Set(reflect.ValueOf(res))
return nil
}).Build()
}
Loading

0 comments on commit 559f823

Please sign in to comment.