diff --git a/README.md b/README.md index a6b92a3..6ce2748 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ go install github.com/takaishi/tfclean/cmd/tfclean - Blocks - [x] Remove moved blocks that is applied. - [x] Remove import blocks that is applied. - - [ ] Remove removed blocks that is applied. + - [x] Remove removed blocks that is applied. - [ ] Forcefully remove all moved/import/removed blocks. - Confirm block is already applied or not to read tfstate (provided by https://github.com/fujiwara/tfstate-lookup) diff --git a/app.go b/app.go index f4f7594..eeaefcf 100644 --- a/app.go +++ b/app.go @@ -3,13 +3,14 @@ package tfclean import ( "context" "fmt" + "os" + "path/filepath" + "strings" + "github.com/fujiwara/tfstate-lookup/tfstate" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/hcl/v2/hclsyntax" - "os" - "path/filepath" - "strings" ) type App struct { @@ -79,6 +80,11 @@ func (app *App) processFile(path string, state *tfstate.TFState) error { if err != nil { return err } + case "removed": + data, err = app.processRemovedBlock(block, state, data) + if err != nil { + return err + } } } return os.WriteFile(path, data, 0644) diff --git a/removed.go b/removed.go new file mode 100644 index 0000000..7a1ff55 --- /dev/null +++ b/removed.go @@ -0,0 +1,144 @@ +package tfclean + +import ( + "bytes" + "fmt" + "strings" + "text/scanner" + "unicode" + + "github.com/fujiwara/tfstate-lookup/tfstate" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type RemovedBlock struct { + From string + Lifecycle *LifecycleBlock +} + +type LifecycleBlock struct { + Destroy string +} + +func (app *App) processRemovedBlock(block *hclsyntax.Block, state *tfstate.TFState, data []byte) ([]byte, error) { + from, _ := app.getValueFromAttribute(block.Body.Attributes["from"]) + isApplied, err := app.removedBlockIsApplied(state, from) + if err != nil { + return data, err + } + if isApplied { + data, err = app.cutRemovedBlock(data, from) + if err != nil { + return data, err + } + } + return data, nil +} + +func (app *App) removedBlockIsApplied(state *tfstate.TFState, from string) (bool, error) { + if len(strings.Split(from, ".")) == 2 { + names, err := state.List() + if err != nil { + return false, err + } + existsFrom := false + for _, name := range names { + if strings.HasPrefix(name, from+".") { + existsFrom = true + break + } + } + if !existsFrom { + return true, nil + } + return false, nil + } else { + // from and to is resource + fromAttrs, err := state.Lookup(from) + if err != nil { + return false, err + } + if fromAttrs.String() == "null" { + return true, nil + } + return false, nil + } +} + +func (app *App) cutRemovedBlock(data []byte, from string) ([]byte, error) { + s := &scanner.Scanner{} + var spos, epos int + s.Init(bytes.NewReader(data)) + s.Mode = scanner.ScanIdents | scanner.ScanFloats + s.IsIdentRune = func(ch rune, i int) bool { + return ch == '-' || ch == '_' || ch == '.' || ch == '"' || ch == '[' || ch == ']' || unicode.IsLetter(ch) || unicode.IsDigit(ch) && i > 0 + } + + for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() { + switch s.TokenText() { + case "removed": + spos = s.Offset + movedBlock := &RemovedBlock{} + var current string + for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() { + fmt.Println(s.TokenText()) + switch s.TokenText() { + case "{": + // Ignore + case "}": + // Remove moved block that includes `}` and newline + epos = s.Offset + 2 + if movedBlock.From == from { + data = bytes.Join([][]byte{data[:spos], data[epos:]}, []byte("")) + return data, nil + } + case "from": + current = "from" + case "lifecycle": + lb, err := app.readLifecycleBlock(s) + if err != nil { + return nil, err + } + movedBlock.Lifecycle = lb + case "=": + // Ignore + default: + switch current { + case "from": + movedBlock.From = s.TokenText() + default: + return nil, fmt.Errorf("unexpected token: " + s.TokenText()) + } + } + } + } + } + + return nil, nil +} + +func (app *App) readLifecycleBlock(s *scanner.Scanner) (*LifecycleBlock, error) { + lifecycleBlock := &LifecycleBlock{} + var current string + for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() { + switch s.TokenText() { + case "{": + // Ignore + case "}": + return lifecycleBlock, nil + case "destroy": + current = "destroy" + case "=": + // Ignore + default: + switch current { + case "destroy": + lifecycleBlock.Destroy = s.TokenText() + default: + return nil, fmt.Errorf("unexpected token: " + s.TokenText()) + } + } + } + + return lifecycleBlock, nil +} diff --git a/removed_test.go b/removed_test.go new file mode 100644 index 0000000..c9e6250 --- /dev/null +++ b/removed_test.go @@ -0,0 +1,82 @@ +package tfclean + +import ( + "reflect" + "testing" + + "github.com/hashicorp/hcl/v2/hclparse" +) + +func TestApp_cutRemovedBlock(t *testing.T) { + type fields struct { + hclParser *hclparse.Parser + CLI *CLI + } + type args struct { + data []byte + from string + } + tests := []struct { + name string + fields fields + args args + want []byte + wantErr bool + }{ + // TODO: Add test cases. + { + name: "", + fields: fields{}, + args: args{ + data: []byte(` +aaa +removed { + from = module.foo.hoge + lifecycle { + destroy = false + } +} +bbb +`), + from: "module.foo.hoge", + }, + want: []byte("\naaa\nbbb\n"), + wantErr: false, + }, + { + name: "", + fields: fields{}, + args: args{ + data: []byte(` +aaa +removed { + from = module.foo.hoge["aaa"] + lifecycle { + destroy = false + } +} +bbb +`), + from: "module.foo.hoge[\"aaa\"]", + }, + want: []byte("\naaa\nbbb\n"), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := &App{ + hclParser: tt.fields.hclParser, + CLI: tt.fields.CLI, + } + got, err := app.cutRemovedBlock(tt.args.data, tt.args.from) + if (err != nil) != tt.wantErr { + t.Errorf("App.cutRemovedBlock() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("App.cutRemovedBlock() = %v, want %v", got, tt.want) + } + }) + } +}