config: add "backend" loading to the Terraform section

This commit is contained in:
Mitchell Hashimoto 2017-01-18 20:46:49 -08:00
parent 3e47bad40a
commit 7b342100d0
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
13 changed files with 281 additions and 6 deletions

View File

@ -41,12 +41,6 @@ type Config struct {
unknownKeys []string
}
// Terraform is the Terraform meta-configuration that can be present
// in configuration files for configuring Terraform itself.
type Terraform struct {
RequiredVersion string `hcl:"required_version"` // Required Terraform version (constraint)
}
// AtlasConfig is the configuration for building in HashiCorp's Atlas.
type AtlasConfig struct {
Name string

View File

@ -50,6 +50,26 @@ func (c *Config) TestString() string {
return strings.TrimSpace(buf.String())
}
func terraformStr(t *Terraform) string {
result := ""
if b := t.Backend; b != nil {
result += fmt.Sprintf("backend (%s)\n", b.Type)
keys := make([]string, 0, len(b.RawConfig.Raw))
for k, _ := range b.RawConfig.Raw {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
result += fmt.Sprintf(" %s\n", k)
}
}
return strings.TrimSpace(result)
}
func modulesStr(ms []*Module) string {
result := ""
order := make([]int, 0, len(ms))

View File

@ -0,0 +1,48 @@
package config
import (
"github.com/mitchellh/hashstructure"
)
// Terraform is the Terraform meta-configuration that can be present
// in configuration files for configuring Terraform itself.
type Terraform struct {
RequiredVersion string `hcl:"required_version"` // Required Terraform version (constraint)
Backend *Backend // See Backend struct docs
}
// Backend is the configuration for the "backend" to use with Terraform.
// A backend is responsible for all major behavior of Terraform's core.
// The abstraction layer above the core (the "backend") allows for behavior
// such as remote operation.
type Backend struct {
Type string
RawConfig *RawConfig
// Hash is a unique hash code representing the original configuration
// of the backend. This won't be recomputed unless Rehash is called.
Hash uint64
}
// Hash returns a unique content hash for this backend's configuration
// as a uint64 value.
func (b *Backend) Rehash() uint64 {
// If we have no backend, the value is zero
if b == nil {
return 0
}
// Use hashstructure to hash only our type with the config.
code, err := hashstructure.Hash(map[string]interface{}{
"type": b.Type,
"config": b.RawConfig.Raw,
}, nil)
// This should never happen since we have just some basic primitives
// so panic if there is an error.
if err != nil {
panic(err)
}
return code
}

View File

@ -0,0 +1,55 @@
package config
import (
"fmt"
"testing"
)
func TestBackendHash(t *testing.T) {
// WARNING: The codes below should _never_ change. If they change, it
// means that a future TF version may falsely recognize unchanged backend
// configuration as changed. Ultimately this should have no adverse
// affect but it is annoying for users and should be avoided if possible.
cases := []struct {
Name string
Fixture string
Code uint64
}{
{
"no backend config",
"backend-hash-empty",
0,
},
{
"backend config with only type",
"backend-hash-type-only",
17852588448730441876,
},
{
"backend config with type and config",
"backend-hash-basic",
10288498853650209002,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c := testConfig(t, tc.Fixture)
err := c.Validate()
if err != nil {
t.Fatalf("err: %s", err)
}
var actual uint64
if c.Terraform != nil && c.Terraform.Backend != nil {
actual = c.Terraform.Backend.Hash
}
if actual != tc.Code {
t.Fatalf("bad: %d != %d", actual, tc.Code)
}
})
}
}

View File

@ -209,6 +209,14 @@ func loadTerraformHcl(list *ast.ObjectList) (*Terraform, error) {
// Get our one item
item := list.Items[0]
// We need the item value as an ObjectList
var listVal *ast.ObjectList
if ot, ok := item.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("terraform block: should be an object")
}
// NOTE: We purposely don't validate unknown HCL keys here so that
// we can potentially read _future_ Terraform version config (to
// still be able to validate the required version).
@ -223,9 +231,62 @@ func loadTerraformHcl(list *ast.ObjectList) (*Terraform, error) {
err)
}
// If we have provisioners, then parse those out
if os := listVal.Filter("backend"); len(os.Items) > 0 {
var err error
config.Backend, err = loadTerraformBackendHcl(os)
if err != nil {
return nil, fmt.Errorf(
"Error reading backend config for terraform block: %s",
err)
}
}
return &config, nil
}
// Loads the Backend configuration from an object list.
func loadTerraformBackendHcl(list *ast.ObjectList) (*Backend, error) {
if len(list.Items) > 1 {
return nil, fmt.Errorf("only one 'backend' block allowed")
}
// Get our one item
item := list.Items[0]
// Verify the keys
if len(item.Keys) != 1 {
return nil, fmt.Errorf(
"position %s: 'backend' must be followed by exactly one string: a type",
item.Pos())
}
typ := item.Keys[0].Token.Value().(string)
// Decode the raw config
var config map[string]interface{}
if err := hcl.DecodeObject(&config, item.Val); err != nil {
return nil, fmt.Errorf(
"Error reading backend config: %s",
err)
}
rawConfig, err := NewRawConfig(config)
if err != nil {
return nil, fmt.Errorf(
"Error reading backend config: %s",
err)
}
b := &Backend{
Type: typ,
RawConfig: rawConfig,
}
b.Hash = b.Rehash()
return b, nil
}
// Given a handle to a HCL object, this transforms it into the Atlas
// configuration.
func loadAtlasHcl(list *ast.ObjectList) (*AtlasConfig, error) {

View File

@ -334,6 +334,43 @@ func TestLoadFile_outputDependsOn(t *testing.T) {
}
}
func TestLoadFile_terraformBackend(t *testing.T) {
c, err := LoadFile(filepath.Join(fixtureDir, "terraform-backend.tf"))
if err != nil {
t.Fatalf("err: %s", err)
}
if c == nil {
t.Fatal("config should not be nil")
}
if c.Dir != "" {
t.Fatalf("bad: %#v", c.Dir)
}
{
actual := terraformStr(c.Terraform)
expected := strings.TrimSpace(`
backend (s3)
foo`)
if actual != expected {
t.Fatalf("bad:\n%s", actual)
}
}
}
func TestLoadFile_terraformBackendMulti(t *testing.T) {
_, err := LoadFile(filepath.Join(fixtureDir, "terraform-backend-multi.tf"))
if err == nil {
t.Fatal("expected error")
}
errorStr := err.Error()
if !strings.Contains(errorStr, "only one 'backend'") {
t.Fatalf("bad: expected error has wrong text: %s", errorStr)
}
}
func TestLoadJSONBasic(t *testing.T) {
raw, err := ioutil.ReadFile(filepath.Join(fixtureDir, "basic.tf.json"))
if err != nil {

38
config/module/testing.go Normal file
View File

@ -0,0 +1,38 @@
package module
import (
"io/ioutil"
"os"
"testing"
"github.com/hashicorp/go-getter"
)
// TestTree loads a module at the given path and returns the tree as well
// as a function that should be deferred to clean up resources.
func TestTree(t *testing.T, path string) (*Tree, func()) {
// Create a temporary directory for module storage
dir, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
return nil, nil
}
// Load the module
mod, err := NewTreeModule("", path)
if err != nil {
t.Fatalf("err: %s", err)
return nil, nil
}
// Get the child modules
s := &getter.FolderStorage{StorageDir: dir}
if err := mod.Load(s, GetModeGet); err != nil {
t.Fatalf("err: %s", err)
return nil, nil
}
return mod, func() {
os.RemoveAll(dir)
}
}

View File

@ -0,0 +1,7 @@
terraform {
backend "foo" {
foo = "bar"
bar = ["baz"]
map = { a = "b" }
}
}

View File

@ -0,0 +1 @@
terraform {}

View File

@ -0,0 +1 @@
# Empty

View File

@ -0,0 +1,4 @@
terraform {
backend "foo" {
}
}

View File

@ -0,0 +1,4 @@
terraform {
backend "s3" {}
backend "s4" {}
}

View File

@ -0,0 +1,5 @@
terraform {
backend "s3" {
foo = "bar"
}
}