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
|
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.
|
// AtlasConfig is the configuration for building in HashiCorp's Atlas.
|
||||||
type AtlasConfig struct {
|
type AtlasConfig struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -50,6 +50,26 @@ func (c *Config) TestString() string {
|
||||||
return strings.TrimSpace(buf.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 {
|
func modulesStr(ms []*Module) string {
|
||||||
result := ""
|
result := ""
|
||||||
order := make([]int, 0, len(ms))
|
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
|
// Get our one item
|
||||||
item := list.Items[0]
|
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
|
// NOTE: We purposely don't validate unknown HCL keys here so that
|
||||||
// we can potentially read _future_ Terraform version config (to
|
// we can potentially read _future_ Terraform version config (to
|
||||||
// still be able to validate the required version).
|
// still be able to validate the required version).
|
||||||
|
@ -223,9 +231,62 @@ func loadTerraformHcl(list *ast.ObjectList) (*Terraform, error) {
|
||||||
err)
|
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
|
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
|
// Given a handle to a HCL object, this transforms it into the Atlas
|
||||||
// configuration.
|
// configuration.
|
||||||
func loadAtlasHcl(list *ast.ObjectList) (*AtlasConfig, error) {
|
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) {
|
func TestLoadJSONBasic(t *testing.T) {
|
||||||
raw, err := ioutil.ReadFile(filepath.Join(fixtureDir, "basic.tf.json"))
|
raw, err := ioutil.ReadFile(filepath.Join(fixtureDir, "basic.tf.json"))
|
||||||
if err != nil {
|
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