Skip to content

Commit

Permalink
Merge pull request #11 from takaishi/feat/support-removed-block
Browse files Browse the repository at this point in the history
feat: support removed block
  • Loading branch information
takaishi authored Aug 31, 2024
2 parents d8d459e + f3701c5 commit 4d16280
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 9 additions & 3 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
144 changes: 144 additions & 0 deletions removed.go
Original file line number Diff line number Diff line change
@@ -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
}
82 changes: 82 additions & 0 deletions removed_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 4d16280

Please sign in to comment.