From 313d404eb339ec8b95580388010200e8b3e4a599 Mon Sep 17 00:00:00 2001 From: Husayn Arrah Date: Fri, 9 Jun 2017 21:25:34 +0200 Subject: [PATCH] Add support for remote state (#60) * add support for remote state in version 0.9 * remove .terraform/terraform.tfstate as a source --- README.md | 16 +++++++++++++- input.go | 8 +------ input_test.go | 20 ++++++++++++++++-- main.go | 58 ++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 038dc02..146f90c 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,25 @@ Can be provisioned separately with: ## More Usage Ansible doesn't seem to support calling a dynamic inventory script with params, -so if you need to specify the location of your state file, set the `TF_STATE` +so if you need to specify the location of your state file or terraform directory, set the `TF_STATE` environment variable before running `ansible-playbook`, like: + TF_STATE=deploy/terraform.tfstate ansible-playbook --inventory-file=/path/to/terraform-inventory deploy/playbook.yml + or + + TF_STATE=../terraform ansible-playbook --inventory-file=/path/to/terraform-inventory deploy/playbook.yml + +If `TF_STATE` is a file, it parses the file as json, if `TF_STATE` is a directory, it runs `terraform state pull` inside the directory, which is supports both local and remote terraform state. + +It looks for state config in this order + +- `TF_STATE`: environment variable of where to find either a statefile or a terraform project +- `TI_TFSTATE`: another environment variable similar to TF_STATE +- `terraform.tfstate`: it looks in the state file in the current directory. +- `.`: lastly it assumes you are at the root of a terraform project. + Alternately, if you need to do something fancier (like downloading your state file from S3 before running), you might wrap this tool with a shell script, and call that instead. Something like: diff --git a/input.go b/input.go index ca7b8bb..5082726 100644 --- a/input.go +++ b/input.go @@ -25,11 +25,5 @@ func GetInputPath(fs vfs.Filesystem, env venv.Env) string { return fn } - fn = ".terraform/terraform.tfstate" - _, err = fs.Stat(fn) - if err == nil { - return fn - } - - return "" + return "." } diff --git a/input_test.go b/input_test.go index 78b23f1..7dde974 100644 --- a/input_test.go +++ b/input_test.go @@ -13,11 +13,12 @@ import ( ) func TestGetInputPath(t *testing.T) { - assert.Equal(t, "", GetInputPath(memfs.Create(), venv.Mock())) + assert.Equal(t, ".", GetInputPath(memfs.Create(), venv.Mock())) assert.Equal(t, "aaa", GetInputPath(memfs.Create(), envWith(map[string]string{"TF_STATE": "aaa"}))) assert.Equal(t, "bbb", GetInputPath(memfs.Create(), envWith(map[string]string{"TI_TFSTATE": "bbb"}))) assert.Equal(t, "terraform.tfstate", GetInputPath(fsWithFiles([]string{"terraform.tfstate"}), venv.Mock())) - assert.Equal(t, ".terraform/terraform.tfstate", GetInputPath(fsWithFiles([]string{".terraform/terraform.tfstate"}), venv.Mock())) + assert.Equal(t, ".", GetInputPath(fsWithFiles([]string{".terraform/terraform.tfstate"}), venv.Mock())) + assert.Equal(t, "terraform", GetInputPath(fsWithDirs([]string{"terraform"}), envWith(map[string]string{"TF_STATE": "terraform"}))) } func envWith(env map[string]string) venv.Env { @@ -53,6 +54,21 @@ func fsWithFiles(filenames []string) vfs.Filesystem { return fs } +func fsWithDirs(dirs []string) vfs.Filesystem { + fs := memfs.Create() + + var err error + + for _, fp := range dirs { + err = vfs.MkdirAll(fs, fp, 0700) + if err != nil { + panic(err) + } + } + + return fs +} + // TODO: Upgrade this later with file contents. func touchFile(fs vfs.Filesystem, filename string) error { return writeFile(fs, filename, []byte{}, 0600) diff --git a/main.go b/main.go index d534f62..19d4727 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,13 @@ package main import ( + "bytes" "flag" "fmt" "github.com/adammck/venv" "github.com/blang/vfs" "os" + "os/exec" "path/filepath" ) @@ -23,17 +25,13 @@ func main() { return } + fs := vfs.OS() if file == "" { - fs := vfs.OS() + env := venv.OS() file = GetInputPath(fs, env) } - if file == "" { - fmt.Printf("Usage: %s [options] path\n", os.Args[0]) - os.Exit(1) - } - if !*list && *host == "" && !*inventory { fmt.Fprint(os.Stderr, "Either --host or --list must be specified") os.Exit(1) @@ -45,17 +43,53 @@ func main() { os.Exit(1) } - stateFile, err := os.Open(path) - defer stateFile.Close() + f, err := fs.Stat(path) + if err != nil { - fmt.Fprintf(os.Stderr, "Error opening tfstate file: %s\n", err) + fmt.Fprintf(os.Stderr, "Invalid file: %s\n", err) os.Exit(1) } var s state - err = s.read(stateFile) - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading tfstate file: %s\n", err) + + if !f.IsDir() { + stateFile, err := os.Open(path) + defer stateFile.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "Error opening tfstate file: %s\n", err) + os.Exit(1) + } + + err = s.read(stateFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading tfstate file: %s\n", err) + os.Exit(1) + } + } + + if f.IsDir() { + cmd := exec.Command("terraform", "state", "pull") + cmd.Dir = path + var out bytes.Buffer + cmd.Stdout = &out + + err = cmd.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "Error running `terraform state pull` in directory %s, %s\n", path, err) + os.Exit(1) + } + + err = s.read(&out) + + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading `terraform state pull` output: %s\n", err) + os.Exit(1) + } + + } + + if s.Modules == nil { + fmt.Printf("Usage: %s [options] path\npath: this is either a path to a state file or a folder from which `terraform commands` are valid\n", os.Args[0]) os.Exit(1) }