mirror of
https://github.com/adammck/terraform-inventory
synced 2024-11-22 20:01:58 +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