diff --git a/config/config.go b/config/config.go index 3c8f8826d..4000eb077 100644 --- a/config/config.go +++ b/config/config.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-version" "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" "github.com/hashicorp/terraform/helper/hilmapstructure" @@ -27,6 +28,7 @@ type Config struct { // any meaningful directory. Dir string + Terraform *Terraform Atlas *AtlasConfig Modules []*Module ProviderConfigs []*ProviderConfig @@ -39,6 +41,12 @@ 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 @@ -236,6 +244,30 @@ func (c *Config) Validate() error { "Unknown root level key: %s", k)) } + // Validate the Terraform config + if tf := c.Terraform; tf != nil { + if raw := tf.RequiredVersion; raw != "" { + // Check that the value has no interpolations + rc, err := NewRawConfig(map[string]interface{}{ + "root": raw, + }) + if err != nil { + errs = append(errs, fmt.Errorf( + "terraform.required_version: %s", err)) + } else if len(rc.Interpolations) > 0 { + errs = append(errs, fmt.Errorf( + "terraform.required_version: cannot contain interpolations")) + } else { + // Check it is valid + _, err := version.NewConstraint(raw) + if err != nil { + errs = append(errs, fmt.Errorf( + "terraform.required_version: invalid syntax: %s", err)) + } + } + } + } + vars := c.InterpolatedVariables() varMap := make(map[string]*Variable) for _, v := range c.Variables { diff --git a/config/config_test.go b/config/config_test.go index 7c7776b94..a0078671b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -137,6 +137,27 @@ func TestConfigValidate(t *testing.T) { } } +func TestConfigValidate_tfVersion(t *testing.T) { + c := testConfig(t, "validate-tf-version") + if err := c.Validate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestConfigValidate_tfVersionBad(t *testing.T) { + c := testConfig(t, "validate-bad-tf-version") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + +func TestConfigValidate_tfVersionInterpolations(t *testing.T) { + c := testConfig(t, "validate-tf-version-interp") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestConfigValidate_badDependsOn(t *testing.T) { c := testConfig(t, "validate-bad-depends-on") if err := c.Validate(); err == nil { diff --git a/config/loader_hcl.go b/config/loader_hcl.go index 2122d622e..f6ef24a7b 100644 --- a/config/loader_hcl.go +++ b/config/loader_hcl.go @@ -19,13 +19,14 @@ type hclConfigurable struct { func (t *hclConfigurable) Config() (*Config, error) { validKeys := map[string]struct{}{ - "atlas": struct{}{}, - "data": struct{}{}, - "module": struct{}{}, - "output": struct{}{}, - "provider": struct{}{}, - "resource": struct{}{}, - "variable": struct{}{}, + "atlas": struct{}{}, + "data": struct{}{}, + "module": struct{}{}, + "output": struct{}{}, + "provider": struct{}{}, + "resource": struct{}{}, + "terraform": struct{}{}, + "variable": struct{}{}, } // Top-level item should be the object list @@ -37,6 +38,15 @@ func (t *hclConfigurable) Config() (*Config, error) { // Start building up the actual configuration. config := new(Config) + // Terraform config + if o := list.Filter("terraform"); len(o.Items) > 0 { + var err error + config.Terraform, err = loadTerraformHcl(o) + if err != nil { + return nil, err + } + } + // Build the variables if vars := list.Filter("variable"); len(vars.Items) > 0 { var err error @@ -190,6 +200,32 @@ func loadFileHcl(root string) (configurable, []string, error) { return result, nil, nil } +// Given a handle to a HCL object, this transforms it into the Terraform config +func loadTerraformHcl(list *ast.ObjectList) (*Terraform, error) { + if len(list.Items) > 1 { + return nil, fmt.Errorf("only one 'terraform' block allowed per module") + } + + // Get our one item + item := list.Items[0] + + // 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). + // + // We should still keep track of unknown keys to validate later, but + // HCL doesn't currently support that. + + var config Terraform + if err := hcl.DecodeObject(&config, item.Val); err != nil { + return nil, fmt.Errorf( + "Error reading terraform config: %s", + err) + } + + return &config, nil +} + // Given a handle to a HCL object, this transforms it into the Atlas // configuration. func loadAtlasHcl(list *ast.ObjectList) (*AtlasConfig, error) { diff --git a/config/loader_test.go b/config/loader_test.go index 73b09a6fe..8c0dfaa6d 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -160,6 +160,11 @@ func TestLoadFileBasic(t *testing.T) { t.Fatalf("bad: %#v", c.Dir) } + expectedTF := &Terraform{RequiredVersion: "foo"} + if !reflect.DeepEqual(c.Terraform, expectedTF) { + t.Fatalf("bad: %#v", c.Terraform) + } + expectedAtlas := &AtlasConfig{Name: "mitchellh/foo"} if !reflect.DeepEqual(c.Atlas, expectedAtlas) { t.Fatalf("bad: %#v", c.Atlas) diff --git a/config/test-fixtures/basic.tf b/config/test-fixtures/basic.tf index 45314d54b..aa5a5c6ed 100644 --- a/config/test-fixtures/basic.tf +++ b/config/test-fixtures/basic.tf @@ -1,3 +1,7 @@ +terraform { + required_version = "foo" +} + variable "foo" { default = "bar" description = "bar" diff --git a/config/test-fixtures/validate-bad-tf-version/main.tf b/config/test-fixtures/validate-bad-tf-version/main.tf new file mode 100644 index 000000000..47348a350 --- /dev/null +++ b/config/test-fixtures/validate-bad-tf-version/main.tf @@ -0,0 +1,3 @@ +terraform { + required_version = "nope" +} diff --git a/config/test-fixtures/validate-tf-version-interp/main.tf b/config/test-fixtures/validate-tf-version-interp/main.tf new file mode 100644 index 000000000..51e68adb3 --- /dev/null +++ b/config/test-fixtures/validate-tf-version-interp/main.tf @@ -0,0 +1,3 @@ +terraform { + required_version = "${var.foo}" +} diff --git a/config/test-fixtures/validate-tf-version/main.tf b/config/test-fixtures/validate-tf-version/main.tf new file mode 100644 index 000000000..9bbc92303 --- /dev/null +++ b/config/test-fixtures/validate-tf-version/main.tf @@ -0,0 +1,3 @@ +terraform { + required_version = "> 0.7.0" +}