Skip to content

Commit

Permalink
Abstract problem solving so that it can be extended other than import
Browse files Browse the repository at this point in the history
  • Loading branch information
minamijoyo committed Apr 22, 2022
1 parent 0f0a961 commit 2e04eae
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 28 deletions.
10 changes: 5 additions & 5 deletions migration/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ package migration

import "fmt"

// Action is an interface of terraform command for state migration.
type Action interface {
// StateAction is an interface of terraform command for state migration.
type StateAction interface {
// MigrationAction returns a string of terraform command for state migration.
MigrationAction() string
}

// StateImportAction implements the Action interface.
// StateImportAction implements the StateAction interface.
type StateImportAction struct {
address string
id string
}

var _ Action = (*StateImportAction)(nil)
var _ StateAction = (*StateImportAction)(nil)

// NewStateImportAction returns a new instance of StateImportAction.
func NewStateImportAction(address string, id string) Action {
func NewStateImportAction(address string, id string) StateAction {
return &StateImportAction{
address: address,
id: id,
Expand Down
30 changes: 22 additions & 8 deletions migration/analyzer.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
package migration

type Analyzer interface {
// PlanAnalyzer is an interface that abstracts the analysis rules of plan.
type PlanAnalyzer interface {
// Analyze analyzes a given plan and generates a state migration so that
// the plan results in no changes.
Analyze(plan *Plan) *StateMigration
}

type defaultAnalyzer struct {
// defaultPlanAnalyzer is a default implementation for PlanAnalyzer.
// This is a predefined rule-based analyzer.
type defaultPlanAnalyzer struct {
// A list of rules used for analysis.
resolvers []Resolver
}

var _ Analyzer = (*defaultAnalyzer)(nil)
var _ PlanAnalyzer = (*defaultPlanAnalyzer)(nil)

func NewDefaultAnalyzer() Analyzer {
return &defaultAnalyzer{
// NewDefaultPlanAnalyzer returns a new instance of defaultPlanAnalyzer.
// The current implementation only supports import, but allows us to compose
// multiple resolvers for future extension.
func NewDefaultPlanAnalyzer() PlanAnalyzer {
return &defaultPlanAnalyzer{
resolvers: []Resolver{
&StateImportResolver{},
},
}
}

func (a *defaultAnalyzer) Analyze(plan *Plan) *StateMigration {
var migration StateMigration
// Analyze analyzes a given plan and generates a state migration so that
// the plan results in no changes.
func (a *defaultPlanAnalyzer) Analyze(plan *Plan) *StateMigration {
subject := NewSubject(plan)

var migration StateMigration
current := subject
for _, r := range a.resolvers {
actions := r.Resolve(plan)
next, actions := r.Resolve(current)
migration.AppendActions(actions...)
current = next
}

return &migration
Expand Down
68 changes: 68 additions & 0 deletions migration/conflict.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package migration

import (
tfjson "github.com/hashicorp/terraform-json"
"github.com/minamijoyo/tfedit/migration/schema"
)

// Conflict is a planned resource change.
// It also has a status of whether it has already been resolved.
type Conflict struct {
// A planned resource change.
rc *tfjson.ResourceChange
// A flag indicating that it has already been resolved.
// The state mv operation reduces two conflicts to a single state migration
// action, so we need a flag to see if it has already been processed.
resolved bool
}

// NewConflict returns a new instalce of Conflict.
func NewConflict(rc *tfjson.ResourceChange) *Conflict {
return &Conflict{
rc: rc,
resolved: false,
}
}

// MarkAsResolved marks the conflict as resolved.
func (c *Conflict) MarkAsResolved() {
c.resolved = true
}

// IsResolved return true if the conflict has already been resolved.
func (c *Conflict) IsResolved() bool {
return c.resolved
}

// PlannedActionType returns a string that represents the type of action.
// Currently some actions that may be included in the plan are not supported.
// It returns "unknown" if not supported.
// The valid values are:
// - create
// - unknown
func (c *Conflict) PlannedActionType() string {
switch {
case c.rc.Change.Actions.Create():
return "create"
default:
return "unknown"
}

}

// ResourceType returns a resource type. (e.g. aws_s3_bucket_acl)
func (c *Conflict) ResourceType() string {
return c.rc.Type
}

// Address returns an absolute address. (e.g. aws_s3_bucket_acl.example)
func (c *Conflict) Address() string {
return c.rc.Address
}

// ResourceAfter retruns a planned resource after change.
// It doesn't contains attributes known after apply.
func (c *Conflict) ResourceAfter() schema.Resource {
after := c.rc.Change.After.(map[string]interface{})
return schema.Resource(after)
}
3 changes: 2 additions & 1 deletion migration/generate.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package migration

// Generate returns bytes of a migration file which reverts a given planned changes.
func Generate(planJSON []byte) ([]byte, error) {
plan, err := NewPlan(planJSON)
if err != nil {
return nil, err
}

analyzer := NewDefaultAnalyzer()
analyzer := NewDefaultPlanAnalyzer()
migration := analyzer.Analyze(plan)

return migration.Render()
Expand Down
7 changes: 3 additions & 4 deletions migration/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import (
// StateMigration is a type which is equivalent to tfmigrate.StateMigratorConfig of
// minamijoyo/tfmigrate.
// The current implementation doesn't encode migration actions to a file
// directly with gohcl, so we avoid to depend on tfmigrate's type and we define
// only what we need here.
// directly with gohcl, so we define only what we need here.
type StateMigration struct {
// Dir is a working directory for executing terraform command.
Dir string
// Actions is a list of state action.
Actions []Action
Actions []StateAction
}

var migrationTemplate = `migration "state" "awsv4upgrade" {
Expand All @@ -30,7 +29,7 @@ var migrationTemplate = `migration "state" "awsv4upgrade" {
var compiledMigrationTemplate = template.Must(template.New("migration").Parse(migrationTemplate))

// AppendActions appends a list of actions to migration.
func (m *StateMigration) AppendActions(actions ...Action) {
func (m *StateMigration) AppendActions(actions ...StateAction) {
m.Actions = append(m.Actions, actions...)
}

Expand Down
31 changes: 21 additions & 10 deletions migration/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,37 @@ import (
_ "github.com/minamijoyo/tfedit/migration/schema/aws" // Register schema for aws
)

// Resolver is an interface that abstracts a rule for solving a subject.
type Resolver interface {
Resolve(plan *Plan) []Action
// Resolve tries to resolve some conflicts in a given subject and returns the
// updated subject and state migration actions.
Resolve(s *Subject) (*Subject, []StateAction)
}

// StateImportResolver is an implementation of Resolver for import.
type StateImportResolver struct {
}

var _ Resolver = (*StateImportResolver)(nil)

func (r *StateImportResolver) Resolve(plan *Plan) []Action {
actions := []Action{}
for _, rc := range plan.ResourceChanges() {
if rc.Change.Actions.Create() {
address := rc.Address
after := rc.Change.After.(map[string]interface{})
importID := schema.ImportID(rc.Type, after)
action := NewStateImportAction(address, importID)
// Resolve tries to resolve some conflicts in a given subject and returns the
// updated subject and state migration actions.
// It translates a planned create action into an import state migration.
func (r *StateImportResolver) Resolve(s *Subject) (*Subject, []StateAction) {
actions := []StateAction{}
for _, c := range s.Conflicts() {
if c.IsResolved() {
continue
}

switch c.PlannedActionType() {
case "create":
importID := schema.ImportID(c.ResourceType(), c.ResourceAfter())
action := NewStateImportAction(c.Address(), importID)
actions = append(actions, action)
c.MarkAsResolved()
}
}

return actions
return s, actions
}
36 changes: 36 additions & 0 deletions migration/subject.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package migration

// Subject is a problem to be solved. It contains multiple conflicts.
type Subject struct {
// A list of conflicts to be solved.
conflicts []*Conflict
}

// NewSubject finds conflicts contained in a given plan and defines a problem.
func NewSubject(plan *Plan) *Subject {
conflicts := []*Conflict{}
for _, rc := range plan.ResourceChanges() {
c := NewConflict(rc)
conflicts = append(conflicts, c)
}

return &Subject{
conflicts: conflicts,
}
}

// Conflicts returns a list of conflicts. It may include already resolved.
func (s *Subject) Conflicts() []*Conflict {
return s.conflicts
}

// IsResolved returns true if all conflicts have been resolved, otherwise false.
func (s *Subject) IsResolved() bool {
for _, c := range s.conflicts {
if !c.IsResolved() {
return false
}
}

return true
}

0 comments on commit 2e04eae

Please sign in to comment.