diff --git a/go.sum b/go.sum index 5673d2533..8d4eabf82 100644 --- a/go.sum +++ b/go.sum @@ -1352,7 +1352,6 @@ gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc= gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/engine/operation/change.go b/pkg/engine/operation/change.go index e02e94009..2b51ffbdc 100644 --- a/pkg/engine/operation/change.go +++ b/pkg/engine/operation/change.go @@ -19,15 +19,17 @@ import ( ) type ChangeStep struct { - ID string // the resource id - Action ActionType // the operation performed by this step. - Old interface{} // the state of the resource before performing this step. - New interface{} // the state of the resource after performing this step. + ID string // the resource id + Action ActionType // the operation performed by this step. + Original interface{} // local stored resource + Modified interface{} // planed resource + Current interface{} // live resource } +// TODO: 3-way diff func (cs *ChangeStep) Diff() (string, error) { // Generate diff report - diffReport, err := diffToReport(cs.Old, cs.New) + diffReport, err := diffToReport(cs.Original, cs.Modified) if err != nil { log.Errorf("failed to compute diff with ChangeStep ID: %s", cs.ID) return "", err @@ -59,12 +61,13 @@ func (cs *ChangeStep) Diff() (string, error) { return buf.String(), nil } -func NewChangeStep(id string, op ActionType, oldData, newData interface{}) *ChangeStep { +func NewChangeStep(id string, op ActionType, original, modified, current interface{}) *ChangeStep { return &ChangeStep{ - ID: id, - Action: op, - Old: oldData, - New: newData, + ID: id, + Action: op, + Original: original, + Modified: modified, + Current: current, } } diff --git a/pkg/engine/operation/change_test.go b/pkg/engine/operation/change_test.go index 07cbec37b..ebaa1dcb7 100644 --- a/pkg/engine/operation/change_test.go +++ b/pkg/engine/operation/change_test.go @@ -11,10 +11,10 @@ import ( ) var ( - TestChangeStepOpCreate = NewChangeStep("id", Create, nil, nil) - TestChangeStepOpDelete = NewChangeStep("id", Delete, nil, nil) - TestChangeStepOpUpdate = NewChangeStep("id", Update, nil, nil) - TestChangeStepOpUnChange = NewChangeStep("id", UnChange, nil, nil) + TestChangeStepOpCreate = NewChangeStep("id", Create, nil, nil, nil) + TestChangeStepOpDelete = NewChangeStep("id", Delete, nil, nil, nil) + TestChangeStepOpUpdate = NewChangeStep("id", Update, nil, nil, nil) + TestChangeStepOpUnChange = NewChangeStep("id", UnChange, nil, nil, nil) TestStepKeys = []string{"test-key-1", "test-key-2", "test-key-3", "test-key-4"} TestChangeSteps = map[string]*ChangeStep{ "test-key-1": TestChangeStepOpCreate, @@ -128,10 +128,10 @@ func TestChangeStep_Diff(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cs := &ChangeStep{ - ID: tt.fields.ID, - Action: tt.fields.Op, - Old: tt.fields.Old, - New: tt.fields.New, + ID: tt.fields.ID, + Action: tt.fields.Op, + Original: tt.fields.Old, + Modified: tt.fields.New, } got, err := cs.Diff() if (err != nil) != tt.wantErr { @@ -145,59 +145,6 @@ func TestChangeStep_Diff(t *testing.T) { } } -func TestNewChangeStep(t *testing.T) { - type args struct { - id string - op ActionType - old interface{} - new interface{} - } - tests := []struct { - name string - args args - want *ChangeStep - }{ - { - name: "t1", - args: args{ - id: "id", - op: Create, - old: nil, - new: nil, - }, - want: &ChangeStep{ - ID: "id", - Action: Create, - Old: nil, - New: nil, - }, - }, - { - name: "t2", - args: args{ - id: "id[0]", - op: Create, - old: nil, - new: nil, - }, - want: &ChangeStep{ - ID: "id[0]", - Action: Create, - Old: nil, - New: nil, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NewChangeStep(tt.args.id, tt.args.op, tt.args.old, tt.args.new); !reflect.DeepEqual(got, - tt.want) { - t.Errorf("NewChangeStep() = %v, want %v", got, tt.want) - } - }) - } -} - func TestChanges_Get(t *testing.T) { type fields struct { order *ChangeOrder diff --git a/pkg/engine/operation/preview.go b/pkg/engine/operation/preview.go index 3c55c0ef0..c31077d07 100644 --- a/pkg/engine/operation/preview.go +++ b/pkg/engine/operation/preview.go @@ -80,6 +80,7 @@ func (o *Operation) Preview(request *PreviewRequest, operation Type) (rsp *Previ PriorStateResourceIndex: priorStateResourceIndex, StateResourceIndex: priorStateResourceIndex, Order: o.Order, + Runtime: o.Runtime, // preview need get the latest manifest from runtime resultState: resultState, lock: &sync.Mutex{}, }, diff --git a/pkg/engine/operation/preview_test.go b/pkg/engine/operation/preview_test.go index b42f2cf9d..4d5004be4 100644 --- a/pkg/engine/operation/preview_test.go +++ b/pkg/engine/operation/preview_test.go @@ -1,6 +1,7 @@ package operation import ( + "context" "reflect" "sync" "testing" @@ -30,6 +31,26 @@ var ( } ) +var _ runtime.Runtime = (*fakePreviewRuntime)(nil) + +type fakePreviewRuntime struct{} + +func (f *fakePreviewRuntime) Apply(ctx context.Context, priorState, planState *models.Resource) (*models.Resource, status.Status) { + return planState, nil +} + +func (f *fakePreviewRuntime) Read(ctx context.Context, resourceState *models.Resource) (*models.Resource, status.Status) { + return resourceState, nil +} + +func (f *fakePreviewRuntime) Delete(ctx context.Context, resourceState *models.Resource) status.Status { + return nil +} + +func (f *fakePreviewRuntime) Watch(ctx context.Context, resourceState *models.Resource) (*models.Resource, status.Status) { + return resourceState, nil +} + func TestOperation_Preview(t *testing.T) { type fields struct { OperationType Type @@ -57,14 +78,14 @@ func TestOperation_Preview(t *testing.T) { { name: "success-when-apply", fields: fields{ - Runtime: &runtime.KubernetesRuntime{}, + Runtime: &fakePreviewRuntime{}, StateStorage: &states.FileSystemState{Path: states.KusionState}, Order: &ChangeOrder{StepKeys: []string{}, ChangeSteps: map[string]*ChangeStep{}}, }, args: args{ request: &PreviewRequest{ Request: Request{ - Tenant: "fake-tennat", + Tenant: "fake-tenant", Stack: "fake-stack", Project: "fake-project", Operator: "fake-operator", @@ -82,10 +103,11 @@ func TestOperation_Preview(t *testing.T) { StepKeys: []string{"fake-id"}, ChangeSteps: map[string]*ChangeStep{ "fake-id": { - ID: "fake-id", - Action: Create, - Old: (*models.Resource)(nil), - New: &FakeResourceState, + ID: "fake-id", + Action: Create, + Original: (*models.Resource)(nil), + Modified: &FakeResourceState, + Current: (*models.Resource)(nil), }, }, }, @@ -95,14 +117,14 @@ func TestOperation_Preview(t *testing.T) { { name: "success-when-destroy", fields: fields{ - Runtime: &runtime.KubernetesRuntime{}, + Runtime: &fakePreviewRuntime{}, StateStorage: &states.FileSystemState{Path: states.KusionState}, Order: &ChangeOrder{}, }, args: args{ request: &PreviewRequest{ Request: Request{ - Tenant: "fake-tennat", + Tenant: "fake-tenant", Stack: "fake-stack", Project: "fake-project", Operator: "fake-operator", @@ -120,10 +142,11 @@ func TestOperation_Preview(t *testing.T) { StepKeys: []string{"fake-id"}, ChangeSteps: map[string]*ChangeStep{ "fake-id": { - ID: "fake-id", - Action: Delete, - Old: &FakeResourceState, - New: (*models.Resource)(nil), + ID: "fake-id", + Action: Delete, + Original: &FakeResourceState, + Modified: (*models.Resource)(nil), + Current: &FakeResourceState, }, }, }, @@ -133,7 +156,7 @@ func TestOperation_Preview(t *testing.T) { { name: "fail-because-empty-models", fields: fields{ - Runtime: &runtime.KubernetesRuntime{}, + Runtime: &fakePreviewRuntime{}, StateStorage: &states.FileSystemState{Path: states.KusionState}, Order: &ChangeOrder{}, }, @@ -151,7 +174,7 @@ func TestOperation_Preview(t *testing.T) { { name: "fail-because-nonexistent-id", fields: fields{ - Runtime: &runtime.KubernetesRuntime{}, + Runtime: &fakePreviewRuntime{}, StateStorage: &states.FileSystemState{Path: states.KusionState}, Order: &ChangeOrder{}, }, diff --git a/pkg/engine/operation/resource_node.go b/pkg/engine/operation/resource_node.go index f540d598a..6ef3add40 100644 --- a/pkg/engine/operation/resource_node.go +++ b/pkg/engine/operation/resource_node.go @@ -39,23 +39,29 @@ func (rn *ResourceNode) Execute(operation *Operation) status.Status { planedState = nil } - // 2. get State + // 2. get prior state which is stored in kusion_state.json key := rn.state.ResourceKey() priorState := operation.PriorStateResourceIndex[key] - // 3. compute ActionType of current resource node - if priorState == nil { + // get the latest resource from runtime + liveState, s := operation.Runtime.Read(context.Background(), priorState) + if status.IsErr(s) { + return s + } + + // 3. compute ActionType of current resource node between planState and liveState + if liveState == nil { rn.Action = Create } else if planedState == nil { rn.Action = Delete - } else if reflect.DeepEqual(priorState, planedState) { + } else if reflect.DeepEqual(liveState, planedState) { rn.Action = UnChange } else { rn.Action = Update } if operation.OperationType == Preview { - fillResponseChangeSteps(operation, rn, priorState, planedState) + fillResponseChangeSteps(operation, rn, priorState, planedState, liveState) return nil } // 4. apply @@ -73,8 +79,8 @@ func (rn *ResourceNode) Execute(operation *Operation) status.Status { return nil } -func (rn *ResourceNode) applyResource(operation *Operation, priorState *models.Resource, planedState *models.Resource) status.Status { - log.Infof("PriorAttributes and PlanAttributes are not equal. operation:%v, prior:%v, plan:%v", rn.Action, +func (rn *ResourceNode) applyResource(operation *Operation, priorState, planedState *models.Resource) status.Status { + log.Infof("operation:%v, prior:%v, plan:%v, live:%v", rn.Action, jsonUtil.Marshal2String(priorState), jsonUtil.Marshal2String(planedState)) var res *models.Resource @@ -122,7 +128,8 @@ func NewResourceNode(key string, state *models.Resource, action ActionType) *Res return &ResourceNode{BaseNode: BaseNode{ID: key}, Action: action, state: state} } -func fillResponseChangeSteps(operation *Operation, rn *ResourceNode, prior, plan interface{}) { +// save change steps in DAG walking order so that we can preview a full applying list +func fillResponseChangeSteps(operation *Operation, rn *ResourceNode, prior, plan, live interface{}) { defer operation.lock.Unlock() operation.lock.Lock() @@ -137,7 +144,7 @@ func fillResponseChangeSteps(operation *Operation, rn *ResourceNode, prior, plan order.ChangeSteps = make(map[string]*ChangeStep) } order.StepKeys = append(order.StepKeys, rn.ID) - order.ChangeSteps[rn.ID] = NewChangeStep(rn.ID, rn.Action, prior, plan) + order.ChangeSteps[rn.ID] = NewChangeStep(rn.ID, rn.Action, prior, plan, live) } var ImplicitReplaceFun = func(resourceIndex map[string]*models.Resource, refPath string) (reflect.Value, status.Status) { diff --git a/pkg/engine/operation/resource_node_test.go b/pkg/engine/operation/resource_node_test.go index 10f79e83b..e8d865a3e 100644 --- a/pkg/engine/operation/resource_node_test.go +++ b/pkg/engine/operation/resource_node_test.go @@ -171,6 +171,10 @@ func TestResourceNode_Execute(t *testing.T) { func(k *runtime.KubernetesRuntime, ctx context.Context, priorState *models.Resource) status.Status { return nil }) + monkey.PatchInstanceMethod(reflect.TypeOf(tt.args.operation.Runtime), "Read", + func(k *runtime.KubernetesRuntime, ctx context.Context, resourceState *models.Resource) (*models.Resource, status.Status) { + return resourceState, nil + }) monkey.PatchInstanceMethod(reflect.TypeOf(tt.args.operation.StateStorage), "Apply", func(f *states.FileSystemState, state *states.State) error { return nil diff --git a/pkg/engine/runtime/kubernetest_runtime.go b/pkg/engine/runtime/kubernetest_runtime.go index 433be706c..bb9da0b2a 100644 --- a/pkg/engine/runtime/kubernetest_runtime.go +++ b/pkg/engine/runtime/kubernetest_runtime.go @@ -4,8 +4,6 @@ import ( "context" "errors" - "kusionstack.io/kusion/pkg/engine/models" - k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -13,12 +11,14 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/jsonmergepatch" "k8s.io/client-go/discovery" memory "k8s.io/client-go/discovery/cached" "k8s.io/client-go/dynamic" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" + "kusionstack.io/kusion/pkg/engine/models" "kusionstack.io/kusion/pkg/log" "kusionstack.io/kusion/pkg/status" "kusionstack.io/kusion/pkg/util/kube/config" @@ -52,51 +52,39 @@ func (k *KubernetesRuntime) Apply(ctx context.Context, priorState, planState *mo return nil, status.NewErrorStatus(errors.New("plan state is nil")) } - // Get kubernetes resource from plan state - obj, resource, err := k.buildKubernetesResourceByState(planState) + // Get kubernetes resource interface from plan state + planObj, resource, err := k.buildKubernetesResourceByState(planState) if err != nil { return nil, status.NewErrorStatus(err) } - // Create or Update resource - if priorState == nil { - // Create - if _, err = resource.Get(ctx, obj.GetName(), metav1.GetOptions{}); err != nil { - // Create the resource if not exists - if !k8serrors.IsNotFound(err) { - return nil, status.NewErrorStatus(err) - } - - log.Infof("Create the resource %s/%s/%s because it doesn't exist", obj.GetKind(), obj.GetNamespace(), obj.GetName()) - _, err = resource.Create(ctx, obj, metav1.CreateOptions{}) - } else { - // Patch the resource because already exists - log.Infof("Patch the resource %s/%s/%s because it already exists", obj.GetKind(), obj.GetNamespace(), obj.GetName()) - - var objJSON []byte - objJSON, err = obj.MarshalJSON() - if err != nil { - return nil, status.NewErrorStatus(err) - } - - _, err = resource.Patch(ctx, obj.GetName(), types.MergePatchType, objJSON, metav1.PatchOptions{ - FieldManager: "kusion", - }) - } - } else { - // Update - _, err = resource.Update(ctx, obj, metav1.UpdateOptions{ - FieldManager: "kusion", - }) + // original equals to last-applied from annotation, kusion store it in kusion_state.json + original := yaml.MergeToOneYAML(priorState.Attributes) + // modified equals input content + modified := yaml.MergeToOneYAML(planState.Attributes) + // get live state + liveState, err := resource.Get(ctx, planObj.GetName(), metav1.GetOptions{}) + if err != nil { + return nil, status.NewErrorStatus(err) } - + // current equals live manifest + current := yaml.MergeToOneYAML(liveState.Object) + // 3-way json merge patch + patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch([]byte(original), []byte(modified), []byte(current)) + if err != nil { + return nil, status.NewErrorStatus(err) + } + // apply patch + patchedObj, err := resource.Patch(ctx, planObj.GetName(), types.MergePatchType, patch, metav1.PatchOptions{ + FieldManager: "kusion", + }) if err != nil { return nil, status.NewErrorStatus(err) } return &models.Resource{ ID: planState.ResourceKey(), - Attributes: obj.Object, + Attributes: patchedObj.Object, DependsOn: planState.DependsOn, }, nil } diff --git a/pkg/kusionctl/cmd/apply/options_test.go b/pkg/kusionctl/cmd/apply/options_test.go index ad4ac0058..6d9a3dffc 100644 --- a/pkg/kusionctl/cmd/apply/options_test.go +++ b/pkg/kusionctl/cmd/apply/options_test.go @@ -157,22 +157,22 @@ func mockOperationPreview() { StepKeys: []string{sa1.ID, sa2.ID, sa3.ID}, ChangeSteps: map[string]*operation.ChangeStep{ sa1.ID: { - ID: sa1.ID, - Action: operation.Create, - Old: nil, - New: &sa1, + ID: sa1.ID, + Action: operation.Create, + Original: nil, + Modified: &sa1, }, sa2.ID: { - ID: sa2.ID, - Action: operation.UnChange, - Old: &sa2, - New: &sa2, + ID: sa2.ID, + Action: operation.UnChange, + Original: &sa2, + Modified: &sa2, }, sa3.ID: { - ID: sa3.ID, - Action: operation.Undefined, - Old: &sa3, - New: &sa1, + ID: sa3.ID, + Action: operation.Undefined, + Original: &sa3, + Modified: &sa1, }, }, }, @@ -218,10 +218,10 @@ func Test_apply(t *testing.T) { StepKeys: []string{sa1.ID}, ChangeSteps: map[string]*operation.ChangeStep{ sa1.ID: { - ID: sa1.ID, - Action: operation.Create, - Old: nil, - New: sa1, + ID: sa1.ID, + Action: operation.Create, + Original: nil, + Modified: sa1, }, }, } @@ -242,16 +242,16 @@ func Test_apply(t *testing.T) { StepKeys: []string{sa1.ID, sa2.ID}, ChangeSteps: map[string]*operation.ChangeStep{ sa1.ID: { - ID: sa1.ID, - Action: operation.Create, - Old: nil, - New: &sa1, + ID: sa1.ID, + Action: operation.Create, + Original: nil, + Modified: &sa1, }, sa2.ID: { - ID: sa2.ID, - Action: operation.UnChange, - Old: &sa2, - New: &sa2, + ID: sa2.ID, + Action: operation.UnChange, + Original: &sa2, + Modified: &sa2, }, }, } @@ -271,10 +271,10 @@ func Test_apply(t *testing.T) { StepKeys: []string{sa1.ID}, ChangeSteps: map[string]*operation.ChangeStep{ sa1.ID: { - ID: sa1.ID, - Action: operation.Create, - Old: nil, - New: &sa1, + ID: sa1.ID, + Action: operation.Create, + Original: nil, + Modified: &sa1, }, }, } diff --git a/pkg/kusionctl/cmd/destroy/options_test.go b/pkg/kusionctl/cmd/destroy/options_test.go index 482b8bd6b..b6fa11d0f 100644 --- a/pkg/kusionctl/cmd/destroy/options_test.go +++ b/pkg/kusionctl/cmd/destroy/options_test.go @@ -149,10 +149,10 @@ func mockOperationPreview() { StepKeys: []string{sa1.ID}, ChangeSteps: map[string]*operation.ChangeStep{ sa1.ID: { - ID: sa1.ID, - Action: operation.Delete, - Old: &sa1, - New: nil, + ID: sa1.ID, + Action: operation.Delete, + Original: &sa1, + Modified: nil, }, }, }, @@ -199,16 +199,16 @@ func Test_destroy(t *testing.T) { StepKeys: []string{sa1.ID, sa2.ID}, ChangeSteps: map[string]*operation.ChangeStep{ sa1.ID: { - ID: sa1.ID, - Action: operation.Delete, - Old: &sa1, - New: nil, + ID: sa1.ID, + Action: operation.Delete, + Original: &sa1, + Modified: nil, }, sa2.ID: { - ID: sa2.ID, - Action: operation.UnChange, - Old: &sa2, - New: &sa2, + ID: sa2.ID, + Action: operation.UnChange, + Original: &sa2, + Modified: &sa2, }, }, } @@ -228,10 +228,10 @@ func Test_destroy(t *testing.T) { StepKeys: []string{sa1.ID}, ChangeSteps: map[string]*operation.ChangeStep{ sa1.ID: { - ID: sa1.ID, - Action: operation.Delete, - Old: &sa1, - New: nil, + ID: sa1.ID, + Action: operation.Delete, + Original: &sa1, + Modified: nil, }, }, }