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

Merge branch 'digitalocean'

This commit is contained in:
Adam Mckaig 2015-06-04 23:19:59 -04:00
commit 56f9b9ff07
10 changed files with 228 additions and 155 deletions

1
.gitignore vendored

@ -1 +1,2 @@
/terraform-inventory /terraform-inventory
/fixtures/secrets.tfvars

@ -1,9 +1,11 @@
# Terraformed Inventory # Terraformed Inventory
This is a little Go app which generates an dynamic [Ansible] [ansible] inventory This is a little Go app which generates an dynamic [Ansible] [ansible] inventory
from an AWS-based [Terraform] [tf] state file. It allows one to spawn a bunch of EC2 VMs with from a [Terraform] [tf] state file. It allows one to spawn a bunch of VMs with
Terraform, then (re-)provision them with Ansible. It's pretty neat. Terraform, then (re-)provision them with Ansible. It's pretty neat.
Currently, only **AWS** and **DigitalOcean** are supported.
# Installation # Installation
@ -41,6 +43,14 @@ It's just a Go app, so the usual:
cd $GOPATH/adammck/terraform-inventory cd $GOPATH/adammck/terraform-inventory
go build 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.
## License ## License

10
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 // add each instance as a pseudo-group, so they can be provisioned
// individually where necessary. // individually where necessary.
for name, inst := range s.instances() { for name, res := range s.resources() {
groups[name] = []string{inst.Attributes["private_ip"]} groups[name] = []string{res.Address()}
} }
return output(stdout, stderr, groups) return output(stdout, stderr, groups)
} }
func cmdHost(stdout io.Writer, stderr io.Writer, s *state, hostname string) int { func cmdHost(stdout io.Writer, stderr io.Writer, s *state, hostname string) int {
for _, inst := range s.instances() { for name, res := range s.resources() {
if hostname == inst.Attributes["private_ip"] { if hostname == name {
return output(stdout, stderr, inst.Attributes) return output(stdout, stderr, res.Attributes())
} }
} }

31
fixtures/example.tf Normal file

@ -0,0 +1,31 @@
variable "do_token" {}
variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "aws_subnet_id" {}
provider "aws" {
access_key = "${var.aws_access_key}"
secret_key = "${var.aws_secret_key}"
region = "us-east-1"
}
provider "digitalocean" {
token = "${var.do_token}"
}
resource "aws_instance" "web-aws" {
ami = "ami-96a818fe"
instance_type = "t2.micro"
subnet_id = "${var.aws_subnet_id}"
root_block_device = {
delete_on_termination = true
}
}
resource "digitalocean_droplet" "web-do" {
image = "centos-7-0-x64"
name = "terraform-inventory-1"
region = "nyc1"
size = "512mb"
ssh_keys = [55015]
}

@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"serial": 1, "serial": 11,
"modules": [ "modules": [
{ {
"path": [ "path": [
@ -8,103 +8,53 @@
], ],
"outputs": {}, "outputs": {},
"resources": { "resources": {
"aws_instance.one": { "aws_instance.web-aws": {
"type": "aws_instance", "type": "aws_instance",
"depends_on": [
"aws_security_group.example"
],
"primary": { "primary": {
"id": "i-aaaaaaaa", "id": "i-366736e6",
"attributes": { "attributes": {
"ami": "ami-XXXXXXXX", "ami": "ami-96a818fe",
"availability_zone": "us-east-1b", "availability_zone": "us-east-1d",
"id": "i-aaaaaaaa", "ebs_block_device.#": "0",
"ebs_optimized": "false",
"ephemeral_block_device.#": "0",
"id": "i-366736e6",
"instance_type": "t2.micro", "instance_type": "t2.micro",
"key_name": "", "private_dns": "ip-10-0-0-4.ec2.internal",
"private_dns": "ip-1-1-1-1.ec2.internal", "private_ip": "10.0.0.4",
"private_ip": "1.1.1.1",
"public_dns": "", "public_dns": "",
"public_ip": "", "root_block_device.#": "1",
"security_groups.#": "1", "root_block_device.0.delete_on_termination": "true",
"security_groups.0": "sg-cccccccc", "root_block_device.0.iops": "0",
"subnet_id": "subnet-XXXXXXXX", "root_block_device.0.volume_size": "8",
"tenancy": "default" "root_block_device.0.volume_type": "standard",
"security_groups.#": "0",
"subnet_id": "subnet-59f9b32e",
"tags.#": "0",
"tenancy": "default",
"vpc_security_group_ids.#": "1",
"vpc_security_group_ids.2076429742": "sg-b42329d0"
},
"meta": {
"schema_version": "1"
} }
} }
}, },
"aws_instance.two": { "digitalocean_droplet.web-do": {
"type": "aws_instance", "type": "digitalocean_droplet",
"depends_on": [
"aws_security_group.example"
],
"primary": { "primary": {
"id": "i-bbbbbbbb", "id": "5579362",
"attributes": { "attributes": {
"ami": "ami-XXXXXXXX", "id": "5579362",
"availability_zone": "us-east-1b", "image": "centos-7-0-x64",
"id": "i-bbbbbbbb", "ipv4_address": "192.241.136.44",
"instance_type": "t2.micro", "locked": "false",
"key_name": "", "name": "terraform-inventory-1",
"private_dns": "ip-2-2-2-2.ec2.internal", "region": "nyc1",
"private_ip": "2.2.2.2", "size": "512mb",
"public_dns": "", "ssh_keys.#": "1",
"public_ip": "", "ssh_keys.0": "55015",
"security_groups.#": "1", "status": "active"
"security_groups.0": "sg-cccccccc",
"subnet_id": "subnet-XXXXXXXX",
"tenancy": "default"
}
}
},
"aws_route53_record.example": {
"type": "aws_route53_record",
"depends_on": [
"aws_instance.one",
"aws_instance.two"
],
"primary": {
"id": "XXXXXXXXXXXXXX_something.example.com_CNAME",
"attributes": {
"id": "XXXXXXXXXXXXXX_something.example.com_CNAME",
"name": "something.example.com",
"records.#": "2",
"records.0": "i-aaaaaaaa",
"records.1": "i-bbbbbbbb",
"ttl": "300",
"type": "CNAME",
"zone_id": "XXXXXXXXXXXXXX"
}
}
},
"aws_security_group.example": {
"type": "aws_security_group",
"primary": {
"id": "sg-cccccccc",
"attributes": {
"description": "Allow SSH and HTTP from inside the firewall",
"id": "sg-cccccccc",
"ingress.#": "2",
"ingress.0.cidr_blocks.#": "2",
"ingress.0.cidr_blocks.0": "10.0.0.0/8",
"ingress.0.cidr_blocks.1": "192.168.0.0/16",
"ingress.0.from_port": "22",
"ingress.0.protocol": "tcp",
"ingress.0.security_groups.#": "0",
"ingress.0.self": "false",
"ingress.0.to_port": "22",
"ingress.1.cidr_blocks.#": "2",
"ingress.1.cidr_blocks.0": "10.0.0.0/8",
"ingress.1.cidr_blocks.1": "192.168.0.0/16",
"ingress.1.from_port": "80",
"ingress.1.protocol": "tcp",
"ingress.1.security_groups.#": "0",
"ingress.1.self": "false",
"ingress.1.to_port": "80",
"name": "example",
"owner_id": "111111111111",
"tags.App": "my_app",
"tags.Environment": "my_env",
"vpc_id": "vpc-XXXXXXXX"
} }
} }
} }

@ -0,0 +1,4 @@
do_token = ""
aws_access_key = ""
aws_secret_key = ""
aws_subnet_id = ""

3
fixtures/update Executable file

@ -0,0 +1,3 @@
#!/bin/bash -e
cd $(cd `dirname "$0"`; cd ..; pwd)
terraform apply -refresh -var-file="fixtures/secrets.tfvars" -state="fixtures/example.tfstate" -backup="-" fixtures

@ -23,7 +23,7 @@ func main() {
env.MustProcess(cfg) env.MustProcess(cfg)
if *version == true { if *version == true {
fmt.Printf("%s version %d\n", os.Args[0], versionInfo()) fmt.Printf("%s version %s\n", os.Args[0], versionInfo())
return return
} }

@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"io/ioutil" "io/ioutil"
"strings" "regexp"
) )
type state struct { type state struct {
@ -29,16 +29,17 @@ func (s *state) read(stateFile io.Reader) error {
return nil return nil
} }
// hosts returns a map of name to instanceState, for each of the aws_instance // resources returns a map of name to resourceState, for any supported resources
// resources found in the statefile. // found in the statefile.
func (s *state) instances() map[string]instanceState { func (s *state) resources() map[string]resourceState {
inst := make(map[string]instanceState) typeRemover := regexp.MustCompile(`^[\w_]+\.`)
inst := make(map[string]resourceState)
for _, m := range s.Modules { for _, m := range s.Modules {
for k, r := range m.Resources { for k, r := range m.Resources {
if r.Type == "aws_instance" { if r.isSupported() {
name := strings.TrimPrefix(k, "aws_instance.") name := typeRemover.ReplaceAllString(k, "")
inst[name] = r.Primary inst[name] = r
} }
} }
} }
@ -55,6 +56,30 @@ type resourceState struct {
Primary instanceState `json:"primary"` 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 { type instanceState struct {
ID string `json:"id"` ID string `json:"id"`
Attributes map[string]string `json:"attributes,omitempty"` Attributes map[string]string `json:"attributes,omitempty"`

@ -22,8 +22,8 @@ const exampleStateFile = `
"primary": { "primary": {
"id": "i-aaaaaaaa", "id": "i-aaaaaaaa",
"attributes": { "attributes": {
"ami": "ami-XXXXXXXX", "id": "i-aaaaaaaa",
"id": "i-aaaaaaaa" "private_ip": "10.0.0.1"
} }
} }
}, },
@ -32,8 +32,8 @@ const exampleStateFile = `
"primary": { "primary": {
"id": "i-bbbbbbbb", "id": "i-bbbbbbbb",
"attributes": { "attributes": {
"ami": "ami-YYYYYYYY", "id": "i-bbbbbbbb",
"id": "i-bbbbbbbb" "private_ip": "10.0.0.2"
} }
} }
}, },
@ -42,8 +42,18 @@ const exampleStateFile = `
"primary": { "primary": {
"id": "sg-cccccccc", "id": "sg-cccccccc",
"attributes": { "attributes": {
"description": "Whatever", "id": "sg-cccccccc",
"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{ Primary: instanceState{
ID: "i-aaaaaaaa", ID: "i-aaaaaaaa",
Attributes: map[string]string{ 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{ Primary: instanceState{
ID: "i-bbbbbbbb", ID: "i-bbbbbbbb",
Attributes: map[string]string{ 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{ Primary: instanceState{
ID: "sg-cccccccc", ID: "sg-cccccccc",
Attributes: map[string]string{ Attributes: map[string]string{
"description": "Whatever",
"id": "sg-cccccccc", "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) assert.Equal(t, exp, s)
} }
func TestInstances(t *testing.T) { func TestResources(t *testing.T) {
r := strings.NewReader(exampleStateFile) r := strings.NewReader(exampleStateFile)
var s state var s state
err := s.read(r) err := s.read(r)
assert.Nil(t, err) assert.Nil(t, err)
inst := s.instances() inst := s.resources()
assert.Equal(t, 2, len(inst)) assert.Equal(t, 3, len(inst))
assert.Equal(t, "i-aaaaaaaa", inst["one"].ID) assert.Equal(t, "aws_instance", inst["one"].Type)
assert.Equal(t, "i-bbbbbbbb", inst["two"].ID) 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())
} }