Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cli plan command #219

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7adca47
feat: draft job plan command
okysetiawan Apr 30, 2024
9353bb0
feat: add command dif and tree list
okysetiawan Apr 30, 2024
3536f95
feat: add job plan command
okysetiawan May 3, 2024
c68efe6
feat: add command plan for resource
okysetiawan May 3, 2024
d4daf72
feat: update plan command description
okysetiawan May 3, 2024
34f0ea5
feat: add executed on plan object
okysetiawan May 3, 2024
e79706a
feat: refactor git api name and support personal token
okysetiawan May 3, 2024
d585506
feat: unified git diff get directories
okysetiawan May 3, 2024
1e9ed7b
feat: change order csv kind at first
okysetiawan May 3, 2024
b9c378a
feat: removed unused git API
okysetiawan May 3, 2024
8d012a5
feat: update message for output file on plan command
okysetiawan May 6, 2024
f9a9862
feat: removed unused attribute of git
okysetiawan May 6, 2024
5dcad38
feat: support github for plan command
okysetiawan May 6, 2024
f425796
refactor: move git from ext to client extension provider
okysetiawan May 6, 2024
b87bed7
feat: add some information related to abstraction contract
okysetiawan May 6, 2024
3d3e806
feat: add unit test on provider git api
okysetiawan May 6, 2024
3199ef0
feat: handle when git response is nil
okysetiawan May 7, 2024
2b53821
feat: update message log for plan subcommand
okysetiawan May 7, 2024
21dd699
feat: plan for migrate command
deryrahman May 13, 2024
eea4c92
feat: append additional plan update for every migrate operation
deryrahman May 14, 2024
9654669
feat: update file creation and format
okysetiawan May 14, 2024
e89ac4a
fix: return error missing on open file
okysetiawan May 14, 2024
c7d9ba9
feat: plan operation json serialization
okysetiawan May 14, 2024
bce0657
feat: add composite plan for handle migrate plan
okysetiawan May 15, 2024
a24f288
chore: add comment and update verbose log on plan command
okysetiawan May 15, 2024
85985d1
refactor: update plan coordinator to compositor
okysetiawan May 16, 2024
5ba6baf
feat: update compositor.GetAll as Merge result
okysetiawan May 16, 2024
a4b8efb
feat: removing plan sorting and simplify Plan Operation
okysetiawan May 16, 2024
140a44b
chore: remove unused go mod
okysetiawan May 17, 2024
71c5a3b
refactor: remove compositor from plan and move to plans
okysetiawan May 21, 2024
52f715a
chore: update method name from merge to specific merge migrate operation
okysetiawan May 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions client/cmd/internal/plan/compositor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package plan

type Compositor struct {
deleteOperation map[string]Queue[*Plan]
createOperation map[string]Queue[*Plan]
result []*Plan
}

func NewCompositor() Compositor {
return Compositor{
deleteOperation: make(map[string]Queue[*Plan]),
createOperation: make(map[string]Queue[*Plan]),
result: make([]*Plan, 0),
}
}

// Add will append Plan and enqueue create and delete plan Operation
func (compositor *Compositor) Add(addPlan *Plan) {
switch addPlan.Operation {
case OperationCreate:
createPlansByName, exist := compositor.createOperation[addPlan.KindName]
if !exist {
createPlansByName = NewQueue[*Plan]()
}
createPlansByName.Push(addPlan)
compositor.createOperation[addPlan.KindName] = createPlansByName
case OperationDelete:
deletePlanByName, exist := compositor.deleteOperation[addPlan.KindName]
if !exist {
deletePlanByName = NewQueue[*Plan]()
}
deletePlanByName.Push(addPlan)
compositor.deleteOperation[addPlan.KindName] = deletePlanByName
default:
compositor.result = append(compositor.result, addPlan)
}
}

func (compositor *Compositor) Merge() []*Plan {
if len(compositor.createOperation)+len(compositor.deleteOperation) == 0 {
return compositor.result
}

compositor.result = append(compositor.result, compositor.getPlan(compositor.createOperation, compositor.deleteOperation)...)
compositor.result = append(compositor.result, compositor.getPlan(compositor.deleteOperation, compositor.createOperation)...)
return compositor.result
}

// getPlan will dequeue create and delete plan Operation
// if there are same Plan.KindName on different Plan.NamespaceName, it will be merged as one plan with OperationMigrate
// else it will append as result directly
func (Compositor) getPlan(createOperation, deleteOperation map[string]Queue[*Plan]) []*Plan {
var res []*Plan
for kindName, createPlanQueue := range createOperation {
if createPlanQueue == nil {
continue
}

deletePlanQueue := deleteOperation[kindName]
for createPlanQueue.Next() {
if !deletePlanQueue.Next() {
res = append(res, createPlanQueue.Pop())
continue
}

deletePlan := deletePlanQueue.Pop()
migratePlan := createPlanQueue.Pop()
migratePlan.Operation = OperationMigrate
migratePlan.OldNamespaceName = &deletePlan.NamespaceName
res = append(res, migratePlan)
}
createOperation[kindName] = createPlanQueue
deleteOperation[kindName] = deletePlanQueue
}
return res
}
139 changes: 139 additions & 0 deletions client/cmd/internal/plan/compositor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package plan_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/goto/optimus/client/cmd/internal/plan"
)

