Skip to content

Commit

Permalink
feat: cmd preview and apply support new flag '--ignore-fields' (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
howieyuen committed Sep 13, 2022
1 parent 3795354 commit 91a54c4
Show file tree
Hide file tree
Showing 16 changed files with 494 additions and 159 deletions.
11 changes: 11 additions & 0 deletions pkg/engine/models/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@ package models
type Spec struct {
Resources Resources `json:"resources" yaml:"resources"`
}

// ParseCluster try to parse Cluster from resource extensions.
// All resources in one compile MUST have the same Cluster and this constraint will be guaranteed by KCL compile logic
func (s *Spec) ParseCluster() string {
resources := s.Resources
var cluster string
if len(resources) != 0 && resources[0].Extensions != nil && resources[0].Extensions["Cluster"] != nil {
cluster = resources[0].Extensions["Cluster"].(string)
}
return cluster
}
8 changes: 8 additions & 0 deletions pkg/engine/operation/graph/resource_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"reflect"
"strings"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"kusionstack.io/kusion/pkg/engine/models"
opsmodels "kusionstack.io/kusion/pkg/engine/operation/models"
"kusionstack.io/kusion/pkg/engine/operation/types"
Expand Down Expand Up @@ -83,6 +85,12 @@ func (rn *ResourceNode) Execute(operation *opsmodels.Operation) status.Status {
return dryRunResp.Status
}
predictableState = dryRunResp.Resource
// Ignore differences of target fields
for _, field := range operation.IgnoreFields {
splits := strings.Split(field, ".")
unstructured.RemoveNestedField(liveState.Attributes, splits...)
unstructured.RemoveNestedField(predictableState.Attributes, splits...)
}
report, err := diff.ToReport(liveState, predictableState)
if err != nil {
return status.NewErrorStatus(err)
Expand Down
1 change: 1 addition & 0 deletions pkg/engine/operation/graph/resource_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func TestResourceNode_Execute(t *testing.T) {
CtxResourceIndex: priorStateResourceIndex,
PriorStateResourceIndex: priorStateResourceIndex,
StateResourceIndex: priorStateResourceIndex,
IgnoreFields: []string{"not_exist_field"},
MsgCh: make(chan opsmodels.Message),
ResultState: states.NewState(),
Lock: &sync.Mutex{},
Expand Down
14 changes: 13 additions & 1 deletion pkg/engine/operation/models/change.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package models
import (
"bytes"
"fmt"
"io"
"strings"

"github.com/AlecAivazis/survey/v2"
Expand Down Expand Up @@ -148,7 +149,17 @@ func (o *ChangeOrder) Diffs() string {
return buf.String()
}

func (p *Changes) Summary() {
func (p *Changes) AllUnChange() bool {
for _, v := range p.ChangeSteps {
if v.Action != types.UnChange {
return false
}
}

return true
}

func (p *Changes) Summary(writer io.Writer) {
// Create a fork of the default table, fill it with data and print it.
// Data can also be generated and inserted later.
tableHeader := []string{fmt.Sprintf("Stack: %s", p.stack.Name), "ID", "Action"}
Expand All @@ -169,6 +180,7 @@ func (p *Changes) Summary() {
WithLeftAlignment(true).
WithSeparator(" ").
WithData(tableData).
WithWriter(writer).
Render()
pterm.Println() // Blank line
}
Expand Down
35 changes: 34 additions & 1 deletion pkg/engine/operation/models/change_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package models

import (
"os"
"reflect"
"testing"

"github.com/stretchr/testify/assert"

"kusionstack.io/kusion/pkg/engine/models"
"kusionstack.io/kusion/pkg/engine/operation/types"
"kusionstack.io/kusion/pkg/projectstack"
Expand Down Expand Up @@ -432,7 +435,7 @@ func TestChanges_Preview(t *testing.T) {
project: tt.fields.project,
stack: tt.fields.stack,
}
p.Summary()
p.Summary(os.Stdout)
})
}
}
Expand Down Expand Up @@ -462,3 +465,33 @@ func Test_buildResourceStateMap(t *testing.T) {
})
}
}

