1
1
mirror of https://github.com/adammck/terraform-inventory synced 2024-11-22 07:32:03 +01:00

Refactor to remove Terraform dependency

Closes #1
This commit is contained in:
Adam Mckaig 2015-02-06 16:31:59 -05:00
parent 9f33a7ec91
commit 2d1c7fdfd9
7 changed files with 392 additions and 139 deletions

48
cli.go Normal file

@ -0,0 +1,48 @@
package main
import (
"encoding/json"
"fmt"
"io"
)
func cmdList(stdout io.Writer, stderr io.Writer, s *state) int {
groups := make(map[string][]string, 0)
// 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"]}
}
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)
}
}
fmt.Fprintf(stderr, "No such host: %s\n", hostname)
return 1
}
// output marshals an arbitrary JSON object and writes it to stdout, or writes
// an error to stderr, then returns the appropriate exit code.
func output(stdout io.Writer, stderr io.Writer, whatever interface{}) int {
b, err := json.Marshal(whatever)
if err != nil {
fmt.Fprintf(stderr, "Error encoding JSON: %s\n", err)
return 1
}
_, err = stdout.Write(b)
if err != nil {
fmt.Fprintf(stderr, "Error writing JSON: %s\n", err)
return 1
}
return 0
}

1
cli_test.go Normal file

@ -0,0 +1 @@
package main

114
fixtures/example.tfstate Normal file

@ -0,0 +1,114 @@
{
"version": 1,
"serial": 1,
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {
"aws_instance.one": {
"type": "aws_instance",
"depends_on": [
"aws_security_group.example"
],
"primary": {
"id": "i-aaaaaaaa",
"attributes": {
"ami": "ami-XXXXXXXX",
"availability_zone": "us-east-1b",
"id": "i-aaaaaaaa",
"instance_type": "t2.micro",
"key_name": "",
"private_dns": "ip-1-1-1-1.ec2.internal",
"private_ip": "1.1.1.1",
"public_dns": "",
"public_ip": "",
"security_groups.#": "1",
"security_groups.0": "sg-cccccccc",
"subnet_id": "subnet-XXXXXXXX",
"tenancy": "default"
}
}
},
"aws_instance.two": {
"type": "aws_instance",
"depends_on": [
"aws_security_group.example"
],
"primary": {
"id": "i-bbbbbbbb",
"attributes": {
"ami": "ami-XXXXXXXX",
"availability_zone": "us-east-1b",
"id": "i-bbbbbbbb",
"instance_type": "t2.micro",
"key_name": "",
"private_dns": "ip-2-2-2-2.ec2.internal",
"private_ip": "2.2.2.2",
"public_dns": "",
"public_ip": "",
"security_groups.#": "1",
"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"
}
}
}
}
}
]
}

54
main.go Normal file

@ -0,0 +1,54 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
)
var list = flag.Bool("list", false, "list mode")
var host = flag.String("host", "", "host mode")
func main() {
flag.Parse()
file := flag.Arg(0)
if file == "" {
fmt.Printf("Usage: %s [options] path\n", os.Args[0])
os.Exit(1)
}
if !*list && *host == "" {
fmt.Println("Either --host or --list must be specified")
os.Exit(1)
}
path, err := filepath.Abs(file)
if err != nil {
fmt.Printf("Invalid file: %s\n", err)
os.Exit(1)
}
stateFile, err := os.Open(path)
defer stateFile.Close()
if err != nil {
fmt.Printf("Error opening tfstate file: %s\n", err)
os.Exit(1)
}
var s state
err = s.read(stateFile)
if err != nil {
fmt.Printf("Error reading tfstate file: %s\n", err)
os.Exit(1)
}
if *list {
os.Exit(cmdList(os.Stdout, os.Stderr, &s))
} else if *host != "" {
os.Exit(cmdHost(os.Stdout, os.Stderr, &s, *host))
}
}

59
parser.go Normal file

@ -0,0 +1,59 @@
package main
import (
"io"
"io/ioutil"
"encoding/json"
)
type state struct {
Modules []moduleState `json:"modules"`
}
// read populates the state object from a statefile.
func (s *state) read(stateFile io.Reader) error {
// read statefile contents
b, err := ioutil.ReadAll(stateFile)
if err != nil {
return err
}
// parse into struct
err = json.Unmarshal(b, s)
if err != nil {
return err
}
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)
for _, m := range s.Modules {
for k, r := range m.Resources {
if r.Type == "aws_instance" {
inst[k] = r.Primary
}
}
}
return inst
}
type moduleState struct {
Resources map[string]resourceState `json:"resources"`
}
type resourceState struct {
Type string `json:"type"`
Primary instanceState `json:"primary"`
}
type instanceState struct {
ID string `json:"id"`
Attributes map[string]string `json:"attributes,omitempty"`
}

