mirror of
https://github.com/adammck/terraform-inventory
synced 2024-11-22 15:52:01 +01:00
parent
9f33a7ec91
commit
2d1c7fdfd9
48
cli.go
Normal file
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
1
cli_test.go
Normal file
@ -0,0 +1 @@
|
||||
package main
|
114
fixtures/example.tfstate
Normal file
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
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
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
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user