func TestChanges_AllUnChange(t *testing.T) {
t.Run("changed", func(t *testing.T) {
changes := &Changes{
ChangeOrder: &ChangeOrder{
ChangeSteps: map[string]*ChangeStep{
"foo": {
Action: types.Update,
},
},
},
}
flag := changes.AllUnChange()
assert.False(t, flag)
})

t.Run("unchanged", func(t *testing.T) {
changes := &Changes{
ChangeOrder: &ChangeOrder{
ChangeSteps: map[string]*ChangeStep{
"bar": {
Action: types.UnChange,
},
},
},
}
flag := changes.AllUnChange()
assert.True(t, flag)
})
}
3 changes: 3 additions & 0 deletions pkg/engine/operation/models/operation_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type Operation struct {
// StateResourceIndex represents resources that will be saved in states.StateStorage
StateResourceIndex map[string]*models.Resource

// IgnoreFields will be ignored in preview stage
IgnoreFields []string

// ChangeOrder is resources' change order during this operation
ChangeOrder *ChangeOrder

Expand Down
1 change: 1 addition & 0 deletions pkg/engine/operation/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func (po *PreviewOperation) Preview(request *PreviewRequest) (rsp *PreviewRespon
CtxResourceIndex: map[string]*models.Resource{},
PriorStateResourceIndex: priorStateResourceIndex,
StateResourceIndex: priorStateResourceIndex,
IgnoreFields: o.IgnoreFields,
ChangeOrder: o.ChangeOrder,
Runtime: o.Runtime, // preview need get the latest spec from runtime
ResultState: resultState,
Expand Down
8 changes: 3 additions & 5 deletions pkg/kusionctl/cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,15 @@ func NewCmdApply() *cobra.Command {
}

o.AddCompileFlags(cmd)
cmd.Flags().StringVarP(&o.Operator, "operator", "", "",
i18n.T("Specify the operator"))
o.AddPreviewFlags(cmd)
o.AddBackendFlags(cmd)

cmd.Flags().BoolVarP(&o.Yes, "yes", "y", false,
i18n.T("Automatically approve and perform the update after previewing it"))
cmd.Flags().BoolVarP(&o.Detail, "detail", "d", false,
i18n.T("Automatically show plan details after previewing it"))
cmd.Flags().BoolVarP(&o.NoStyle, "no-style", "", false,
i18n.T("no-style sets to RawOutput mode and disables all of styling"))
cmd.Flags().BoolVarP(&o.DryRun, "dry-run", "", false,
i18n.T("dry-run to preview the execution effect (always successful) without actually applying the changes"))
o.AddBackendFlags(cmd)

return cmd
}
105 changes: 13 additions & 92 deletions pkg/kusionctl/cmd/apply/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,29 @@ import (
"kusionstack.io/kusion/pkg/engine/runtime"
runtimeInit "kusionstack.io/kusion/pkg/engine/runtime/init"
"kusionstack.io/kusion/pkg/engine/states"
compilecmd "kusionstack.io/kusion/pkg/kusionctl/cmd/compile"
previewcmd "kusionstack.io/kusion/pkg/kusionctl/cmd/preview"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/projectstack"
"kusionstack.io/kusion/pkg/status"
)

// ApplyOptions defines flags for the `apply` command
type ApplyOptions struct {
compilecmd.CompileOptions
Operator string
previewcmd.PreviewOptions
ApplyFlag
}

type ApplyFlag struct {
Yes bool
Detail bool
NoStyle bool
DryRun bool
OnlyPreview bool
backend.BackendOps
}

// NewApplyOptions returns a new ApplyOptions instance
func NewApplyOptions() *ApplyOptions {
return &ApplyOptions{
CompileOptions: compilecmd.CompileOptions{
Filenames: []string{},
Arguments: []string{},
Settings: []string{},
Overrides: []string{},
},
PreviewOptions: *previewcmd.NewPreviewOptions(),
}
}

Expand Down Expand Up @@ -80,20 +76,20 @@ func (o *ApplyOptions) Run() error {
sp.Success() // Resolve spinner with success message.
pterm.Println()

// Get stateStroage from backend config to manage state
// Get state storage from backend config to manage state
stateStorage, err := backend.BackendFromConfig(project.Backend, o.BackendOps, o.WorkDir)
if err != nil {
return err
}

// Compute changes for preview
runtimes := runtimeInit.InitRuntime()
runtime, err := runtimes[planResources.Resources[0].Type]()
r, err := runtimes[planResources.Resources[0].Type]()
if err != nil {
return err
}

changes, err := Preview(o, runtime, stateStorage, planResources, project, stack, os.Stdout)
changes, err := previewcmd.Preview(&o.PreviewOptions, r, stateStorage, planResources, project, stack)
if err != nil {
return err
}
Expand All @@ -104,7 +100,7 @@ func (o *ApplyOptions) Run() error {
}

// Summary preview table
changes.Summary()
changes.Summary(os.Stdout)

// Detail detection
if o.Detail && !o.Yes {
Expand Down Expand Up @@ -136,7 +132,7 @@ func (o *ApplyOptions) Run() error {

if !o.OnlyPreview {
fmt.Println("Start applying diffs ...")
if err := Apply(o, runtime, stateStorage, planResources, changes, os.Stdout); err != nil {
if err := Apply(o, r, stateStorage, planResources, changes, os.Stdout); err != nil {
return err
}

Expand All @@ -149,81 +145,6 @@ func (o *ApplyOptions) Run() error {
return nil
}

// The Preview function calculates the upcoming actions of each resource
// through the execution Kusion Engine, and you can customize the
// runtime of engine and the state storage through `runtime` and
// `storage` parameters.
//
// Example:
//
// o := NewApplyOptions()
// stateStorage := &states.FileSystemState{
// Path: filepath.Join(o.WorkDir, states.KusionState)
// }
// kubernetesRuntime, err := runtime.NewKubernetesRuntime()
// if err != nil {
// return err
// }
//
// changes, err := Preview(o, kubernetesRuntime, stateStorage,
// planResources, project, stack, os.Stdout)
// if err != nil {
// return err
// }
//
// todo @elliotxx io.Writer is not used now
func Preview(
o *ApplyOptions,
runtime runtime.Runtime,
storage states.StateStorage,
planResources *models.Spec,
project *projectstack.Project,
stack *projectstack.Stack,
out io.Writer,
) (*opsmodels.Changes, error) {
log.Info("Start compute preview changes ...")

// Construct the preview operation
pc := &operation.PreviewOperation{
Operation: opsmodels.Operation{
OperationType: types.ApplyPreview,
Runtime: runtime,
StateStorage: storage,
ChangeOrder: &opsmodels.ChangeOrder{StepKeys: []string{}, ChangeSteps: map[string]*opsmodels.ChangeStep{}},
},
}

log.Info("Start call pc.Preview() ...")

cluster := parseCluster(planResources)
rsp, s := pc.Preview(&operation.PreviewRequest{
Request: opsmodels.Request{
Tenant: project.Tenant,
Project: project.Name,
Stack: stack.Name,
Operator: o.Operator,
Spec: planResources,
Cluster: cluster,
},
})
if status.IsErr(s) {
return nil, fmt.Errorf("preview failed.\n%s", s.String())
}

return opsmodels.NewChanges(project, stack, rsp.Order), nil
}

// parseCluster try to parse Cluster from resource extensions.
// All resources in one compile MUST have the same Cluster and this constraint will be guaranteed by KCL compile logic
func parseCluster(planResources *models.Spec) string {
resources := planResources.Resources
var cluster string
if len(resources) != 0 && resources[0].Extensions != nil && resources[0].Extensions["Cluster"] != nil {
cluster = resources[0].Extensions["Cluster"].(string)
}
return cluster
}

// The Apply function will apply the resources changes
// through the execution Kusion Engine, and will save
// the state to specified storage.
Expand Down Expand Up @@ -344,7 +265,7 @@ func Apply(
}
close(ac.MsgCh)
} else {
cluster := parseCluster(planResources)
cluster := planResources.ParseCluster()
_, st := ac.Apply(&operation.ApplyRequest{
Request: opsmodels.Request{
Tenant: changes.Project().Tenant,
Expand Down
12 changes: 0 additions & 12 deletions pkg/kusionctl/cmd/apply/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,6 @@ func mockCompileWithSpinner() {
})
}

func Test_preview(t *testing.T) {
stateStorage := &local.FileSystemState{Path: filepath.Join("", local.KusionState)}
t.Run("preview success", func(t *testing.T) {
defer monkey.UnpatchAll()
mockOperationPreview()

o := NewApplyOptions()
_, err := Preview(o, &fakerRuntime{}, stateStorage, &models.Spec{Resources: []models.Resource{sa1, sa2, sa3}}, project, stack, os.Stdout)
assert.Nil(t, err)
})
}

func mockNewKubernetesRuntime() {
monkey.Patch(runtime.NewKubernetesRuntime, func() (runtime.Runtime, error) {
return &fakerRuntime{}, nil
Expand Down
Loading

0 comments on commit 91a54c4

Please sign in to comment.