1
1
mirror of https://github.com/adammck/terraform-inventory synced 2024-11-22 15:52:01 +01:00

Support Terraform 0.12 state format (#114)

This commit is contained in:
Andreas Sommer 2019-08-01 17:21:54 +02:00 committed by Adam Mckaig
parent 3a1f433061
commit 94a66e3c5e
8 changed files with 769 additions and 63 deletions

@ -1,8 +1,9 @@
language: go
go:
- 1.5
- 1.6
- "1.8"
- "1.11.x"
- "1.x" # latest
script:
- go test -v ./...

108
cli.go

@ -42,7 +42,107 @@ func appendUniq(strs []string, item string) []string {
return strs
}
func gatherResources(s *state) map[string]interface{} {
func gatherResources(s *stateAnyTerraformVersion) map[string]interface{} {
if s.TerraformVersion == TerraformVersionPre0dot12 {
return gatherResourcesPre0dot12(&s.StatePre0dot12)
} else if s.TerraformVersion == TerraformVersion0dot12 {
return gatherResources0dot12(&s.State0dot12)
} else {
panic("Unimplemented Terraform version enum")
}
}
func gatherResourcesPre0dot12(s *state) map[string]interface{} {
outputGroups := make(map[string]interface{})
all := &allGroup{Hosts: make([]string, 0), Vars: make(map[string]interface{})}
types := make(map[string][]string)
individual := make(map[string][]string)
ordered := make(map[string][]string)
tags := make(map[string][]string)
unsortedOrdered := make(map[string][]*Resource)
resourceIDNames := s.mapResourceIDNames()
for _, res := range s.resources() {
// place in list of all resources
all.Hosts = appendUniq(all.Hosts, res.Hostname())
// place in list of resource types
tp := fmt.Sprintf("type_%s", res.resourceType)
types[tp] = appendUniq(types[tp], res.Hostname())
unsortedOrdered[res.baseName] = append(unsortedOrdered[res.baseName], res)
// store as invdividual host (eg. <name>.<count>)
invdName := fmt.Sprintf("%s.%d", res.baseName, res.counter)
if old, exists := individual[invdName]; exists {
fmt.Fprintf(os.Stderr, "overwriting already existing individual key %s, old: %v, new: %v\n", invdName, old, res.Hostname())
}
individual[invdName] = []string{res.Hostname()}
// inventorize tags
for k, v := range res.Tags() {
// Valueless
tag := k
if v != "" {
tag = fmt.Sprintf("%s_%s", k, v)
}
// if v is a resource ID, then tag should be resource name
if _, exists := resourceIDNames[v]; exists {
tag = resourceIDNames[v]
}
tags[tag] = appendUniq(tags[tag], res.Hostname())
}
}
// inventorize outputs as variables
if len(s.outputs()) > 0 {
for _, out := range s.outputs() {
all.Vars[out.keyName] = out.value
}
}
// sort the ordered groups
for basename, resources := range unsortedOrdered {
cs := counterSorter{resources}
sort.Sort(cs)
for i := range resources {
ordered[basename] = append(ordered[basename], resources[i].Hostname())
}
}
outputGroups["all"] = all
for k, v := range individual {
if old, exists := outputGroups[k]; exists {
fmt.Fprintf(os.Stderr, "individual overwriting already existing output with key %s, old: %v, new: %v", k, old, v)
}
outputGroups[k] = v
}
for k, v := range ordered {
if old, exists := outputGroups[k]; exists {
fmt.Fprintf(os.Stderr, "ordered overwriting already existing output with key %s, old: %v, new: %v", k, old, v)
}
outputGroups[k] = v
}
for k, v := range types {
if old, exists := outputGroups[k]; exists {
fmt.Fprintf(os.Stderr, "types overwriting already existing output key %s, old: %v, new: %v", k, old, v)
}
outputGroups[k] = v
}
for k, v := range tags {
if old, exists := outputGroups[k]; exists {
fmt.Fprintf(os.Stderr, "tags overwriting already existing output key %s, old: %v, new: %v", k, old, v)
}
outputGroups[k] = v
}
return outputGroups
}
func gatherResources0dot12(s *stateTerraform0dot12) map[string]interface{} {
outputGroups := make(map[string]interface{})
all := &allGroup{Hosts: make([]string, 0), Vars: make(map[string]interface{})}
@ -132,11 +232,11 @@ func gatherResources(s *state) map[string]interface{} {
return outputGroups
}
func cmdList(stdout io.Writer, stderr io.Writer, s *state) int {
func cmdList(stdout io.Writer, stderr io.Writer, s *stateAnyTerraformVersion) int {
return output(stdout, stderr, gatherResources(s))
}
func cmdInventory(stdout io.Writer, stderr io.Writer, s *state) int {
func cmdInventory(stdout io.Writer, stderr io.Writer, s *stateAnyTerraformVersion) int {
groups := gatherResources(s)
group_names := []string{}
for group, _ := range groups {
@ -190,7 +290,7 @@ func checkErr(err error, stderr io.Writer) int {
return 0
}
func cmdHost(stdout io.Writer, stderr io.Writer, s *state, hostname string) int {
func cmdHost(stdout io.Writer, stderr io.Writer, s *stateAnyTerraformVersion, hostname string) int {
for _, res := range s.resources() {
if hostname == res.Hostname() {
attributes := res.Attributes()

32
main.go

@ -51,7 +51,7 @@ func main() {
os.Exit(1)
}
var s state
var s stateAnyTerraformVersion
if !f.IsDir() {
stateFile, err := os.Open(path)
@ -69,39 +69,51 @@ func main() {
}
if f.IsDir() {
cmd := exec.Command("terraform", "state", "pull")
cmd := exec.Command("terraform", "show", "-json")
cmd.Dir = path
var out bytes.Buffer
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error running `terraform show -json` in directory %s, %s, falling back to trying Terraform pre-0.12 command\n", path, err)
cmd = exec.Command("terraform", "state", "pull")
cmd.Dir = path
out.Reset()
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error running `terraform state pull` in directory %s, %s\n", path, err)
os.Exit(1)
}
}
err = s.read(&out)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading `terraform state pull` output: %s\n", err)
fmt.Fprintf(os.Stderr, "Error reading Terraform state: %s\n", err)
os.Exit(1)
}
}
if s.TerraformVersion == TerraformVersionUnknown {
fmt.Fprintf(os.Stderr, "Unknown state format\n\nUsage: %s [options] path\npath: this is either a path to a state file or a folder from which `terraform commands` are valid\n", os.Args[0])
os.Exit(1)
}
}
if s.Modules == nil {
fmt.Printf("Usage: %s [options] path\npath: this is either a path to a state file or a folder from which `terraform commands` are valid\n", os.Args[0])
if (s.TerraformVersion == TerraformVersionPre0dot12 && s.StatePre0dot12.Modules == nil) ||
(s.TerraformVersion == TerraformVersion0dot12 && s.State0dot12.Values.RootModule == nil) {
fmt.Fprintf(os.Stderr, "No modules found in state\n\nUsage: %s [options] path\npath: this is either a path to a state file or a folder from which `terraform commands` are valid\n", os.Args[0])
os.Exit(1)
}
if *list {
os.Exit(cmdList(os.Stdout, os.Stderr, &s))
} else if *inventory {
os.Exit(cmdInventory(os.Stdout, os.Stderr, &s))
} else if *host != "" {
os.Exit(cmdHost(os.Stdout, os.Stderr, &s, *host))
}
}

@ -15,7 +15,7 @@ func NewOutput(keyName string, value interface{}) (*Output, error) {
// TODO: Warn instead of silently ignore error?
if len(keyName) == 0 {
return nil, fmt.Errorf("couldn't parse keyName: %s", keyName)
return nil, fmt.Errorf("couldn't parse output keyName: %s", keyName)
}
return &Output{

276
parser.go

@ -2,29 +2,94 @@ package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strconv"
"strings"
)
// TerraformVersion defines which version of Terraform state applies
type TerraformVersion int
const (
// TerraformVersionUnknown means unknown version
TerraformVersionUnknown TerraformVersion = 0
// TerraformVersionPre0dot12 means < 0.12
TerraformVersionPre0dot12 TerraformVersion = 1
// TerraformVersion0dot12 means >= 0.12
TerraformVersion0dot12 TerraformVersion = 2
)
type stateAnyTerraformVersion struct {
StatePre0dot12 state
State0dot12 stateTerraform0dot12
TerraformVersion TerraformVersion
}
// Terraform < v0.12
type state struct {
Modules []moduleState `json:"modules"`
}
// read populates the state object from a statefile.
func (s *state) read(stateFile io.Reader) error {
// read statefile contents
b, err := ioutil.ReadAll(stateFile)
if err != nil {
return err
type moduleState struct {
Path []string `json:"path"`
ResourceStates map[string]resourceState `json:"resources"`
Outputs map[string]interface{} `json:"outputs"`
}
type resourceState struct {
// Populated from statefile
Type string `json:"type"`
Primary instanceState `json:"primary"`
}
type instanceState struct {
ID string `json:"id"`
Attributes map[string]string `json:"attributes,omitempty"`
}
// parse into struct
err = json.Unmarshal(b, s)
if err != nil {
return err
// Terraform <= v0.12
type stateTerraform0dot12 struct {
Values valuesStateTerraform0dot12 `json:"values"`
}
type valuesStateTerraform0dot12 struct {
RootModule *moduleStateTerraform0dot12 `json:"root_module"`
Outputs map[string]interface{} `json:"outputs"`
}
type moduleStateTerraform0dot12 struct {
ResourceStates []resourceStateTerraform0dot12 `json:"resources"`
ChildModules []moduleStateTerraform0dot12 `json:"child_modules"`
Address string `json:"address"` // empty for root module, else e.g. `module.mymodulename`
}
type resourceStateTerraform0dot12 struct {
Address string `json:"address"`
Index *int `json:"index"` // only set by Terraform for counted resources
Name string `json:"name"`
RawValues map[string]interface{} `json:"values"`
Type string `json:"type"`
}
// read populates the state object from a statefile.
func (s *stateAnyTerraformVersion) read(stateFile io.Reader) error {
s.TerraformVersion = TerraformVersionUnknown
b, readErr := ioutil.ReadAll(stateFile)
if readErr != nil {
return readErr
}
err0dot12 := json.Unmarshal(b, &(*s).State0dot12)
if err0dot12 == nil && s.State0dot12.Values.RootModule != nil {
s.TerraformVersion = TerraformVersion0dot12
} else {
errPre0dot12 := json.Unmarshal(b, &(*s).StatePre0dot12)
if errPre0dot12 == nil && s.StatePre0dot12.Modules != nil {
s.TerraformVersion = TerraformVersionPre0dot12
} else {
return fmt.Errorf("0.12 format error: %v; pre-0.12 format error: %v (nil error means no content/modules found in the respective format)", err0dot12, errPre0dot12)
}
}
return nil
@ -53,6 +118,25 @@ func (s *state) outputs() []*Output {
return inst
}
// outputs returns a slice of the Outputs found in the statefile.
func (s *stateTerraform0dot12) outputs() []*Output {
inst := make([]*Output, 0)
for k, v := range s.Values.Outputs {
var o *Output
switch v := v.(type) {
case map[string]interface{}:
o, _ = NewOutput(k, v["value"])
default: // not expected
o, _ = NewOutput(k, "<error>")
}
inst = append(inst, o)
}
return inst
}
// map of resource ID -> resource Name
func (s *state) mapResourceIDNames() map[string]string {
t := map[string]string{}
@ -68,17 +152,91 @@ func (s *state) mapResourceIDNames() map[string]string {
return t
}
// map of resource ID -> resource Name
func (s *stateTerraform0dot12) mapResourceIDNames() map[string]string {
t := map[string]string{}
for _, module := range s.getAllModules() {
for _, resourceState := range module.ResourceStates {
id, typeOk := resourceState.RawValues["id"].(string)
if typeOk && id != "" && resourceState.Name != "" {
k := strings.ToLower(id)
t[k] = resourceState.Name
}
}
}
return t
}
func (s *stateTerraform0dot12) getAllModules() []*moduleStateTerraform0dot12 {
var allModules []*moduleStateTerraform0dot12
allModules = append(allModules, s.Values.RootModule)
addChildModules(&allModules, s.Values.RootModule)
return allModules
}
// recursively adds all child modules to the slice
func addChildModules(out *[]*moduleStateTerraform0dot12, from *moduleStateTerraform0dot12) {
for i := range from.ChildModules {
addChildModules(out, &from.ChildModules[i])
*out = append(*out, &from.ChildModules[i])
}
}
// resources returns a slice of the Resources found in the statefile.
func (s *stateAnyTerraformVersion) resources() []*Resource {
switch s.TerraformVersion {
case TerraformVersionPre0dot12:
return s.StatePre0dot12.resources()
case TerraformVersion0dot12:
return s.State0dot12.resources()
case TerraformVersionUnknown:
}
panic("Unimplemented Terraform version enum")
}
// resources returns a slice of the Resources found in the statefile.
func (s *state) resources() []*Resource {
inst := make([]*Resource, 0)
for _, m := range s.Modules {
for _, k := range m.resourceKeys() {
if strings.HasPrefix(k, "data.") {
// This does not represent a host (e.g. AWS AMI)
continue
}
// If a module is used, the resource key may not be unique, for instance:
//
// The module cannot use dynamic resource naming and thus has to use some hardcoded name:
//
// resource "aws_instance" "host" { ... }
//
// The main file then uses the module twice:
//
// module "application1" { source = "./modules/mymodulename" }
// module "application2" { source = "./modules/mymodulename" }
//
// Avoid key clashes by prepending module name to the key. If path is ["root"], don't
// prepend anything.
//
// In the above example: `aws_instance.host` -> `aws_instance.application1_host`
fullKey := k
resourceNameIndex := strings.Index(fullKey, ".") + 1
if len(m.Path) > 1 && resourceNameIndex > 0 {
for i := len(m.Path) - 1; i >= 1; i-- {
fullKey = fullKey[:resourceNameIndex] + strings.Replace(m.Path[i], ".", "_", -1) + "_" + fullKey[resourceNameIndex:]
}
}
// Terraform stores resources in a name->map map, but we need the name to
// decide which groups to include the resource in. So wrap it in a higher-
// level object with both properties.
r, err := NewResource(k, m.ResourceStates[k])
r, err := NewResource(fullKey, m.ResourceStates[k])
if err != nil {
asJSON, _ := json.Marshal(m.ResourceStates[k])
fmt.Fprintf(os.Stderr, "Warning: failed to parse resource %s (%v)\n", asJSON, err)
continue
}
if r.IsSupported() {
@ -90,9 +248,81 @@ func (s *state) resources() []*Resource {
return inst
}
type moduleState struct {
ResourceStates map[string]resourceState `json:"resources"`
Outputs map[string]interface{} `json:"outputs"`
func encodeTerraform0Dot12ValuesAsAttributes(rawValues *map[string]interface{}) map[string]string {
ret := make(map[string]string)
for k, v := range *rawValues {
switch v := v.(type) {
case map[string]interface{}:
ret[k+".#"] = strconv.Itoa(len(v))
for kk, vv := range v {
if str, typeOk := vv.(string); typeOk {
ret[k+"."+kk] = str
} else {
ret[k+"."+kk] = "<error>"
}
}
case string:
ret[k] = v
default:
ret[k] = "<error>"
}
}
return ret
}
// resources returns a slice of the Resources found in the statefile.
func (s *stateTerraform0dot12) resources() []*Resource {
inst := make([]*Resource, 0)
for _, module := range s.getAllModules() {
for _, rs := range module.ResourceStates {
id, typeOk := rs.RawValues["id"].(string)
if !typeOk {
continue
}
if strings.HasPrefix(rs.Address, "data.") {
// This does not represent a host (e.g. AWS AMI)
continue
}
modulePrefix := ""
if module.Address != "" {
modulePrefix = strings.Replace(module.Address, ".", "_", -1) + "_"
}
resourceKeyName := rs.Type + "." + modulePrefix + rs.Name
if rs.Index != nil {
resourceKeyName += "." + strconv.Itoa(*rs.Index)
}
// Terraform stores resources in a name->map map, but we need the name to
// decide which groups to include the resource in. So wrap it in a higher-
// level object with both properties.
//
// Convert to the pre-0.12 structure for backwards compatibility of code.
r, err := NewResource(resourceKeyName, resourceState{
Type: rs.Type,
Primary: instanceState{
ID: id,
Attributes: encodeTerraform0Dot12ValuesAsAttributes(&rs.RawValues),
},
})
if err != nil {
asJSON, _ := json.Marshal(rs)
fmt.Fprintf(os.Stderr, "Warning: failed to parse resource %s (%v)\n", asJSON, err)
continue
}
if r.IsSupported() {
inst = append(inst, r)
}
}
}
sort.Slice(inst, func(i, j int) bool {
return inst[i].baseName < inst[j].baseName
})
return inst
}
// resourceKeys returns a sorted slice of the key names of the resources in this
@ -105,21 +335,9 @@ func (ms *moduleState) resourceKeys() []string {
for k := range ms.ResourceStates {
keys[i] = k
i += 1
i++
}
sort.Strings(keys)
return keys
}
type resourceState struct {
// Populated from statefile
Type string `json:"type"`
Primary instanceState `json:"primary"`
}
type instanceState struct {
ID string `json:"id"`
Attributes map[string]string `json:"attributes,omitempty"`
}

@ -797,11 +797,13 @@ const expectedHostOneOutput = `
`
func TestListCommand(t *testing.T) {
var s state
var s stateAnyTerraformVersion
r := strings.NewReader(exampleStateFile)
err := s.read(r)
assert.NoError(t, err)
assert.Equal(t, TerraformVersionPre0dot12, s.TerraformVersion)
// Decode expectation as JSON
var exp interface{}
err = json.Unmarshal([]byte(expectedListOutput), &exp)
@ -822,11 +824,13 @@ func TestListCommand(t *testing.T) {
}
func TestListCommandEnvHostname(t *testing.T) {
var s state
var s stateAnyTerraformVersion
r := strings.NewReader(exampleStateFileEnvHostname)
err := s.read(r)
assert.NoError(t, err)
assert.Equal(t, TerraformVersionPre0dot12, s.TerraformVersion)
// Decode expectation as JSON
var exp interface{}
err = json.Unmarshal([]byte(expectedListOutputEnvHostname), &exp)
@ -849,11 +853,13 @@ func TestListCommandEnvHostname(t *testing.T) {
}
func TestHostCommand(t *testing.T) {
var s state
var s stateAnyTerraformVersion
r := strings.NewReader(exampleStateFile)
err := s.read(r)
assert.NoError(t, err)
assert.Equal(t, TerraformVersionPre0dot12, s.TerraformVersion)
// Decode expectation as JSON
var exp interface{}
err = json.Unmarshal([]byte(expectedHostOneOutput), &exp)
@ -874,11 +880,13 @@ func TestHostCommand(t *testing.T) {
}
func TestInventoryCommand(t *testing.T) {
var s state
var s stateAnyTerraformVersion
r := strings.NewReader(exampleStateFile)
err := s.read(r)
assert.NoError(t, err)
assert.Equal(t, TerraformVersionPre0dot12, s.TerraformVersion)
// Run the command, capture the output
var stdout, stderr bytes.Buffer
exitCode := cmdInventory(&stdout, &stderr, &s)
@ -887,3 +895,283 @@ func TestInventoryCommand(t *testing.T) {
assert.Equal(t, expectedInventoryOutput, stdout.String())
}
//
// Terraform 0.12 BEGIN
//
const exampleStateFileTerraform0dot12 = `
{
"format_version": "0.1",
"terraform_version": "0.12.1",
"values": {
"outputs": {
"my_endpoint": {
"sensitive": false,
"value": "a.b.c.d.example.com"
},
"my_password": {
"sensitive": true,
"value": "1234"
},
"map": {
"sensitive": false,
"value": {
"first": "a",
"second": "b"
}
}
},
"root_module": {
"resources": [
{
"address": "aws_instance.one",
"type": "aws_instance",
"name": "one",
"provider_name": "aws",
"schema_version": 1,
"values": {
"ami": "ami-00000000000000000",
"id": "i-11111111111111111",
"private_ip": "10.0.0.1",
"public_ip": "35.159.25.34",
"tags": {
"Name": "one-aws-instance"
},
"volume_tags": {
"Ignored": "stuff"
}
}
}
],
"child_modules": [
{
"resources": [
{
"address": "aws_instance.host",
"type": "aws_instance",
"name": "host",
"values": {
"ami": "ami-00000000000000001",
"id": "i-22222222222222222",
"private_ip": "10.0.0.2",
"public_ip": "",
"tags": {
"Name": "two-aws-instance"
}
}
}
],
"address": "module.my-module-two"
},
{
"resources": [
{
"address": "aws_instance.host",
"type": "aws_instance",
"name": "host",
"index": 0,
"values": {
"ami": "ami-00000000000000001",
"id": "i-33333333333333333",
"private_ip": "10.0.0.3",
"public_ip": "",
"tags": {
"Name": "three-aws-instance"
}
}
},
{
"address": "aws_instance.host",
"type": "aws_instance",
"name": "host",
"index": 1,
"values": {
"ami": "ami-00000000000000001",
"id": "i-11133333333333333",
"private_ip": "10.0.1.3",
"public_ip": "",
"tags": {
"Name": "three-aws-instance"
}
}
}
],
"address": "module.my-module-three"
}
]
}
}
}
`
const expectedListOutputTerraform0dot12 = `
{
"all": {
"hosts": [
"10.0.0.2",
"10.0.0.3",
"10.0.1.3",
"35.159.25.34"
],
"vars": {
"my_endpoint": "a.b.c.d.example.com",
"my_password": "1234",
"map": {"first": "a", "second": "b"}
}
},
"one.0": ["35.159.25.34"],
"one": ["35.159.25.34"],
"module_my-module-two_host.0": ["10.0.0.2"],
"module_my-module-two_host": ["10.0.0.2"],
"module_my-module-three_host.0": ["10.0.0.3"],
"module_my-module-three_host.1": ["10.0.1.3"],
"module_my-module-three_host": ["10.0.0.3", "10.0.1.3"],
"type_aws_instance": ["10.0.0.2", "10.0.0.3", "10.0.1.3", "35.159.25.34"],
"name_one-aws-instance": ["35.159.25.34"],
"name_two-aws-instance": ["10.0.0.2"],
"name_three-aws-instance": ["10.0.0.3", "10.0.1.3"]
}
`
const expectedInventoryOutputTerraform0dot12 = `[all]
10.0.0.2
10.0.0.3
10.0.1.3
35.159.25.34
[all:vars]
map={"first":"a","second":"b"}
my_endpoint="a.b.c.d.example.com"
my_password="1234"
[module_my-module-three_host]
10.0.0.3
10.0.1.3
[module_my-module-three_host.0]
10.0.0.3
[module_my-module-three_host.1]
10.0.1.3
[module_my-module-two_host]
10.0.0.2
[module_my-module-two_host.0]
10.0.0.2
[name_one-aws-instance]
35.159.25.34
[name_three-aws-instance]
10.0.0.3
10.0.1.3
[name_two-aws-instance]
10.0.0.2
[one]
35.159.25.34
[one.0]
35.159.25.34
[type_aws_instance]
10.0.0.2
10.0.0.3
10.0.1.3
35.159.25.34
`
const expectedHostOneOutputTerraform0dot12 = `
{
"ami": "ami-00000000000000000",
"ansible_host": "35.159.25.34",
"id":"i-11111111111111111",
"private_ip":"10.0.0.1",
"public_ip": "35.159.25.34",
"tags.#": "1",
"tags.Name": "one-aws-instance",
"volume_tags.#":"1",
"volume_tags.Ignored":"stuff"
}
`
func TestListCommandTerraform0dot12(t *testing.T) {
var s stateAnyTerraformVersion
r := strings.NewReader(exampleStateFileTerraform0dot12)
err := s.read(r)
assert.NoError(t, err)
assert.Equal(t, TerraformVersion0dot12, s.TerraformVersion)
// Decode expectation as JSON
var exp interface{}
err = json.Unmarshal([]byte(expectedListOutputTerraform0dot12), &exp)
assert.NoError(t, err)
// Run the command, capture the output
var stdout, stderr bytes.Buffer
exitCode := cmdList(&stdout, &stderr, &s)
assert.Equal(t, 0, exitCode)
assert.Equal(t, "", stderr.String())
// Decode the output to compare
var act interface{}
err = json.Unmarshal([]byte(stdout.String()), &act)
assert.NoError(t, err)
assert.Equal(t, exp, act)
}
func TestHostCommandTerraform0dot12(t *testing.T) {
var s stateAnyTerraformVersion
r := strings.NewReader(exampleStateFileTerraform0dot12)
err := s.read(r)
assert.NoError(t, err)
assert.Equal(t, TerraformVersion0dot12, s.TerraformVersion)
// Decode expectation as JSON
var exp interface{}
err = json.Unmarshal([]byte(expectedHostOneOutputTerraform0dot12), &exp)
assert.NoError(t, err)
// Run the command, capture the output
var stdout, stderr bytes.Buffer
exitCode := cmdHost(&stdout, &stderr, &s, "35.159.25.34")
assert.Equal(t, 0, exitCode)
assert.Equal(t, "", stderr.String())
// Decode the output to compare
var act interface{}
err = json.Unmarshal([]byte(stdout.String()), &act)
assert.NoError(t, err)
assert.Equal(t, exp, act)
}
func TestInventoryCommandTerraform0dot12(t *testing.T) {
var s stateAnyTerraformVersion
r := strings.NewReader(exampleStateFileTerraform0dot12)
err := s.read(r)
assert.NoError(t, err)
assert.Equal(t, TerraformVersion0dot12, s.TerraformVersion)
// Run the command, capture the output
var stdout, stderr bytes.Buffer
exitCode := cmdInventory(&stdout, &stderr, &s)
assert.Equal(t, 0, exitCode)
assert.Equal(t, "", stderr.String())
assert.Equal(t, expectedInventoryOutputTerraform0dot12, stdout.String())
}
//
// Terraform 0.12 END
//

@ -0,0 +1,86 @@
{
"format_version": "0.1",
"terraform_version": "0.12.1",
"values": {
"outputs": {
"my_endpoint": {
"sensitive": false,
"value": "a.b.c.d.example.com"
},
"my_password": {
"sensitive": true,
"value": "1234"
},
"map": {
"sensitive": false,
"value": {
"first": "a",
"second": "b"
}
}
},
"root_module": {
"resources": [
{
"address": "aws_instance.one",
"type": "aws_instance",
"name": "one",
"provider_name": "aws",
"schema_version": 1,
"values": {
"ami": "ami-00000000000000000",
"id": "i-11111111111111111",
"private_ip": "10.0.0.1",
"public_ip": "35.159.25.34",
"tags": {
"Name": "one-aws-instance"
},
"volume_tags": {
"Ignored": "stuff"
}
}
}
],
"child_modules": [
{
"resources": [
{
"address": "aws_instance.host",
"type": "aws_instance",
"name": "host",
"values": {
"ami": "ami-00000000000000001",
"id": "i-22222222222222222",
"private_ip": "10.0.0.2",
"public_ip": "",
"tags": {
"Name": "two-aws-instance"
}
}
}
],
"address": "module.my-module-two"
},
{
"resources": [
{
"address": "aws_instance.host",
"type": "aws_instance",
"name": "host",
"values": {
"ami": "ami-00000000000000001",
"id": "i-33333333333333333",
"private_ip": "10.0.0.3",
"public_ip": "",
"tags": {
"Name": "three-aws-instance"
}
}
}
],
"address": "module.my-module-three"
}
]
}
}
}

@ -38,8 +38,11 @@ func init() {
"nic_list.0.ip_endpoint_list.0.ip", // Nutanix
}
// type.name.0
nameParser = regexp.MustCompile(`^(\w+)\.([\w\-]+)(?:\.(\d+))?$`)
// Formats:
// - type.[module_]name (no `count` attribute; contains module name if we're not in the root module)
// - type.[module_]name.0 (if resource has `count` attribute)
// - "data." prefix should not parse and be ignored by caller (does not represent a host)
nameParser = regexp.MustCompile(`^([\w\-]+)\.([\w\-]+)(?:\.(\d+))?$`)
}
type Resource struct {
@ -62,15 +65,13 @@ func NewResource(keyName string, state resourceState) (*Resource, error) {
m := nameParser.FindStringSubmatch(keyName)
// This should not happen unless our regex changes.
// TODO: Warn instead of silently ignore error?
if len(m) != 4 {
return nil, fmt.Errorf("couldn't parse keyName: %s", keyName)
return nil, fmt.Errorf("couldn't parse resource keyName: %s", keyName)
}
var c int
var err error
if m[3] != "" {
// The third section should be the index, if it's present. Not sure what
// else we can do other than panic (which seems highly undesirable) if that
// isn't the case.