116
parser_test.go Normal file

@ -0,0 +1,116 @@
package main
import (
"github.com/stretchr/testify/assert"
"testing"
"strings"
)
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"
}
}
}
}
}
]
}
`
func TestStateRead(t *testing.T) {
r := strings.NewReader(exampleStateFile)
var s state
err := s.read(r)
assert.Nil(t, err)
exp := state{
Modules: []moduleState{
moduleState{
Resources: map[string]resourceState{
"aws_instance.one": resourceState{
Type: "aws_instance",
Primary: instanceState{
ID: "i-aaaaaaaa",
Attributes: map[string]string{
"ami": "ami-XXXXXXXX",
"id": "i-aaaaaaaa",
},
},
},
"aws_instance.two": resourceState{
Type: "aws_instance",
Primary: instanceState{
ID: "i-bbbbbbbb",
Attributes: map[string]string{
"ami": "ami-YYYYYYYY",
"id": "i-bbbbbbbb",
},
},
},
"aws_security_group.example": resourceState{
Type: "aws_security_group",
Primary: instanceState{
ID: "sg-cccccccc",
Attributes: map[string]string{
"description": "Whatever",
"id": "sg-cccccccc",
},
},
},
},
},
},
}
assert.Equal(t, exp, s)
}
func TestInstances(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["aws_instance.one"].ID)
assert.Equal(t, "i-bbbbbbbb", inst["aws_instance.two"].ID)
}

@ -1,139 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"github.com/hashicorp/terraform/terraform"
"os"
"path/filepath"
)
var list = flag.Bool("list", false, "list mode")
var host = flag.String("host", "", "host mode")
type Host struct {
Name string `json:"name"`
IP string `json:"ip"`
vars HostVars
}
type HostVars map[string]interface{}
type Group struct {
name string
hosts []*Host
}
func main() {
flag.Parse()
file := flag.Arg(0)
if file == "" {
fmt.Printf("Usage: %s [options] path\n", os.Args[0])
os.Exit(1)
}
if !*list && *host == "" {
fmt.Println("Either --host or --list must be specified")
os.Exit(1)
}
path, err := filepath.Abs(file)
if err != nil {
fmt.Printf("Invalid file: %s\n", err)
os.Exit(1)
}
f, err := os.Open(path)
if err != nil {
fmt.Printf("Error opening tfstate file: %s\n", err)
os.Exit(1)
}
state, err := terraform.ReadState(f)
if err != nil {
fmt.Printf("Error reading state: %s\n", err)
os.Exit(1)
}
if *list {
hosts := mustGetList(state)
os.Stdout.Write(mustMarshal(hosts))
} else if *host != "" {
host := mustGetHost(state, *host)
os.Stdout.Write(mustMarshal(host.vars))
}
}
func mustGetList(state *terraform.State) interface{} {
hosts, err := getList(state)
if err != nil {
fmt.Printf("Error getting host list: %s\n", err)
os.Exit(1)
}
return hosts
}
func mustGetHost(state *terraform.State, hostname string) *Host {
host, err := getHost(state, hostname)
if err != nil {
fmt.Printf("Error getting host variables: %s\n", err)
os.Exit(1)
}
return host
}
func mustMarshal(whatever interface{}) []byte {
json, err := json.Marshal(whatever)
if err != nil {
fmt.Printf("Error encoding JSON: %s\n", err)
os.Exit(1)
}
return json
}
func getList(state *terraform.State) (interface{}, error) {
hostnames := make([]string, 0)
for _, h := range readHosts(state) {
hostnames = append(hostnames, h.IP)
}
groups := map[string][]string{"production": hostnames}
return groups, nil
}
func getHost(state *terraform.State, hostname string) (*Host, error) {
for _, h := range readHosts(state) {
if h.IP == hostname {
return h, nil
}
}
return nil, fmt.Errorf("No such host: %s", hostname)
}
func readHosts(state *terraform.State) []*Host {
hosts := make([]*Host, 0)
for _, resource := range state.Resources {
if resource.Type == "aws_instance" {
host := &Host{
Name: resource.Attributes["id"],
IP: resource.Attributes["public_ip"],
vars: HostVars{},
}
hosts = append(hosts, host)
}
}
return hosts
}