From bde4ea23d152bd87718ec47a5b1b27915e37b8f6 Mon Sep 17 00:00:00 2001 From: Adam Mckaig Date: Thu, 4 Jun 2015 22:43:56 -0400 Subject: [PATCH] Add Digital Ocean support --- README.md | 5 ++ cli.go | 10 ++-- main.go | 2 +- parser.go | 41 ++++++++++--- parser_test.go | 153 ++++++++++++++++++++++++++++++++----------------- 5 files changed, 145 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 5dfb856..5e9f22a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,11 @@ It's just a Go app, so the usual: cd $GOPATH/adammck/terraform-inventory go build +To test against an example statefile, run: + + terraform-inventory --list fixtures/example.tfstate + terraform-inventory --host=web-aws fixtures/example.tfstate + To update the fixtures, populate `fixtures/secrets.tfvars` with your DO and AWS account details, and run `fixtures/update`. You probably don't need to do this. diff --git a/cli.go b/cli.go index a68f09d..eba2446 100644 --- a/cli.go +++ b/cli.go @@ -11,17 +11,17 @@ func cmdList(stdout io.Writer, stderr io.Writer, s *state) int { // add each instance as a pseudo-group, so they can be provisioned // individually where necessary. - for name, inst := range s.instances() { - groups[name] = []string{inst.Attributes["private_ip"]} + for name, res := range s.resources() { + groups[name] = []string{res.Address()} } return output(stdout, stderr, groups) } func cmdHost(stdout io.Writer, stderr io.Writer, s *state, hostname string) int { - for _, inst := range s.instances() { - if hostname == inst.Attributes["private_ip"] { - return output(stdout, stderr, inst.Attributes) + for name, res := range s.resources() { + if hostname == name { + return output(stdout, stderr, res.Attributes()) } } diff --git a/main.go b/main.go index 7149e68..4d82b12 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ func main() { env.MustProcess(cfg) if *version == true { - fmt.Printf("%s version %d\n", os.Args[0], versionInfo()) + fmt.Printf("%s version %s\n", os.Args[0], versionInfo()) return } diff --git a/parser.go b/parser.go index 96a780b..9e9203b 100644 --- a/parser.go +++ b/parser.go @@ -4,7 +4,7 @@ import ( "encoding/json" "io" "io/ioutil" - "strings" + "regexp" ) type state struct { @@ -29,16 +29,17 @@ func (s *state) read(stateFile io.Reader) error { return nil } -// hosts returns a map of name to instanceState, for each of the aws_instance -// resources found in the statefile. -func (s *state) instances() map[string]instanceState { - inst := make(map[string]instanceState) +// resources returns a map of name to resourceState, for any supported resources +// found in the statefile. +func (s *state) resources() map[string]resourceState { + typeRemover := regexp.MustCompile(`^[\w_]+\.`) + inst := make(map[string]resourceState) for _, m := range s.Modules { for k, r := range m.Resources { - if r.Type == "aws_instance" { - name := strings.TrimPrefix(k, "aws_instance.") - inst[name] = r.Primary + if r.isSupported() { + name := typeRemover.ReplaceAllString(k, "") + inst[name] = r } } } @@ -55,6 +56,30 @@ type resourceState struct { Primary instanceState `json:"primary"` } +// isSupported returns true if terraform-inventory supports this resource. +func (s *resourceState) isSupported() bool { + return s.Address() != "" +} + +// Address returns the IP address of this resource. +func (s *resourceState) Address() string { + switch s.Type { + case "aws_instance": + return s.Primary.Attributes["private_ip"] + + case "digitalocean_droplet": + return s.Primary.Attributes["ipv4_address"] + + default: + return "" + } +} + +// Attributes returns a map containing everything we know about this resource. +func (s *resourceState) Attributes() map[string]string { + return s.Primary.Attributes +} + type instanceState struct { ID string `json:"id"` Attributes map[string]string `json:"attributes,omitempty"` diff --git a/parser_test.go b/parser_test.go index 632a0d4..1b7f18d 100644 --- a/parser_test.go +++ b/parser_test.go @@ -8,48 +8,58 @@ import ( const exampleStateFile = ` { - "version": 1, - "serial": 1, - "modules": [ - { - "path": [ - "root" - ], - "outputs": {}, - "resources": { - "aws_instance.one": { - "type": "aws_instance", - "primary": { - "id": "i-aaaaaaaa", - "attributes": { - "ami": "ami-XXXXXXXX", - "id": "i-aaaaaaaa" - } - } - }, - "aws_instance.two": { - "type": "aws_instance", - "primary": { - "id": "i-bbbbbbbb", - "attributes": { - "ami": "ami-YYYYYYYY", - "id": "i-bbbbbbbb" - } - } - }, - "aws_security_group.example": { - "type": "aws_security_group", - "primary": { - "id": "sg-cccccccc", - "attributes": { - "description": "Whatever", - "id": "sg-cccccccc" - } - } - } - } - } - ] + "version": 1, + "serial": 1, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": { + "aws_instance.one": { + "type": "aws_instance", + "primary": { + "id": "i-aaaaaaaa", + "attributes": { + "id": "i-aaaaaaaa", + "private_ip": "10.0.0.1" + } + } + }, + "aws_instance.two": { + "type": "aws_instance", + "primary": { + "id": "i-bbbbbbbb", + "attributes": { + "id": "i-bbbbbbbb", + "private_ip": "10.0.0.2" + } + } + }, + "aws_security_group.example": { + "type": "aws_security_group", + "primary": { + "id": "sg-cccccccc", + "attributes": { + "id": "sg-cccccccc", + "description": "Whatever" + } + } + }, + "digitalocean_droplet.three": { + "type": "digitalocean_droplet", + "primary": { + "id": "ddddddd", + "attributes": { + "id": "ddddddd", + "ipv4_address": "192.168.0.3" + } + } + } + } + } + ] } ` @@ -69,8 +79,8 @@ func TestStateRead(t *testing.T) { Primary: instanceState{ ID: "i-aaaaaaaa", Attributes: map[string]string{ - "ami": "ami-XXXXXXXX", - "id": "i-aaaaaaaa", + "id": "i-aaaaaaaa", + "private_ip": "10.0.0.1", }, }, }, @@ -79,8 +89,8 @@ func TestStateRead(t *testing.T) { Primary: instanceState{ ID: "i-bbbbbbbb", Attributes: map[string]string{ - "ami": "ami-YYYYYYYY", - "id": "i-bbbbbbbb", + "id": "i-bbbbbbbb", + "private_ip": "10.0.0.2", }, }, }, @@ -89,8 +99,18 @@ func TestStateRead(t *testing.T) { Primary: instanceState{ ID: "sg-cccccccc", Attributes: map[string]string{ - "description": "Whatever", "id": "sg-cccccccc", + "description": "Whatever", + }, + }, + }, + "digitalocean_droplet.three": resourceState{ + Type: "digitalocean_droplet", + Primary: instanceState{ + ID: "ddddddd", + Attributes: map[string]string{ + "id": "ddddddd", + "ipv4_address": "192.168.0.3", }, }, }, @@ -102,15 +122,44 @@ func TestStateRead(t *testing.T) { assert.Equal(t, exp, s) } -func TestInstances(t *testing.T) { +func TestResources(t *testing.T) { r := strings.NewReader(exampleStateFile) var s state err := s.read(r) assert.Nil(t, err) - inst := s.instances() - assert.Equal(t, 2, len(inst)) - assert.Equal(t, "i-aaaaaaaa", inst["one"].ID) - assert.Equal(t, "i-bbbbbbbb", inst["two"].ID) + inst := s.resources() + assert.Equal(t, 3, len(inst)) + assert.Equal(t, "aws_instance", inst["one"].Type) + assert.Equal(t, "aws_instance", inst["two"].Type) + assert.Equal(t, "digitalocean_droplet", inst["three"].Type) +} + +func TestIsSupported(t *testing.T) { + + r := resourceState{ + Type: "something", + } + assert.Equal(t, false, r.isSupported()) + + r = resourceState{ + Type: "aws_instance", + Primary: instanceState{ + Attributes: map[string]string{ + "private_ip": "10.0.0.2", + }, + }, + } + assert.Equal(t, true, r.isSupported()) + + r = resourceState{ + Type: "digitalocean_droplet", + Primary: instanceState{ + Attributes: map[string]string{ + "ipv4_address": "192.168.0.3", + }, + }, + } + assert.Equal(t, true, r.isSupported()) }