func TestPlanCompositor(t *testing.T) {
var (
projectName = "p-optimus-1"
job1 = "j-job-1"
namespace1 = "n-optimus-1"
namespace2 = "n-optimus-2"
)

t.Run("case migration only", func(t *testing.T) {
inputs := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationDelete, KindName: job1},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: job1},
}
expected := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationMigrate, KindName: job1, OldNamespaceName: &namespace1},
}

compositor := plan.NewCompositor()
for i := range inputs {
compositor.Add(inputs[i])
}

actual := compositor.Merge()
assert.ElementsMatch(t, actual, expected)
})

t.Run("case create update delete on different kind name and namespace", func(t *testing.T) {
inputs := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationCreate, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationDelete, KindName: "job-3"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationDelete, KindName: "job-3"},
}
expected := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationCreate, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationDelete, KindName: "job-3"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationDelete, KindName: "job-3"},
}

compositor := plan.NewCompositor()
for i := range inputs {
compositor.Add(inputs[i])
}

actual := compositor.Merge()
assert.Len(t, actual, len(expected), "actual has %d length, but expect has %d length", len(actual), len(expected))
assert.ElementsMatch(t, actual, expected)
})

t.Run("case migration from create to delete", func(t *testing.T) {
inputs := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationCreate, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationDelete, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: "job-3"},
}
expected := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationMigrate, KindName: "job-1", OldNamespaceName: &namespace2},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: "job-3"},
}

compositor := plan.NewCompositor()
for i := range inputs {
compositor.Add(inputs[i])
}

actual := compositor.Merge()
assert.ElementsMatch(t, actual, expected)
})

t.Run("case migration from delete to create", func(t *testing.T) {
inputs := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationDelete, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: "job-1"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: "job-3"},
}
expected := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationMigrate, KindName: "job-1", OldNamespaceName: &namespace1},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationUpdate, KindName: "job-2"},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: "job-3"},
}

compositor := plan.NewCompositor()
for i := range inputs {
compositor.Add(inputs[i])
}

actual := compositor.Merge()
assert.ElementsMatch(t, actual, expected)
})

t.Run("case multiple migration on multiple namespace", func(t *testing.T) {
namespace3, namespace4, namespace5 := "n-optimus-3", "n-optimus-4", "n-optimus-5"
inputs := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace1, Operation: plan.OperationDelete, KindName: job1},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationCreate, KindName: job1},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace3, Operation: plan.OperationCreate, KindName: job1},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace4, Operation: plan.OperationDelete, KindName: job1},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace5, Operation: plan.OperationCreate, KindName: job1},
}
expected := []*plan.Plan{
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace2, Operation: plan.OperationMigrate, KindName: job1, OldNamespaceName: &namespace1},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace3, Operation: plan.OperationMigrate, KindName: job1, OldNamespaceName: &namespace4},
{Kind: plan.KindJob, ProjectName: projectName, NamespaceName: namespace5, Operation: plan.OperationCreate, KindName: job1},
}

compositor := plan.NewCompositor()
for i := range inputs {
compositor.Add(inputs[i])
}

actual := compositor.Merge()
assert.ElementsMatch(t, actual, expected)

// - make sure the actual result still same when its called multiple times
assert.ElementsMatch(t, compositor.Merge(), expected)
})
}
8 changes: 8 additions & 0 deletions client/cmd/internal/plan/kind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package plan

type Kind string

const (
KindJob Kind = "job"
KindResource Kind = "resource"
)
29 changes: 29 additions & 0 deletions client/cmd/internal/plan/operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package plan

import "encoding/json"

// Operation determine Plan Operation
type Operation string

const (
OperationDelete Operation = "delete"
OperationCreate Operation = "create"
OperationMigrate Operation = "migrate"
OperationUpdate Operation = "update"
)

// String is fmt.Stringer implementation
func (o Operation) String() string { return string(o) }

// MarshalJSON implement json.Marshaler with returning its string value
func (o Operation) MarshalJSON() ([]byte, error) { return json.Marshal(o.String()) }

// UnmarshalJSON implement json.Unmarshaler and initialized based on string value of Operation
func (o *Operation) UnmarshalJSON(value []byte) error {
var operationValue string
if err := json.Unmarshal(value, &operationValue); err != nil {
return err
}
*o = Operation(operationValue)
return nil
}
15 changes: 15 additions & 0 deletions client/cmd/internal/plan/plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package plan

type Plan struct {
Kind Kind `json:"kind"`
ProjectName string `json:"project_name"`
NamespaceName string `json:"namespace_name"`
KindName string `json:"kind_name"`
Operation Operation `json:"operation"`
Executed bool `json:"executed"`

// OldNamespaceName used when Operation is OperationMigrate where it used on migrate namespace command / API
OldNamespaceName *string `json:"old_namespace_name"`
}

type Plans []*Plan
24 changes: 24 additions & 0 deletions client/cmd/internal/plan/queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package plan

type Queue[T any] []T

func NewQueue[T any]() Queue[T] { return make(Queue[T], 0) }

func (q *Queue[T]) Push(value T) {
*q = append(*q, value)
}

func (q *Queue[T]) Pop() T {
var front T
if !q.Next() {
return front
}

front = (*q)[0]
*q = (*q)[1:]
return front
}

func (q *Queue[T]) Next() bool {
return len(*q) > 0
}
1 change: 1 addition & 0 deletions client/cmd/job/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func NewJobCommand() *cobra.Command {
NewJobRunInputCommand(),
NewChangeNamespaceCommand(),
NewDeleteCommand(),
NewPlanCommand(),
)
return cmd
}
Loading
Loading