Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds the module name to the base name if ENV TF_ADD_MODULE_PATH is set. #95

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![GitHub release](https://img.shields.io/homebrew/v/terraform-inventory.svg?maxAge=2592000)](http://braumeister.org/formula/terraform-inventory)

This is a little Go app which generates a dynamic [Ansible][ans] inventory from
a [Terraform][tf] state file. It allows one to spawn a bunch of instances with
a [Terraform][tf] state file. It allows one to spawn a bunch of instances with
Terraform, then (re-)provision them with Ansible.

The following providers are supported:
Expand Down Expand Up @@ -152,6 +152,11 @@ you to overwrite the source of the ansible inventory name.

TF_HOSTNAME_KEY_NAME=name ansible-playbook --inventory-file=/path/to/terraform-inventory deploy/playbook.yml


When using the same terraform module multiple times, the resources have the same name and overwrite themselves. It is therefore possible to set the environment variable TF_ADD_MODULE_PATH to add the module path to the base name.

TF_ADD_MODULE_PATH=true ansible-playbook --inventory-file=/path/to/terraform-inventory deploy/playbook.yml

## Development

It's just a Go app, so the usual:
Expand Down
38 changes: 35 additions & 3 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"sort"
"strings"
)

type counterSorter struct {
Expand Down Expand Up @@ -46,6 +47,7 @@ func gatherResources(s *state) map[string]interface{} {
outputGroups := make(map[string]interface{})

all := &allGroup{Hosts: make([]string, 0), Vars: make(map[string]interface{})}
modules := make(map[string]*allGroup)
types := make(map[string][]string)
individual := make(map[string][]string)
ordered := make(map[string][]string)
Expand All @@ -62,10 +64,27 @@ func gatherResources(s *state) map[string]interface{} {
tp := fmt.Sprintf("type_%s", res.resourceType)
types[tp] = appendUniq(types[tp], res.Hostname())

unsortedOrdered[res.baseName] = append(unsortedOrdered[res.baseName], res)
if len(res.modulePathArray) > 1 && res.modulePathArray[0] == "root" {
if _, ok := modules[res.ModulePath()]; !ok {
modules[res.ModulePath()] = &allGroup{
Hosts: make([]string, 0),
Vars: make(map[string]interface{}),
}
}
modules[res.ModulePath()].Hosts = appendUniq(modules[res.ModulePath()].Hosts, res.Hostname())
}

var baseNameArray []string
if os.Getenv("TF_ADD_MODULE_PATH") != "" && res.ModulePath() != "" {
baseNameArray = append(baseNameArray, res.ModulePath())
}
baseNameArray = append(baseNameArray, res.baseName)
baseName := strings.Join(baseNameArray, ".")

unsortedOrdered[baseName] = append(unsortedOrdered[baseName], res)

// store as invdividual host (eg. <name>.<count>)
invdName := fmt.Sprintf("%s.%d", res.baseName, res.counter)
invdName := fmt.Sprintf("%s.%d", baseName, res.counter)
if old, exists := individual[invdName]; exists {
fmt.Fprintf(os.Stderr, "overwriting already existing individual key %s, old: %v, new: %v", invdName, old, res.Hostname())
}
Expand All @@ -89,7 +108,17 @@ func gatherResources(s *state) map[string]interface{} {
// inventorize outputs as variables
if len(s.outputs()) > 0 {
for _, out := range s.outputs() {
all.Vars[out.keyName] = out.value
if len(out.modulePathArray) > 1 && out.modulePathArray[0] == "root" {
if _, ok := modules[out.ModulePath()]; !ok {
modules[out.ModulePath()] = &allGroup{
Hosts: make([]string, 0),
Vars: make(map[string]interface{}),
}
}
modules[out.ModulePath()].Vars[out.keyName] = out.value
} else {
all.Vars[out.keyName] = out.value
}
}
}

Expand All @@ -104,6 +133,9 @@ func gatherResources(s *state) map[string]interface{} {
}

outputGroups["all"] = all
for k, v := range modules {
outputGroups[k] = v
}
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)
Expand Down
18 changes: 12 additions & 6 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@ package main

import (
"fmt"
"strings"
)

type Output struct {

// The keyName and value of the output
keyName string
value interface{}
keyName string
value interface{}
modulePathArray []string
}

func (o Output) ModulePath() string {
return strings.Join(o.modulePathArray, ".")
}

func NewOutput(keyName string, value interface{}) (*Output, error) {
func NewOutput(keyName string, value interface{}, path []string) (*Output, error) {

// TODO: Warn instead of silently ignore error?
if len(keyName) == 0 {
return nil, fmt.Errorf("couldn't parse keyName: %s", keyName)
}

return &Output{
keyName: keyName,
value: value,
keyName: keyName,
value: value,
modulePathArray: path,
}, nil
}
9 changes: 5 additions & 4 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ func (s *state) outputs() []*Output {
var o *Output
switch v := v.(type) {
case map[string]interface{}:
o, _ = NewOutput(k, v["value"])
o, _ = NewOutput(k, v["value"], m.Path)
case string:
o, _ = NewOutput(k, v)
o, _ = NewOutput(k, v, m.Path)
default:
o, _ = NewOutput(k, "<error>")
o, _ = NewOutput(k, "<error>", m.Path)
}

inst = append(inst, o)
Expand Down Expand Up @@ -77,7 +77,7 @@ func (s *state) resources() []*Resource {
// 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(k, m.ResourceStates[k], m.Path)
if err != nil {
continue
}
Expand All @@ -91,6 +91,7 @@ func (s *state) resources() []*Resource {
}

type moduleState struct {
Path []string `json:"path"`
ResourceStates map[string]resourceState `json:"resources"`
Outputs map[string]interface{} `json:"outputs"`
}
Expand Down
81 changes: 81 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,38 @@ import (
"github.com/stretchr/testify/assert"
)

const exampleStateFileEnvModulePath = `
{
"version": 1,
"serial": 1,
"modules": [
{
"path": [
"root",
"web"
],
"outputs": {
"foo": "bar"
},
"resources": {
"libvirt_domain.fourteen": {
"type": "libvirt_domain",
"primary": {
"id": "824c29be-2164-44c8-83e0-787705571d95",
"attributes": {
"name": "fourteen",
"network_interface.#": "1",
"network_interface.0.addresses.#": "1",
"network_interface.0.addresses.0": "192.168.102.14",
"network_interface.0.mac": "96:EE:4D:BD:B2:45"
}
}
}
}
}
]
}`

const exampleStateFileEnvHostname = `
{
"version": 1,
Expand Down Expand Up @@ -49,6 +81,28 @@ const expectedListOutputEnvHostname = `
"type_libvirt_domain": ["fourteen"]
}`

const expectedListOutputEnvModulePath = `
{
"all": {
"hosts": [
"192.168.102.14"
],
"vars": {
}
},
"root.web": {
"hosts": [
"192.168.102.14"
],
"vars": {
"foo": "bar"
}
},
"root.web.fourteen": ["192.168.102.14"],
"root.web.fourteen.0": ["192.168.102.14"],
"type_libvirt_domain": ["192.168.102.14"]
}`

const exampleStateFile = `
{
"version": 1,
Expand Down Expand Up @@ -748,6 +802,33 @@ func TestListCommandEnvHostname(t *testing.T) {
assert.Equal(t, exp, act)
}

func TestListCommandEnvModulePath(t *testing.T) {
var s state
r := strings.NewReader(exampleStateFileEnvModulePath)
err := s.read(r)
assert.NoError(t, err)

// Decode expectation as JSON
var exp interface{}
err = json.Unmarshal([]byte(expectedListOutputEnvModulePath), &exp)
assert.NoError(t, err)

// Run the command, capture the output
var stdout, stderr bytes.Buffer
os.Setenv("TF_ADD_MODULE_PATH", "true")
exitCode := cmdList(&stdout, &stderr, &s)
os.Unsetenv("TF_ADD_MODULE_PATH")
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 TestHostCommand(t *testing.T) {
var s state
r := strings.NewReader(exampleStateFile)
Expand Down
25 changes: 16 additions & 9 deletions resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ type Resource struct {
keyName string

// Extracted from keyName
resourceType string
baseName string
counter int
resourceType string
baseName string
counter int
modulePath string
modulePathArray []string
}

func NewResource(keyName string, state resourceState) (*Resource, error) {
func NewResource(keyName string, state resourceState, path []string) (*Resource, error) {
m := nameParser.FindStringSubmatch(keyName)

// This should not happen unless our regex changes.
Expand All @@ -80,14 +82,19 @@ func NewResource(keyName string, state resourceState) (*Resource, error) {
}

return &Resource{
State: state,
keyName: keyName,
resourceType: m[1],
baseName: m[2],
counter: c,
State: state,
keyName: keyName,
resourceType: m[1],
baseName: m[2],
counter: c,
modulePathArray: path,
}, nil
}

func (r Resource) ModulePath() string {
return strings.Join(r.modulePathArray, ".")
}

func (r Resource) IsSupported() bool {
return r.Address() != ""
}
Expand Down