config: parse and validate terraform.required_version

This commit is contained in:
Mitchell Hashimoto 2016-11-12 16:20:33 -08:00
parent 8d993d9edd
commit 85d3439fa0
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
8 changed files with 114 additions and 7 deletions

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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)

View File

@ -1,3 +1,7 @@
terraform {
required_version = "foo"
}
variable "foo" {
default = "bar"
description = "bar"

View File

@ -0,0 +1,3 @@
terraform {
required_version = "nope"
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = "${var.foo}"
}

View File

@ -0,0 +1,3 @@
terraform {
required_version = "> 0.7.0"
}