Skip to content

Commit

Permalink
Add terraform state list command
Browse files Browse the repository at this point in the history
  • Loading branch information
minamijoyo committed Jul 19, 2020
1 parent e33196a commit be12241
Show file tree
Hide file tree
Showing 3 changed files with 277 additions and 0 deletions.
4 changes: 4 additions & 0 deletions test-fixtures/backend_s3/main.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
resource "aws_security_group" "foo" {
name = "foo"
}

resource "aws_security_group" "bar" {
name = "bar"
}
46 changes: 46 additions & 0 deletions tfexec/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"regexp"
"strings"
)

// tfVersionRe is a pattern to parse outputs from terraform version.
Expand Down Expand Up @@ -39,6 +40,10 @@ type TerraformCLI interface {
// Destroy destroys resources.
Destroy(ctx context.Context, dir string, opts ...string) error

// StateList shows a list of resources.
// If a state is given, use it for the input state.
StateList(ctx context.Context, state *State, addresses []string, opts ...string) ([]string, error)

// StatePull returns the current tfstate from remote.
StatePull(ctx context.Context) (State, error)

Expand Down Expand Up @@ -124,6 +129,37 @@ func (c *terraformCLI) Destroy(ctx context.Context, dir string, opts ...string)
return err
}

// StateList shows a list of resources.
// If a state is given, use it for the input state.
func (c *terraformCLI) StateList(ctx context.Context, state *State, addresses []string, opts ...string) ([]string, error) {
args := []string{"state", "list"}

if state != nil {
if hasPrefixOptions(opts, "-state=") {
return nil, fmt.Errorf("failed to build `terraform state list` options. The state argument (!= nil) and the -state= option cannot be set at the same time: state=%v, opts=%v", state, opts)
}
tmpfile, err := writeTempFile([]byte(*state))
defer os.Remove(tmpfile.Name())
if err != nil {
return nil, err
}
args = append(args, "-state="+tmpfile.Name())
}

args = append(args, opts...)

if len(addresses) > 0 {
args = append(args, addresses...)
}

stdout, err := c.run(ctx, args...)
if err != nil {
return nil, err
}

return strings.Split(strings.TrimRight(stdout, "\n"), "\n"), nil
}

// StatePull returns the current tfstate from remote.
func (c *terraformCLI) StatePull(ctx context.Context) (State, error) {
stdout, err := c.run(ctx, "state", "pull")
Expand Down Expand Up @@ -163,3 +199,13 @@ func writeTempFile(content []byte) (*os.File, error) {

return tmpfile, nil
}

// hasPrefixOptions returns true if any element in a list of string has a given prefix.
func hasPrefixOptions(opts []string, prefix string) bool {
for _, opt := range opts {
if strings.HasPrefix(opt, prefix) {
return true
}
}
return false
}
227 changes: 227 additions & 0 deletions tfexec/terraform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tfexec

import (
"context"
"reflect"
"regexp"
"testing"
)
Expand Down Expand Up @@ -293,6 +294,140 @@ func TestTerraformCLIDestroy(t *testing.T) {
}
}

func TestTerraformCLIStateList(t *testing.T) {
state := State(testStateListState)
stdout := `aws_security_group.bar
aws_security_group.foo
`

cases := []struct {
desc string
mockCommands []*mockCommand
state *State
addresses []string
opts []string
want []string
ok bool
}{
{
desc: "no addresses and no opts",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "state", "list"},
stdout: stdout,
exitCode: 0,
},
},
state: nil,
want: []string{"aws_security_group.bar", "aws_security_group.foo"},
ok: true,
},
{
desc: "failed to run terraform state list",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "state", "list"},
exitCode: 1,
},
},
state: nil,
want: nil,
ok: false,
},
{
desc: "with addresses",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "state", "list", "aws_instance.example", "module.example"},
stdout: stdout,
exitCode: 0,
},
},
state: nil,
addresses: []string{"aws_instance.example", "module.example"},
want: []string{"aws_security_group.bar", "aws_security_group.foo"},
ok: true,
},
{
desc: "with opts",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "state", "list", "-state=foo.tfstate", "-id=bar"},
stdout: stdout,
exitCode: 0,
},
},
state: nil,
opts: []string{"-state=foo.tfstate", "-id=bar"},
want: []string{"aws_security_group.bar", "aws_security_group.foo"},
ok: true,
},
{
desc: "with addresses and opts",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "state", "list", "-state=foo.tfstate", "-id=bar", "aws_instance.example", "module.example"},
stdout: stdout,
exitCode: 0,
},
},
state: nil,
addresses: []string{"aws_instance.example", "module.example"},
opts: []string{"-state=foo.tfstate", "-id=bar"},
want: []string{"aws_security_group.bar", "aws_security_group.foo"},
ok: true,
},
{
desc: "with state",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "state", "list", "-state=/path/to/tempfile", "-id=bar", "aws_instance.example", "module.example"},
argsRe: regexp.MustCompile(`^terraform state list -state=.+ -id=bar aws_instance.example module.example$`),
stdout: stdout,
exitCode: 0,
},
},
state: &state,
addresses: []string{"aws_instance.example", "module.example"},
opts: []string{"-id=bar"},
want: []string{"aws_security_group.bar", "aws_security_group.foo"},
ok: true,
},
{
desc: "with state and -state= (conflict error)",
mockCommands: []*mockCommand{
{
args: []string{"terraform", "state", "list", "-state=/path/to/tempfile", "-id=bar", "-state=foo.tfstate", "aws_instance.example", "module.example"},
argsRe: regexp.MustCompile(`^terraform state list -state=\S+ -id=bar -state=foo.tfstate aws_instance.example module.example$`),
exitCode: 1,
},
},
state: &state,
addresses: nil,
opts: []string{"-id=bar", "-state=foo.tfstate"},
want: nil,
ok: false,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
e := NewMockExecutor(tc.mockCommands)
terraformCLI := NewTerraformCLI(e)
got, err := terraformCLI.StateList(context.Background(), tc.state, tc.addresses, tc.opts...)
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
}
if !tc.ok && err == nil {
t.Fatal("expected to return an error, but no error")
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got: %v, want: %v", got, tc.want)
}
})
}
}

func TestTerraformCLIStatePull(t *testing.T) {
tfstate := `{
"version": 4,
Expand Down Expand Up @@ -408,3 +543,95 @@ func TestTerraformCLIStatePush(t *testing.T) {
})
}
}

const testStateListState = `
{
"version": 4,
"terraform_version": "0.12.28",
"serial": 1,
"lineage": "a19299f0-68d7-3763-56ca-15ae05f60684",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_security_group",
"name": "bar",
"provider": "provider.aws",
"instances": [
{
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ec2:ap-northeast-1:000000000000:security-group/sg-ecde6356",
"description": "Managed by Terraform",
"egress": [
{
"cidr_blocks": [
"0.0.0.0/0"
],
"description": "",
"from_port": 0,
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "-1",
"security_groups": [],
"self": false,
"to_port": 0
}
],
"id": "sg-ecde6356",
"ingress": [],
"name": "bar",
"name_prefix": null,
"owner_id": "000000000000",
"revoke_rules_on_delete": false,
"tags": null,
"timeouts": null,
"vpc_id": ""
},
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0="
}
]
},
{
"mode": "managed",
"type": "aws_security_group",
"name": "foo",
"provider": "provider.aws",
"instances": [
{
"schema_version": 1,
"attributes": {
"arn": "arn:aws:ec2:ap-northeast-1:000000000000:security-group/sg-d1ff4d60",
"description": "Managed by Terraform",
"egress": [
{
"cidr_blocks": [
"0.0.0.0/0"
],
"description": "",
"from_port": 0,
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "-1",
"security_groups": [],
"self": false,
"to_port": 0
}
],
"id": "sg-d1ff4d60",
"ingress": [],
"name": "foo",
"name_prefix": null,
"owner_id": "000000000000",
"revoke_rules_on_delete": false,
"tags": {},
"timeouts": null,
"vpc_id": ""
},
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0="
}
]
}
]
}
`

0 comments on commit be12241

Please sign in to comment.