diff --git a/migration/action.go b/migration/action.go index ae4a58d..cb4a6d1 100644 --- a/migration/action.go +++ b/migration/action.go @@ -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, diff --git a/migration/analyzer.go b/migration/analyzer.go index e7f0a88..559994f 100644 --- a/migration/analyzer.go +++ b/migration/analyzer.go @@ -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 diff --git a/migration/conflict.go b/migration/conflict.go new file mode 100644 index 0000000..736a9a5 --- /dev/null +++ b/migration/conflict.go @@ -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) +} diff --git a/migration/generate.go b/migration/generate.go index 8ed277c..601662b 100644 --- a/migration/generate.go +++ b/migration/generate.go @@ -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() diff --git a/migration/migration.go b/migration/migration.go index eb0a7c8..078ea58 100644 --- a/migration/migration.go +++ b/migration/migration.go @@ -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" { @@ -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...) } diff --git a/migration/resolver.go b/migration/resolver.go index 06853c5..77d38c1 100644 --- a/migration/resolver.go +++ b/migration/resolver.go @@ -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 } diff --git a/migration/subject.go b/migration/subject.go new file mode 100644 index 0000000..f43a34a --- /dev/null +++ b/migration/subject.go @@ -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 +}