From 90ee117ab3a223015a84b68092001761c10d196a Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 25 May 2019 06:11:53 +0000 Subject: [PATCH] v0.12 Support This commit adds initial support for Terraform v0.12 while also retaining compatibility for Terraform v0.11. Testing has also been restructured and enhanced. --- .gitignore | 1 + Makefile | 21 +- fixtures/main.tf | 45 +++- fixtures/module/main.tf | 8 + fixtures/{ => v011}/terraform.tfstate | 109 +++++++++- fixtures/v012/terraform.tfstate | 224 ++++++++++++++++++++ main.go | 68 +++++- state.go | 269 ++---------------------- state_v011.go | 222 ++++++++++++++++++++ state_test.go => state_v011_test.go | 142 ++++++++++--- state_v012.go | 207 ++++++++++++++++++ state_v012_test.go | 291 ++++++++++++++++++++++++++ 12 files changed, 1317 insertions(+), 290 deletions(-) create mode 100644 fixtures/module/main.tf rename fixtures/{ => v011}/terraform.tfstate (54%) create mode 100644 fixtures/v012/terraform.tfstate create mode 100644 state_v011.go rename state_test.go => state_v011_test.go (51%) create mode 100644 state_v012.go create mode 100644 state_v012_test.go diff --git a/.gitignore b/.gitignore index a76af49..171ce78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/*.swp +**/*.terraform work diff --git a/Makefile b/Makefile index 64ed4b6..72e1a74 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,26 @@ CURDIR := $(shell pwd) TEST?=$$(go list ./... |grep -v 'vendor') TARGETS=darwin linux -TERRAFORM_VERSION="0.11.13" +TERRAFORM_VERSION_v011="0.11.13" +TERRAFORM_VERSION_v012="0.12.0" -test: +test: test_v011 test_v012 + +test_v011: + rm -rf work || true + mkdir work ; \ + cd work ; \ + wget https://releases.hashicorp.com/terraform/$(TERRAFORM_VERSION_v011)/terraform_$(TERRAFORM_VERSION_v011)_linux_amd64.zip ; \ + unzip terraform_$(TERRAFORM_VERSION_v011)_linux_amd64.zip + PATH=$(CURDIR)/work:$(PATH) go test -v -run="V011" ./... -count=1 + +test_v012: rm -rf work || true mkdir work ; \ cd work ; \ - wget https://releases.hashicorp.com/terraform/$(TERRAFORM_VERSION)/terraform_$(TERRAFORM_VERSION)_linux_amd64.zip ; \ - unzip terraform_$(TERRAFORM_VERSION)_linux_amd64.zip - PATH=$(CURDIR)/work:$(PATH) go test -v $(TEST) ./... -count=1 + wget https://releases.hashicorp.com/terraform/$(TERRAFORM_VERSION_v012)/terraform_$(TERRAFORM_VERSION_v012)_linux_amd64.zip ; \ + unzip terraform_$(TERRAFORM_VERSION_v012)_linux_amd64.zip + PATH=$(CURDIR)/work:$(PATH) go test -v -run "V012" ./... -count=1 build: go install diff --git a/fixtures/main.tf b/fixtures/main.tf index 23686a9..1a074b0 100644 --- a/fixtures/main.tf +++ b/fixtures/main.tf @@ -2,7 +2,7 @@ resource "ansible_group" "group_1" { inventory_group_name = "group_1" children = ["group_2"] - vars { + vars = { foo = "bar" } } @@ -11,11 +11,16 @@ resource "ansible_group" "group_2" { inventory_group_name = "group_2" } +resource "ansible_group" "other_groups" { + count = 2 + inventory_group_name = "some_group_${count.index}" +} + resource "ansible_host" "host_1" { inventory_hostname = "host_1" groups = ["group_1"] - vars { + vars = { ansible_user = "ubuntu" ansible_host = "1.2.3.4" test = "host_1" @@ -26,9 +31,43 @@ resource "ansible_host" "host_2" { inventory_hostname = "host_2" groups = ["group_1"] - vars { + vars = { ansible_user = "ubuntu" ansible_host = "1.2.3.5" test = "host_2" } } + +resource "ansible_host" "host_3" { + inventory_hostname = "host_3" + groups = ["group_3"] + + vars = { + ansible_user = "ubuntu" + ansible_host = "1.2.3.6" + } +} + +resource "ansible_host" "host_4" { + inventory_hostname = "host_4" + + vars = { + ansible_user = "ubuntu" + ansible_host = "1.2.3.7" + } +} + +resource "ansible_host" "other_hosts" { + count = 2 + inventory_hostname = "some_host_${count.index}" + groups = ["some_group_${count.index}"] + + vars = { + ansible_user = "ubuntu" + ansible_host = "1.2.4.${count.index}" + } +} + +module "more_hosts" { + source = "./module" +} diff --git a/fixtures/module/main.tf b/fixtures/module/main.tf new file mode 100644 index 0000000..944a8b6 --- /dev/null +++ b/fixtures/module/main.tf @@ -0,0 +1,8 @@ +resource "ansible_host" "host_5" { + inventory_hostname = "host_5" + + vars = { + ansible_user = "ubuntu" + ansible_host = "1.2.3.8" + } +} diff --git a/fixtures/terraform.tfstate b/fixtures/v011/terraform.tfstate similarity index 54% rename from fixtures/terraform.tfstate rename to fixtures/v011/terraform.tfstate index 77a83f3..7ededaa 100644 --- a/fixtures/terraform.tfstate +++ b/fixtures/v011/terraform.tfstate @@ -1,8 +1,8 @@ { "version": 3, - "terraform_version": "0.11.1", - "serial": 3, - "lineage": "4da608b9-cfb2-4135-9a02-cb0326eed951", + "terraform_version": "0.11.14", + "serial": 1, + "lineage": "16015fe5-5b24-330c-ca11-e3630a674808", "modules": [ { "path": [ @@ -44,6 +44,36 @@ "deposed": [], "provider": "provider.ansible" }, + "ansible_group.other_groups.0": { + "type": "ansible_group", + "depends_on": [], + "primary": { + "id": "some_group_0", + "attributes": { + "id": "some_group_0", + "inventory_group_name": "some_group_0" + }, + "meta": {}, + "tainted": false + }, + "deposed": [], + "provider": "provider.ansible" + }, + "ansible_group.other_groups.1": { + "type": "ansible_group", + "depends_on": [], + "primary": { + "id": "some_group_1", + "attributes": { + "id": "some_group_1", + "inventory_group_name": "some_group_1" + }, + "meta": {}, + "tainted": false + }, + "deposed": [], + "provider": "provider.ansible" + }, "ansible_host.host_1": { "type": "ansible_host", "depends_on": [], @@ -96,7 +126,7 @@ "groups.0": "group_3", "id": "host_3", "inventory_hostname": "host_3", - "vars.%": "3", + "vars.%": "2", "vars.ansible_host": "1.2.3.6", "vars.ansible_user": "ubuntu" }, @@ -112,10 +142,9 @@ "primary": { "id": "host_4", "attributes": { - "groups.#": "0", "id": "host_4", "inventory_hostname": "host_4", - "vars.%": "3", + "vars.%": "2", "vars.ansible_host": "1.2.3.7", "vars.ansible_user": "ubuntu" }, @@ -124,6 +153,74 @@ }, "deposed": [], "provider": "provider.ansible" + }, + "ansible_host.other_hosts.0": { + "type": "ansible_host", + "depends_on": [], + "primary": { + "id": "some_host_0", + "attributes": { + "groups.#": "1", + "groups.0": "some_group_0", + "id": "some_host_0", + "inventory_hostname": "some_host_0", + "vars.%": "2", + "vars.ansible_host": "1.2.4.0", + "vars.ansible_user": "ubuntu" + }, + "meta": {}, + "tainted": false + }, + "deposed": [], + "provider": "provider.ansible" + }, + "ansible_host.other_hosts.1": { + "type": "ansible_host", + "depends_on": [], + "primary": { + "id": "some_host_1", + "attributes": { + "groups.#": "1", + "groups.0": "some_group_1", + "id": "some_host_1", + "inventory_hostname": "some_host_1", + "vars.%": "2", + "vars.ansible_host": "1.2.4.1", + "vars.ansible_user": "ubuntu" + }, + "meta": {}, + "tainted": false + }, + "deposed": [], + "provider": "provider.ansible" + } + }, + "depends_on": [] + }, + { + "path": [ + "root", + "more_hosts" + ], + "outputs": {}, + "resources": { + "ansible_host.host_5": { + "type": "ansible_host", + "depends_on": [], + "primary": { + "id": "host_5", + "attributes": { + "id": "host_5", + "inventory_hostname": "host_5", + "vars.%": "2", + "vars.ansible_host": "1.2.3.8", + "vars.ansible_user": "ubuntu" + }, + "meta": {}, + "tainted": false + }, + "deposed": [], + "provider": "provider.ansible" } }, "depends_on": [] diff --git a/fixtures/v012/terraform.tfstate b/fixtures/v012/terraform.tfstate new file mode 100644 index 0000000..5eed5d4 --- /dev/null +++ b/fixtures/v012/terraform.tfstate @@ -0,0 +1,224 @@ +{ + "version": 4, + "terraform_version": "0.12.0", + "serial": 12, + "lineage": "9b8b06a2-914e-26df-58d1-c5146f4d8e25", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "ansible_group", + "name": "group_1", + "provider": "provider.ansible", + "instances": [ + { + "schema_version": 0, + "attributes": { + "children": [ + "group_2" + ], + "id": "group_1", + "inventory_group_name": "group_1", + "vars": { + "foo": "bar" + } + } + } + ] + }, + { + "mode": "managed", + "type": "ansible_group", + "name": "group_2", + "provider": "provider.ansible", + "instances": [ + { + "schema_version": 0, + "attributes": { + "children": null, + "id": "group_2", + "inventory_group_name": "group_2", + "vars": null + } + } + ] + }, + { + "mode": "managed", + "type": "ansible_group", + "name": "other_groups", + "each": "list", + "provider": "provider.ansible", + "instances": [ + { + "index_key": 0, + "schema_version": 0, + "attributes": { + "children": null, + "id": "some_group_0", + "inventory_group_name": "some_group_0", + "vars": null + } + }, + { + "index_key": 1, + "schema_version": 0, + "attributes": { + "children": null, + "id": "some_group_1", + "inventory_group_name": "some_group_1", + "vars": null + } + } + ] + }, + { + "mode": "managed", + "type": "ansible_host", + "name": "host_1", + "provider": "provider.ansible", + "instances": [ + { + "schema_version": 0, + "attributes": { + "groups": [ + "group_1" + ], + "id": "host_1", + "inventory_hostname": "host_1", + "vars": { + "ansible_host": "1.2.3.4", + "ansible_user": "ubuntu", + "test": "host_1" + } + } + } + ] + }, + { + "mode": "managed", + "type": "ansible_host", + "name": "host_2", + "provider": "provider.ansible", + "instances": [ + { + "schema_version": 0, + "attributes": { + "groups": [ + "group_1" + ], + "id": "host_2", + "inventory_hostname": "host_2", + "vars": { + "ansible_host": "1.2.3.5", + "ansible_user": "ubuntu", + "test": "host_2" + } + } + } + ] + }, + { + "mode": "managed", + "type": "ansible_host", + "name": "host_3", + "provider": "provider.ansible", + "instances": [ + { + "schema_version": 0, + "attributes": { + "groups": [ + "group_3" + ], + "id": "host_3", + "inventory_hostname": "host_3", + "vars": { + "ansible_host": "1.2.3.6", + "ansible_user": "ubuntu" + } + } + } + ] + }, + { + "mode": "managed", + "type": "ansible_host", + "name": "host_4", + "provider": "provider.ansible", + "instances": [ + { + "schema_version": 0, + "attributes": { + "groups": null, + "id": "host_4", + "inventory_hostname": "host_4", + "vars": { + "ansible_host": "1.2.3.7", + "ansible_user": "ubuntu" + } + } + } + ] + }, + { + "module": "module.more_hosts", + "mode": "managed", + "type": "ansible_host", + "name": "host_5", + "provider": "provider.ansible", + "instances": [ + { + "schema_version": 0, + "attributes": { + "groups": null, + "id": "host_5", + "inventory_hostname": "host_5", + "vars": { + "ansible_host": "1.2.3.8", + "ansible_user": "ubuntu" + } + } + } + ] + }, + { + "mode": "managed", + "type": "ansible_host", + "name": "other_hosts", + "each": "list", + "provider": "provider.ansible", + "instances": [ + { + "index_key": 0, + "schema_version": 0, + "attributes": { + "groups": [ + "some_group_0" + ], + "id": "some_host_0", + "inventory_hostname": "some_host_0", + "vars": { + "ansible_host": "1.2.4.0", + "ansible_user": "ubuntu" + } + } + }, + { + "index_key": 1, + "schema_version": 0, + "attributes": { + "groups": [ + "some_group_1" + ], + "id": "some_host_1", + "inventory_hostname": "some_host_1", + "vars": { + "ansible_host": "1.2.4.1", + "ansible_user": "ubuntu" + } + } + } + ] + } + ] +} diff --git a/main.go b/main.go index b444aa7..df84a04 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,13 @@ package main import ( + "bytes" + "encoding/json" "flag" "fmt" + "io/ioutil" "os" + "os/exec" "path/filepath" ) @@ -40,7 +44,7 @@ func main() { os.Exit(1) } - j, err := s.ToJSON() + j, err := ToJSON(s) if err != nil { errAndExit(err) } @@ -60,6 +64,68 @@ func getStatePath() string { return "." } +func getState(path string) (State, error) { + var out bytes.Buffer + var state State + terraformVersion := "0.12" + + cmd := exec.Command("terraform", "state", "pull") + cmd.Dir = path + cmd.Stdout = &out + + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("Error running `terraform state pull` in directory %s, %s\n", path, err) + } + + b, err := ioutil.ReadAll(&out) + if err != nil { + return nil, fmt.Errorf("Error reading output of `terraform state pull`: %s\n", err) + } + + // If there was no output, return nil and no error + if string(b) == "" { + return nil, nil + } + + if string(b[0]) == "o" && string(b[1]) == ":" { + b = append(b[:0], b[2:]...) + } + + var tmpState interface{} + err = json.Unmarshal(b, &tmpState) + if err != nil { + return nil, fmt.Errorf("Error unmarshaling state: %s\n", err) + } + + if v, ok := tmpState.(map[string]interface{}); ok { + if v, ok := v["version"].(float64); ok { + if v == 3 { + terraformVersion = "0.11" + } + } + } + + switch terraformVersion { + case "0.11": + var s StateV011 + err = json.Unmarshal(b, &s) + if err != nil { + return nil, fmt.Errorf("Error unmarshaling state: %s\n", err) + } + state = s + default: + var s StateV012 + err = json.Unmarshal(b, &s) + if err != nil { + return nil, fmt.Errorf("Error unmarshaling state: %s\n", err) + } + state = s + } + + return state, nil +} + func errAndExit(err error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/state.go b/state.go index 3d7e4d7..86c5a10 100644 --- a/state.go +++ b/state.go @@ -1,212 +1,35 @@ package main import ( - "bytes" "encoding/json" - "fmt" - "io/ioutil" - "os/exec" "sort" - "strings" ) -// The following structs are for Terraform State. -type State struct { - Modules []Module `json:"modules"` -} - -// GetGroups will return all ansible_group resources. -func (r State) GetGroups() ([]string, error) { - var groups []string - - for _, m := range r.Modules { - for _, resource := range m.Resources { - if resource.Type == "ansible_group" { - groups = append(groups, resource.Primary.ID) - } - } - } - - sort.Strings(groups) - return groups, nil -} - -// GetHosts will return all ansible_group resources. -func (r State) GetHosts() ([]string, error) { - var hosts []string - - for _, m := range r.Modules { - for _, resource := range m.Resources { - if resource.Type == "ansible_host" { - hosts = append(hosts, resource.Primary.ID) - } - } - } - - sort.Strings(hosts) - return hosts, nil -} - -// GetGroup will find and return a specific ansible_group resource. -func (r State) GetGroup(group string) (*Resource, error) { - for _, m := range r.Modules { - for _, resource := range m.Resources { - if resource.Type == "ansible_group" { - if resource.Primary.ID == group { - return &resource, nil - } - } - } - } - - return nil, fmt.Errorf("Unable to find group %s", group) -} - -// GetChildrenForGroup will return the "children" members of an -// ansible_group resource. -func (r State) GetChildrenForGroup(group string) ([]string, error) { - var children []string - - resource, err := r.GetGroup(group) - if err != nil { - return nil, err - } - - for attrName, attr := range resource.Primary.Attributes { - if strings.HasPrefix(attrName, "children.") { - if attrName == "children.#" { - continue - } - children = append(children, attr) - } - } - - sort.Strings(children) - return children, nil -} - -// GetVarsForGroup will return the variables defined in an ansible_group -// resource. -func (r State) GetVarsForGroup(group string) (map[string]interface{}, error) { - vars := make(map[string]interface{}) - - resource, err := r.GetGroup(group) - if err != nil { - return nil, err - } - - for attrName, attr := range resource.Primary.Attributes { - if strings.HasPrefix(attrName, "vars.") { - if attrName == "vars.%" { - continue - } - - pieces := strings.SplitN(attrName, ".", 2) - if len(pieces) == 2 { - vars[pieces[1]] = attr - } - } - } - - return vars, nil -} - -// GetHostsForGroup will return the hosts in a specific ansible_group -// resource. -func (r State) GetHostsForGroup(group string) ([]string, error) { - var hosts []string - - for _, m := range r.Modules { - for _, resource := range m.Resources { - if resource.Type == "ansible_host" { - for attrName, attr := range resource.Primary.Attributes { - if strings.HasPrefix(attrName, "groups.") { - if group == attr { - hosts = append(hosts, resource.Primary.ID) - } - } - } - } - } - } +// Interface State represents the methods a state struct has to implement to +// parse a Terraform state. +type State interface { + GetGroups() ([]string, error) + GetGroup(group string) (interface{}, error) + GetGroupsForHost(host string) ([]string, error) - sort.Strings(hosts) - return hosts, nil -} + GetChildrenForGroup(group string) ([]string, error) -// GetHost will return a specific ansible_host. -func (r State) GetHost(host string) (*Resource, error) { - for _, m := range r.Modules { - for _, resource := range m.Resources { - if resource.Type == "ansible_host" { - if resource.Primary.ID == host { - return &resource, nil - } - } - } - } + GetVarsForGroup(group string) (map[string]interface{}, error) + GetVarsForHost(host string) (map[string]interface{}, error) - return nil, fmt.Errorf("Unable to find host %s", host) + GetHosts() ([]string, error) + GetHost(host string) (interface{}, error) + GetHostsForGroup(group string) ([]string, error) } -// GetGroupsForHost will return the groups defined in an ansible_host resource. -func (r State) GetGroupsForHost(host string) ([]string, error) { - groups := []string{} - - resource, err := r.GetHost(host) - if err != nil { - return nil, err - } - - for attrName, attr := range resource.Primary.Attributes { - if strings.HasPrefix(attrName, "groups.") { - if attrName == "groups.#" { - continue - } - - pieces := strings.SplitN(attrName, ".", 2) - if len(pieces) == 2 { - groups = append(groups, attr) - } - } - } - - return groups, nil -} - -// GetVarsForHost will return the variables defined in an ansible_host resource. -func (r State) GetVarsForHost(host string) (map[string]interface{}, error) { - vars := make(map[string]interface{}) - - resource, err := r.GetHost(host) - if err != nil { - return nil, err - } - - for attrName, attr := range resource.Primary.Attributes { - if strings.HasPrefix(attrName, "vars.") { - if attrName == "vars.%" { - continue - } - - pieces := strings.SplitN(attrName, ".", 2) - if len(pieces) == 2 { - vars[pieces[1]] = attr - } - } - } - - return vars, nil -} - -func (r State) BuildInventory() (map[string]interface{}, error) { +func BuildInventory(state State) (map[string]interface{}, error) { inv := make(map[string]interface{}) meta := make(map[string]interface{}) hostvars := make(map[string]interface{}) allHosts := []string{} // Get all ansible_group resources. - groups, err := r.GetGroups() + groups, err := state.GetGroups() if err != nil { return nil, err } @@ -214,19 +37,19 @@ func (r State) BuildInventory() (map[string]interface{}, error) { // For each ansible_group defined... for _, group := range groups { g := make(map[string]interface{}) - hosts, err := r.GetHostsForGroup(group) + hosts, err := state.GetHostsForGroup(group) if err != nil { return nil, err } // Get the children of the group. - children, err := r.GetChildrenForGroup(group) + children, err := state.GetChildrenForGroup(group) if err != nil { return nil, err } // Get any variables for the group. - vars, err := r.GetVarsForGroup(group) + vars, err := state.GetVarsForGroup(group) if err != nil { return nil, err } @@ -257,7 +80,7 @@ func (r State) BuildInventory() (map[string]interface{}, error) { var ungrouped []string // Get all ansible_host resources defined. - hosts, err := r.GetHosts() + hosts, err := state.GetHosts() if err != nil { return nil, err } @@ -268,7 +91,7 @@ func (r State) BuildInventory() (map[string]interface{}, error) { allHosts = append(allHosts, host) // Get any variable defined and set it in the inventory. - vars, err := r.GetVarsForHost(host) + vars, err := state.GetVarsForHost(host) if err != nil { return nil, err } @@ -276,7 +99,7 @@ func (r State) BuildInventory() (map[string]interface{}, error) { hostvars[host] = vars // Find all groups that the host is a part of. - groups, err := r.GetGroupsForHost(host) + groups, err := state.GetGroupsForHost(host) if err != nil { return nil, err } @@ -340,10 +163,10 @@ func (r State) BuildInventory() (map[string]interface{}, error) { return inv, nil } -func (r State) ToJSON() (string, error) { +func ToJSON(state State) (string, error) { var s string - inv, err := r.BuildInventory() + inv, err := BuildInventory(state) if err != nil { return s, err } @@ -357,51 +180,3 @@ func (r State) ToJSON() (string, error) { return s, nil } - -type Module struct { - Resources map[string]Resource `json:"resources"` -} - -type Resource struct { - Type string `json:"type"` - Primary Primary `json:"primary"` -} - -type Primary struct { - ID string `json:"id"` - Attributes map[string]string `json:"attributes"` -} - -func getState(path string) (*State, error) { - cmd := exec.Command("terraform", "state", "pull") - cmd.Dir = path - var out bytes.Buffer - cmd.Stdout = &out - - err := cmd.Run() - if err != nil { - return nil, fmt.Errorf("Error running `terraform state pull` in directory %s, %s\n", path, err) - } - - b, err := ioutil.ReadAll(&out) - if err != nil { - return nil, fmt.Errorf("Error reading output of `terraform state pull`: %s\n", err) - } - - // If there was no output, return nil and no error - if string(b) == "" { - return nil, nil - } - - if string(b[0]) == "o" && string(b[1]) == ":" { - b = append(b[:0], b[2:]...) - } - - var s State - err = json.Unmarshal(b, &s) - if err != nil { - return nil, fmt.Errorf("Error unmarshaling state: %s\n", err) - } - - return &s, nil -} diff --git a/state_v011.go b/state_v011.go new file mode 100644 index 0000000..50d0c23 --- /dev/null +++ b/state_v011.go @@ -0,0 +1,222 @@ +package main + +import ( + "fmt" + "sort" + "strings" +) + +// The following structs are for Terraform State +// from version v0.11 and prior. +type StateV011 struct { + Modules []ModuleV011 `json:"modules"` +} + +// GetGroups will return all ansible_group resources. +func (r StateV011) GetGroups() ([]string, error) { + var groups []string + + for _, m := range r.Modules { + for _, resource := range m.Resources { + if resource.Type == "ansible_group" { + groups = append(groups, resource.Primary.ID) + } + } + } + + sort.Strings(groups) + return groups, nil +} + +// GetHosts will return all ansible_group resources. +func (r StateV011) GetHosts() ([]string, error) { + var hosts []string + + for _, m := range r.Modules { + for _, resource := range m.Resources { + if resource.Type == "ansible_host" { + hosts = append(hosts, resource.Primary.ID) + } + } + } + + sort.Strings(hosts) + return hosts, nil +} + +// GetGroup will find and return a specific ansible_group resource. +func (r StateV011) GetGroup(group string) (interface{}, error) { + for _, m := range r.Modules { + for _, resource := range m.Resources { + if resource.Type == "ansible_group" { + if resource.Primary.ID == group { + return resource, nil + } + } + } + } + + return nil, fmt.Errorf("Unable to find group %s", group) +} + +// GetChildrenForGroup will return the "children" members of an +// ansible_group resource. +func (r StateV011) GetChildrenForGroup(group string) ([]string, error) { + var children []string + var resource ResourceV011 + + v, err := r.GetGroup(group) + if err != nil { + return nil, err + } + + resource = v.(ResourceV011) + + for attrName, attr := range resource.Primary.Attributes { + if strings.HasPrefix(attrName, "children.") { + if attrName == "children.#" { + continue + } + children = append(children, attr) + } + } + + sort.Strings(children) + return children, nil +} + +// GetVarsForGroup will return the variables defined in an ansible_group +// resource. +func (r StateV011) GetVarsForGroup(group string) (map[string]interface{}, error) { + var resource ResourceV011 + vars := make(map[string]interface{}) + + v, err := r.GetGroup(group) + if err != nil { + return nil, err + } + + resource = v.(ResourceV011) + + for attrName, attr := range resource.Primary.Attributes { + if strings.HasPrefix(attrName, "vars.") { + if attrName == "vars.%" { + continue + } + + pieces := strings.SplitN(attrName, ".", 2) + if len(pieces) == 2 { + vars[pieces[1]] = attr + } + } + } + + return vars, nil +} + +// GetHostsForGroup will return the hosts that belong to a defined group. +func (r StateV011) GetHostsForGroup(group string) ([]string, error) { + var hosts []string + + for _, m := range r.Modules { + for _, resource := range m.Resources { + if resource.Type == "ansible_host" { + for attrName, attr := range resource.Primary.Attributes { + if strings.HasPrefix(attrName, "groups.") { + if group == attr { + hosts = append(hosts, resource.Primary.ID) + } + } + } + } + } + } + + sort.Strings(hosts) + return hosts, nil +} + +// GetHost will return a specific ansible_host. +func (r StateV011) GetHost(host string) (interface{}, error) { + for _, m := range r.Modules { + for _, resource := range m.Resources { + if resource.Type == "ansible_host" { + if resource.Primary.ID == host { + return resource, nil + } + } + } + } + + return nil, fmt.Errorf("Unable to find host %s", host) +} + +// GetGroupsForHost will return the groups defined in an ansible_host resource. +func (r StateV011) GetGroupsForHost(host string) ([]string, error) { + var resource ResourceV011 + groups := []string{} + + v, err := r.GetHost(host) + if err != nil { + return nil, err + } + + resource = v.(ResourceV011) + + for attrName, attr := range resource.Primary.Attributes { + if strings.HasPrefix(attrName, "groups.") { + if attrName == "groups.#" { + continue + } + + pieces := strings.SplitN(attrName, ".", 2) + if len(pieces) == 2 { + groups = append(groups, attr) + } + } + } + + return groups, nil +} + +// GetVarsForHost will return the variables defined in an ansible_host resource. +func (r StateV011) GetVarsForHost(host string) (map[string]interface{}, error) { + var resource ResourceV011 + vars := make(map[string]interface{}) + + v, err := r.GetHost(host) + if err != nil { + return nil, err + } + + resource = v.(ResourceV011) + + for attrName, attr := range resource.Primary.Attributes { + if strings.HasPrefix(attrName, "vars.") { + if attrName == "vars.%" { + continue + } + + pieces := strings.SplitN(attrName, ".", 2) + if len(pieces) == 2 { + vars[pieces[1]] = attr + } + } + } + + return vars, nil +} + +type ModuleV011 struct { + Resources map[string]ResourceV011 `json:"resources"` +} + +type ResourceV011 struct { + Type string `json:"type"` + Primary PrimaryV011 `json:"primary"` +} + +type PrimaryV011 struct { + ID string `json:"id"` + Attributes map[string]string `json:"attributes"` +} diff --git a/state_test.go b/state_v011_test.go similarity index 51% rename from state_test.go rename to state_v011_test.go index 02302a1..e02a376 100644 --- a/state_test.go +++ b/state_v011_test.go @@ -6,13 +6,13 @@ import ( "github.com/stretchr/testify/assert" ) -var expectedState = State{ - Modules: []Module{ - Module{ - Resources: map[string]Resource{ - "ansible_host.host_1": Resource{ +var expectedStateV011 = StateV011{ + Modules: []ModuleV011{ + ModuleV011{ + Resources: map[string]ResourceV011{ + "ansible_host.host_1": ResourceV011{ Type: "ansible_host", - Primary: Primary{ + Primary: PrimaryV011{ ID: "host_1", Attributes: map[string]string{ "id": "host_1", @@ -26,9 +26,9 @@ var expectedState = State{ }, }, }, - "ansible_host.host_2": Resource{ + "ansible_host.host_2": ResourceV011{ Type: "ansible_host", - Primary: Primary{ + Primary: PrimaryV011{ ID: "host_2", Attributes: map[string]string{ "id": "host_2", @@ -42,38 +42,67 @@ var expectedState = State{ }, }, }, - "ansible_host.host_3": Resource{ + "ansible_host.host_3": ResourceV011{ Type: "ansible_host", - Primary: Primary{ + Primary: PrimaryV011{ ID: "host_3", Attributes: map[string]string{ "id": "host_3", "inventory_hostname": "host_3", "groups.#": "1", "groups.0": "group_3", - "vars.%": "3", + "vars.%": "2", "vars.ansible_host": "1.2.3.6", "vars.ansible_user": "ubuntu", }, }, }, - "ansible_host.host_4": Resource{ + "ansible_host.host_4": ResourceV011{ Type: "ansible_host", - Primary: Primary{ + Primary: PrimaryV011{ ID: "host_4", Attributes: map[string]string{ "id": "host_4", "inventory_hostname": "host_4", - "groups.#": "0", - "vars.%": "3", + "vars.%": "2", "vars.ansible_host": "1.2.3.7", "vars.ansible_user": "ubuntu", }, }, }, - "ansible_group.group_1": Resource{ + "ansible_host.other_hosts.0": ResourceV011{ + Type: "ansible_host", + Primary: PrimaryV011{ + ID: "some_host_0", + Attributes: map[string]string{ + "id": "some_host_0", + "inventory_hostname": "some_host_0", + "groups.#": "1", + "groups.0": "some_group_0", + "vars.%": "2", + "vars.ansible_host": "1.2.4.0", + "vars.ansible_user": "ubuntu", + }, + }, + }, + "ansible_host.other_hosts.1": ResourceV011{ + Type: "ansible_host", + Primary: PrimaryV011{ + ID: "some_host_1", + Attributes: map[string]string{ + "id": "some_host_1", + "inventory_hostname": "some_host_1", + "groups.#": "1", + "groups.0": "some_group_1", + "vars.%": "2", + "vars.ansible_host": "1.2.4.1", + "vars.ansible_user": "ubuntu", + }, + }, + }, + "ansible_group.group_1": ResourceV011{ Type: "ansible_group", - Primary: Primary{ + Primary: PrimaryV011{ ID: "group_1", Attributes: map[string]string{ "id": "group_1", @@ -85,9 +114,9 @@ var expectedState = State{ }, }, }, - "ansible_group.group_2": Resource{ + "ansible_group.group_2": ResourceV011{ Type: "ansible_group", - Primary: Primary{ + Primary: PrimaryV011{ ID: "group_2", Attributes: map[string]string{ "id": "group_2", @@ -95,18 +124,55 @@ var expectedState = State{ }, }, }, + "ansible_group.other_groups.0": ResourceV011{ + Type: "ansible_group", + Primary: PrimaryV011{ + ID: "some_group_0", + Attributes: map[string]string{ + "id": "some_group_0", + "inventory_group_name": "some_group_0", + }, + }, + }, + "ansible_group.other_groups.1": ResourceV011{ + Type: "ansible_group", + Primary: PrimaryV011{ + ID: "some_group_1", + Attributes: map[string]string{ + "id": "some_group_1", + "inventory_group_name": "some_group_1", + }, + }, + }, + }, + }, + { + Resources: map[string]ResourceV011{ + "ansible_host.host_5": ResourceV011{ + Type: "ansible_host", + Primary: PrimaryV011{ + ID: "host_5", + Attributes: map[string]string{ + "id": "host_5", + "inventory_hostname": "host_5", + "vars.%": "2", + "vars.ansible_host": "1.2.3.8", + "vars.ansible_user": "ubuntu", + }, + }, + }, }, }, }, } -var expectedInventory = map[string]interface{}{ +var expectedInventoryV011 = map[string]interface{}{ "all": map[string]interface{}{ - "hosts": []string{"host_1", "host_2", "host_3", "host_4"}, + "hosts": []string{"host_1", "host_2", "host_3", "host_4", "host_5", "some_host_0", "some_host_1"}, "vars": map[string]interface{}{}, }, "ungrouped": map[string]interface{}{ - "hosts": []string{"host_4"}, + "hosts": []string{"host_4", "host_5"}, "vars": map[string]interface{}{}, }, "group_1": map[string]interface{}{ @@ -123,6 +189,14 @@ var expectedInventory = map[string]interface{}{ "hosts": []string{"host_3"}, "vars": map[string]interface{}{}, }, + "some_group_0": map[string]interface{}{ + "hosts": []string{"some_host_0"}, + "vars": map[string]interface{}{}, + }, + "some_group_1": map[string]interface{}{ + "hosts": []string{"some_host_1"}, + "vars": map[string]interface{}{}, + }, "_meta": map[string]interface{}{ "hostvars": map[string]interface{}{ "host_1": map[string]interface{}{ @@ -143,19 +217,31 @@ var expectedInventory = map[string]interface{}{ "ansible_host": "1.2.3.7", "ansible_user": "ubuntu", }, + "host_5": map[string]interface{}{ + "ansible_host": "1.2.3.8", + "ansible_user": "ubuntu", + }, + "some_host_0": map[string]interface{}{ + "ansible_host": "1.2.4.0", + "ansible_user": "ubuntu", + }, + "some_host_1": map[string]interface{}{ + "ansible_host": "1.2.4.1", + "ansible_user": "ubuntu", + }, }, }, } -func TestState_basic(t *testing.T) { - actual, err := getState("fixtures") +func TestStateV011_basic(t *testing.T) { + actual, err := getState("fixtures/v011") if err != nil { t.Fatal(err) } - assert.Equal(t, &expectedState, actual) + assert.Equal(t, expectedStateV011, actual) - expectedGroups := []string{"group_1", "group_2"} + expectedGroups := []string{"group_1", "group_2", "some_group_0", "some_group_1"} actualGroups, err := actual.GetGroups() if err != nil { t.Fatal(err) @@ -184,10 +270,10 @@ func TestState_basic(t *testing.T) { assert.Equal(t, expectedVars, actualVars) - actualInventory, err := actual.BuildInventory() + actualInventory, err := BuildInventory(actual) if err != nil { t.Fatal(err) } - assert.Equal(t, expectedInventory, actualInventory) + assert.Equal(t, expectedInventoryV011, actualInventory) } diff --git a/state_v012.go b/state_v012.go new file mode 100644 index 0000000..cd0b1ec --- /dev/null +++ b/state_v012.go @@ -0,0 +1,207 @@ +package main + +import ( + "fmt" + "sort" +) + +// The following structs are for Terraform State +// for version v0.12. +type StateV012 struct { + Resources []ResourceV012 `json:"resources"` +} + +// GetGroups will return all ansible_group resources. +func (r StateV012) GetGroups() ([]string, error) { + var groups []string + + for _, resource := range r.Resources { + if resource.Type == "ansible_group" { + for _, instance := range resource.Instances { + if v, ok := instance.Attributes["inventory_group_name"].(string); ok { + groups = append(groups, v) + } + } + } + } + + sort.Strings(groups) + + return groups, nil +} + +// GetHosts will return all ansible_hosts resources. +func (r StateV012) GetHosts() ([]string, error) { + var hosts []string + + for _, resource := range r.Resources { + if resource.Type == "ansible_host" { + for _, instance := range resource.Instances { + if v, ok := instance.Attributes["inventory_hostname"].(string); ok { + hosts = append(hosts, v) + } + } + } + } + + sort.Strings(hosts) + + return hosts, nil +} + +// GetGroup will find and return a specific ansible_group resource. +func (r StateV012) GetGroup(group string) (interface{}, error) { + for _, resource := range r.Resources { + if resource.Type == "ansible_group" { + for _, instance := range resource.Instances { + if v, ok := instance.Attributes["inventory_group_name"].(string); ok { + if v == group { + return instance, nil + } + } + } + } + } + + return nil, fmt.Errorf("Unable to find group %s", group) +} + +// GetChildrenForGroup will return the "children" members of an +// ansible_group resource. +func (r StateV012) GetChildrenForGroup(group string) ([]string, error) { + var children []string + var instance InstanceV012 + + v, err := r.GetGroup(group) + if err != nil { + return nil, err + } + + instance = v.(InstanceV012) + + if v, ok := instance.Attributes["children"].([]interface{}); ok { + for _, c := range v { + children = append(children, c.(string)) + } + } + + sort.Strings(children) + return children, nil +} + +// GetVarsForGroup will return the variables defined in an ansible_group +// resource. +func (r StateV012) GetVarsForGroup(group string) (map[string]interface{}, error) { + var instance InstanceV012 + vars := make(map[string]interface{}) + + v, err := r.GetGroup(group) + if err != nil { + return nil, err + } + + instance = v.(InstanceV012) + + if v, ok := instance.Attributes["vars"].(map[string]interface{}); ok { + vars = v + } + + return vars, nil +} + +// GetHostsForGroup will return the hosts that belong to a defined group. +func (r StateV012) GetHostsForGroup(group string) ([]string, error) { + var hosts []string + + for _, resource := range r.Resources { + if resource.Type == "ansible_host" { + for _, instance := range resource.Instances { + hostname, ok := instance.Attributes["inventory_hostname"].(string) + if !ok { + continue + } + + groups, ok := instance.Attributes["groups"].([]interface{}) + if !ok { + continue + } + + for _, g := range groups { + if group == g.(string) { + hosts = append(hosts, hostname) + } + } + } + } + } + + sort.Strings(hosts) + return hosts, nil +} + +// GetHost will return a specific ansible_host. +func (r StateV012) GetHost(host string) (interface{}, error) { + for _, resource := range r.Resources { + if resource.Type == "ansible_host" { + for _, instance := range resource.Instances { + if v, ok := instance.Attributes["inventory_hostname"].(string); ok { + if v == host { + return instance, nil + } + } + } + } + } + + return nil, fmt.Errorf("Unable to find host %s", host) +} + +// GetGroupsForHost will return the groups defined in an ansible_host resource. +func (r StateV012) GetGroupsForHost(host string) ([]string, error) { + var instance InstanceV012 + groups := []string{} + + v, err := r.GetHost(host) + if err != nil { + return nil, err + } + + instance = v.(InstanceV012) + + if v, ok := instance.Attributes["groups"].([]interface{}); ok { + for _, group := range v { + groups = append(groups, group.(string)) + } + } + + return groups, nil +} + +// GetVarsForHost will return the variables defined in an ansible_host resource. +func (r StateV012) GetVarsForHost(host string) (map[string]interface{}, error) { + var instance InstanceV012 + vars := make(map[string]interface{}) + + v, err := r.GetHost(host) + if err != nil { + return nil, err + } + + instance = v.(InstanceV012) + + if v, ok := instance.Attributes["vars"].(map[string]interface{}); ok { + vars = v + } + + return vars, nil +} + +type ResourceV012 struct { + Type string `json:"type"` + Name string `json:"name"` + Instances []InstanceV012 `json:"instances"` +} + +type InstanceV012 struct { + Attributes map[string]interface{} `json:"attributes"` +} diff --git a/state_v012_test.go b/state_v012_test.go new file mode 100644 index 0000000..f224b13 --- /dev/null +++ b/state_v012_test.go @@ -0,0 +1,291 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var expectedStateV012 = StateV012{ + Resources: []ResourceV012{ + { + Name: "group_1", + Type: "ansible_group", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "group_1", + "inventory_group_name": "group_1", + "children": []interface{}{"group_2"}, + "vars": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + }, + }, + { + Name: "group_2", + Type: "ansible_group", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "group_2", + "inventory_group_name": "group_2", + "children": nil, + "vars": nil, + }, + }, + }, + }, + { + Name: "other_groups", + Type: "ansible_group", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "some_group_0", + "inventory_group_name": "some_group_0", + "children": nil, + "vars": nil, + }, + }, + { + Attributes: map[string]interface{}{ + "id": "some_group_1", + "inventory_group_name": "some_group_1", + "children": nil, + "vars": nil, + }, + }, + }, + }, + { + Name: "host_1", + Type: "ansible_host", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "host_1", + "inventory_hostname": "host_1", + "groups": []interface{}{"group_1"}, + "vars": map[string]interface{}{ + "ansible_host": "1.2.3.4", + "ansible_user": "ubuntu", + "test": "host_1", + }, + }, + }, + }, + }, + { + Name: "host_2", + Type: "ansible_host", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "host_2", + "inventory_hostname": "host_2", + "groups": []interface{}{"group_1"}, + "vars": map[string]interface{}{ + "ansible_host": "1.2.3.5", + "ansible_user": "ubuntu", + "test": "host_2", + }, + }, + }, + }, + }, + { + Name: "host_3", + Type: "ansible_host", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "host_3", + "inventory_hostname": "host_3", + "groups": []interface{}{"group_3"}, + "vars": map[string]interface{}{ + "ansible_host": "1.2.3.6", + "ansible_user": "ubuntu", + }, + }, + }, + }, + }, + { + Name: "host_4", + Type: "ansible_host", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "host_4", + "inventory_hostname": "host_4", + "groups": nil, + "vars": map[string]interface{}{ + "ansible_host": "1.2.3.7", + "ansible_user": "ubuntu", + }, + }, + }, + }, + }, + { + Name: "host_5", + Type: "ansible_host", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "host_5", + "inventory_hostname": "host_5", + "groups": nil, + "vars": map[string]interface{}{ + "ansible_host": "1.2.3.8", + "ansible_user": "ubuntu", + }, + }, + }, + }, + }, + { + Name: "other_hosts", + Type: "ansible_host", + Instances: []InstanceV012{ + { + Attributes: map[string]interface{}{ + "id": "some_host_0", + "inventory_hostname": "some_host_0", + "groups": []interface{}{"some_group_0"}, + "vars": map[string]interface{}{ + "ansible_host": "1.2.4.0", + "ansible_user": "ubuntu", + }, + }, + }, + { + Attributes: map[string]interface{}{ + "id": "some_host_1", + "inventory_hostname": "some_host_1", + "groups": []interface{}{"some_group_1"}, + "vars": map[string]interface{}{ + "ansible_host": "1.2.4.1", + "ansible_user": "ubuntu", + }, + }, + }, + }, + }, + }, +} + +var expectedInventoryV012 = map[string]interface{}{ + "all": map[string]interface{}{ + "hosts": []string{"host_1", "host_2", "host_3", "host_4", "host_5", "some_host_0", "some_host_1"}, + "vars": map[string]interface{}{}, + }, + "ungrouped": map[string]interface{}{ + "hosts": []string{"host_4", "host_5"}, + "vars": map[string]interface{}{}, + }, + "group_1": map[string]interface{}{ + "hosts": []string{"host_1", "host_2"}, + "children": []string{"group_2"}, + "vars": map[string]interface{}{ + "foo": "bar", + }, + }, + "group_2": map[string]interface{}{ + "vars": map[string]interface{}{}, + }, + "group_3": map[string]interface{}{ + "hosts": []string{"host_3"}, + "vars": map[string]interface{}{}, + }, + "some_group_0": map[string]interface{}{ + "hosts": []string{"some_host_0"}, + "vars": map[string]interface{}{}, + }, + "some_group_1": map[string]interface{}{ + "hosts": []string{"some_host_1"}, + "vars": map[string]interface{}{}, + }, + "_meta": map[string]interface{}{ + "hostvars": map[string]interface{}{ + "host_1": map[string]interface{}{ + "ansible_host": "1.2.3.4", + "ansible_user": "ubuntu", + "test": "host_1", + }, + "host_2": map[string]interface{}{ + "ansible_host": "1.2.3.5", + "ansible_user": "ubuntu", + "test": "host_2", + }, + "host_3": map[string]interface{}{ + "ansible_host": "1.2.3.6", + "ansible_user": "ubuntu", + }, + "host_4": map[string]interface{}{ + "ansible_host": "1.2.3.7", + "ansible_user": "ubuntu", + }, + "host_5": map[string]interface{}{ + "ansible_host": "1.2.3.8", + "ansible_user": "ubuntu", + }, + "some_host_0": map[string]interface{}{ + "ansible_host": "1.2.4.0", + "ansible_user": "ubuntu", + }, + "some_host_1": map[string]interface{}{ + "ansible_host": "1.2.4.1", + "ansible_user": "ubuntu", + }, + }, + }, +} + +func TestStateV012_basic(t *testing.T) { + actual, err := getState("fixtures/v012") + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, expectedStateV012, actual) + + expectedGroups := []string{"group_1", "group_2", "some_group_0", "some_group_1"} + actualGroups, err := actual.GetGroups() + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, expectedGroups, actualGroups) + + expectedHosts := []string{"host_1", "host_2"} + actualHosts, err := actual.GetHostsForGroup(expectedGroups[0]) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, expectedHosts, actualHosts) + + expectedVars := map[string]interface{}{ + "ansible_host": "1.2.3.4", + "ansible_user": "ubuntu", + "test": "host_1", + } + + actualVars, err := actual.GetVarsForHost("host_1") + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, expectedVars, actualVars) + + actualInventory, err := BuildInventory(actual) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, expectedInventoryV012, actualInventory) +}