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:
parent
3a1f433061
commit
94a66e3c5e
@ -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
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()
|
||||
|
34
main.go
34
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 state pull` in directory %s, %s\n", path, err)
|
||||
os.Exit(1)
|
||||
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.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 == 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.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{
|
||||
|
270
parser.go
270
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"`
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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 *state) read(stateFile io.Reader) error {
|
||||
func (s *stateAnyTerraformVersion) read(stateFile io.Reader) error {
|
||||
s.TerraformVersion = TerraformVersionUnknown
|
||||
|
||||
// read statefile contents
|
||||
b, err := ioutil.ReadAll(stateFile)
|
||||
if err != nil {
|
||||
return err
|
||||
b, readErr := ioutil.ReadAll(stateFile)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
|
||||
// parse into struct
|
||||
err = json.Unmarshal(b, s)
|
||||
if err != nil {
|
||||
return err
|
||||
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"`
|
||||
}
|
||||
|
296
parser_test.go
296
parser_test.go
@ -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
|
||||
//
|
||||
|
86
parser_test.go.example.tfstate
Normal file
86
parser_test.go.example.tfstate
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
31
resource.go
31
resource.go
@ -16,16 +16,16 @@ var nameParser *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
keyNames = []string{
|
||||
"ipv4_address", // DO and SoftLayer
|
||||
"public_ip", // AWS
|
||||
"public_ipv6", // Scaleway
|
||||
"ipaddress", // CS
|
||||
"ip_address", // VMware, Docker, Linode
|
||||
"private_ip", // AWS
|
||||
"network_interface.0.ipv4_address", // VMware
|
||||
"default_ip_address", // provider.vsphere v1.1.1
|
||||
"access_ip_v4", // OpenStack
|
||||
"floating_ip", // OpenStack
|
||||
"ipv4_address", // DO and SoftLayer
|
||||
"public_ip", // AWS
|
||||
"public_ipv6", // Scaleway
|
||||
"ipaddress", // CS
|
||||
"ip_address", // VMware, Docker, Linode
|
||||
"private_ip", // AWS
|
||||
"network_interface.0.ipv4_address", // VMware
|
||||
"default_ip_address", // provider.vsphere v1.1.1
|
||||
"access_ip_v4", // OpenStack
|
||||
"floating_ip", // OpenStack
|
||||
"network_interface.0.access_config.0.nat_ip", // GCE
|
||||
"network_interface.0.access_config.0.assigned_nat_ip", // GCE
|
||||
"network_interface.0.address", // GCE
|
||||
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user