Skip to content

Commit

Permalink
Module support
Browse files Browse the repository at this point in the history
- `-target` support resource address with module spec prefixed. E.g. `module.mod1.module.mod2[0].module.mod3["foo"].null_resource.test`
- When no `-target` is specified, all managed resources in the state will generate their configs, including the ones resides in the nested modules.
  • Loading branch information
magodo committed Dec 14, 2022
1 parent df91bc7 commit 27c06e7
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 69 deletions.
118 changes: 112 additions & 6 deletions addr/addr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,124 @@ package addr

import (
"fmt"
"regexp"
"strconv"
"strings"
)

type ResourceAddr struct {
Type string
type ModuleStep struct {
Name string

// At most one of below is not nil
Key *string
Index *int
}

func (step ModuleStep) String() string {
out := "module." + step.Name
switch {
case step.Key != nil:
out += `["` + *step.Key + `"]`
case step.Index != nil:
out += `[` + strconv.Itoa(*step.Index) + `]`
}
return out
}

type ModuleAddr []ModuleStep

func (addr ModuleAddr) String() string {
var segs []string
for _, ms := range addr {
segs = append(segs, ms.String())
}
if len(segs) == 0 {
return ""
}
return strings.Join(segs, ".")
}

func ParseAddress(addr string) (*ResourceAddr, error) {
func ParseModuleAddr(addr string) (ModuleAddr, error) {
segs := strings.Split(addr, ".")
if len(segs) != 2 {
return nil, fmt.Errorf("invalid resource address found: %s", addr)
if len(segs)%2 != 0 {
return nil, fmt.Errorf("invalid module address")
}

var maddr ModuleAddr
p := regexp.MustCompile(`^([^\[\]]+)(\[(.+)\])?$`)
for i := 0; i < len(segs); i += 2 {
if segs[i] != "module" {
return nil, fmt.Errorf(`expect "module", got %q`, segs[i])
}
moduleSeg := segs[i+1]
matches := p.FindStringSubmatch(moduleSeg)
if len(matches) == 0 {
return nil, fmt.Errorf("invalid module segment: %s", moduleSeg)
}
ms := ModuleStep{
Name: matches[1],
}
if matches[3] == "" {
if matches[2] != "" {
return nil, fmt.Errorf("invalid module segment: %s", moduleSeg)
}
} else {
idxLit := matches[3]
if strings.HasPrefix(idxLit, `"`) && strings.HasSuffix(idxLit, `"`) {
key, err := strconv.Unquote(idxLit)
if err != nil {
return nil, fmt.Errorf("unquoting module key %s: %v", idxLit, err)
}
ms.Key = &key
} else {
idx, err := strconv.Atoi(idxLit)
if err != nil {
return nil, fmt.Errorf("converting module index to number %s: %v", idxLit, err)
}
ms.Index = &idx
}
}
maddr = append(maddr, ms)
}
return &ResourceAddr{Type: segs[0], Name: segs[1]}, nil
return maddr, nil
}

type ResourceAddr struct {
ModuleAddr ModuleAddr
Type string
Name string
}

func (addr ResourceAddr) String() string {
raddr := addr.Type + "." + addr.Name
if moduleAddr := addr.ModuleAddr.String(); moduleAddr != "" {
raddr = moduleAddr + "." + raddr
}
return raddr
}

func ParseResourceAddr(addr string) (*ResourceAddr, error) {
segs := strings.Split(addr, ".")

if len(segs)%2 != 0 {
return nil, fmt.Errorf("invalid resource address")
}

raddr := &ResourceAddr{
Type: segs[len(segs)-2],
Name: segs[len(segs)-1],
}

if len(segs) == 2 {
return raddr, nil
}

maddr, err := ParseModuleAddr(strings.Join(segs[:len(segs)-2], "."))
if err != nil {
return nil, err
}

raddr.ModuleAddr = maddr
return raddr, nil

}
206 changes: 206 additions & 0 deletions addr/addr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package addr

import (
"testing"

"github.com/stretchr/testify/require"
)

func ptr[T any](in T) *T {
return &in
}

func TestParseModuleAddr(t *testing.T) {
cases := []struct {
name string
input string
addr ModuleAddr
err bool
}{
{
name: "one module",
input: "module.mod1",
addr: []ModuleStep{
{
Name: "mod1",
},
},
},
{
name: "module instance (key)",
input: `module.mod1["foo"]`,
addr: []ModuleStep{
{
Name: "mod1",
Key: ptr("foo"),
},
},
},
{
name: "module instance (idx)",
input: `module.mod1[0]`,
addr: []ModuleStep{
{
Name: "mod1",
Index: ptr(0),
},
},
},
{
name: "nested module instance",
input: `module.mod1[0].module.mod2["foo"].module.mod3`,
addr: []ModuleStep{
{
Name: "mod1",
Index: ptr(0),
},
{
Name: "mod2",
Key: ptr("foo"),
},
{
Name: "mod3",
},
},
},
{
name: "invalid module",
input: "mod1",
err: true,
},
{
name: "invalid module instance",
input: "module.mod1[]",
err: true,
},
{
name: "invalid module instance key",
input: "module.mod1[xyz]",
err: true,
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
addr, err := ParseModuleAddr(tt.input)
if tt.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.addr, addr)
})
}
}

func TestParseResourceAddr(t *testing.T) {
cases := []struct {
name string
input string
addr ResourceAddr
err bool
}{
{
name: "resource only",
input: "null_resource.test",
addr: ResourceAddr{
Type: "null_resource",
Name: "test",
},
},
{
name: "resource with module",
input: "module.mod1.null_resource.test",
addr: ResourceAddr{
ModuleAddr: []ModuleStep{
{
Name: "mod1",
},
},
Type: "null_resource",
Name: "test",
},
},
{
name: "resource with module instance (key)",
input: `module.mod1["foo"].null_resource.test`,
addr: ResourceAddr{
ModuleAddr: []ModuleStep{
{
Name: "mod1",
Key: ptr("foo"),
},
},
Type: "null_resource",
Name: "test",
},
},
{
name: "resource with module instance (idx)",
input: `module.mod1[0].null_resource.test`,
addr: ResourceAddr{
ModuleAddr: []ModuleStep{
{
Name: "mod1",
Index: ptr(0),
},
},
Type: "null_resource",
Name: "test",
},
},
{
name: "resource with nested module instance",
input: `module.mod1[0].module.mod2["foo"].module.mod3.null_resource.test`,
addr: ResourceAddr{
ModuleAddr: []ModuleStep{
{
Name: "mod1",
Index: ptr(0),
},
{
Name: "mod2",
Key: ptr("foo"),
},
{
Name: "mod3",
},
},
Type: "null_resource",
Name: "test",
},
},
{
name: "invalid resource addr",
input: "null_resource",
err: true,
},
{
name: "invalid resource addr with module",
input: "mod1.null_resource.test",
err: true,
},
{
name: "invalid resource addr with module instance",
input: "module.mod1[].null_resource.test",
err: true,
},
{
name: "invalid resource addr with module instance key",
input: "module.mod1[xyz].null_resource.test",
err: true,
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
addr, err := ParseResourceAddr(tt.input)
if tt.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.addr, *addr)
})
}
}
22 changes: 4 additions & 18 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/hashicorp/hc-install/fs"
"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/terraform-exec/tfexec"
"github.com/magodo/tfadd/addr"
"github.com/magodo/tfadd/tfadd"
"github.com/mitchellh/cli"
)
Expand Down Expand Up @@ -63,28 +62,15 @@ Usage: tfadd [global options] state [options]
Options:
-full Output all non-computed properties in the generated config
-target=addr Only generate for the specified resource, can be specified multiple times
-target=addr Only generate for the specified resource
`
return strings.TrimSpace(helpText)
}

type targetFlag []string

func (f *targetFlag) String() string {
return fmt.Sprint(*f)
}

func (f *targetFlag) Set(value string) error {
*f = append(*f, value)
_, err := addr.ParseAddress(value)
return err
}

func (r *stateCommand) Run(args []string) int {
var targets targetFlag
fset := defaultFlagSet("state")
flagFull := fset.Bool("full", false, "Whether to generate all non-computed properties")
fset.Var(&targets, "target", "Only generate for the specified resource")
flagTarget := fset.String("target", "", "Only generate for the specified resource")
if err := fset.Parse(args); err != nil {
fmt.Fprintf(os.Stderr, err.Error())
return 1
Expand All @@ -105,8 +91,8 @@ func (r *stateCommand) Run(args []string) int {
return 1
}
opts := []tfadd.StateOption{tfadd.Full(*flagFull)}
for _, target := range targets {
opts = append(opts, tfadd.Target(target))
if *flagTarget != "" {
opts = append(opts, tfadd.Target(*flagTarget))
}
templates, err := tfadd.State(ctx, tf, opts...)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion tfadd/internal/state_to_tpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

func StateToTpl(r *tfstate.StateResource, schema *tfjson.SchemaBlock) ([]byte, error) {
var buf strings.Builder
addr, err := addr2.ParseAddress(r.Address)
addr, err := addr2.ParseResourceAddr(r.Address)
if err != nil {
return nil, fmt.Errorf("parsing resource address: %v", err)
}
Expand Down
Loading

0 comments on commit 27c06e7

Please sign in to comment.