config: add "backend" loading to the Terraform section
This commit is contained in:
parent
3e47bad40a
commit
7b342100d0
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
terraform {
|
||||
backend "foo" {
|
||||
foo = "bar"
|
||||
bar = ["baz"]
|
||||
map = { a = "b" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
terraform {}
|
|
@ -0,0 +1 @@
|
|||
# Empty
|
|
@ -0,0 +1,4 @@
|
|||
terraform {
|
||||
backend "foo" {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
terraform {
|
||||
backend "s3" {}
|
||||
backend "s4" {}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
terraform {
|
||||
backend "s3" {
|
||||
foo = "bar"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue