From 2c467e0f7481eca92c76bbaaba67e7895d2040d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Nov 2016 16:50:26 -0800 Subject: [PATCH] terraform: verify version requirements from configuration --- terraform/context.go | 7 ++ terraform/context_test.go | 78 +++++++++++++++++++ .../child/main.tf | 1 + .../context-required-version-module/main.tf | 3 + .../context-required-version/main.tf | 1 + terraform/version_required.go | 69 ++++++++++++++++ 6 files changed, 159 insertions(+) create mode 100644 terraform/test-fixtures/context-required-version-module/child/main.tf create mode 100644 terraform/test-fixtures/context-required-version-module/main.tf create mode 100644 terraform/test-fixtures/context-required-version/main.tf create mode 100644 terraform/version_required.go diff --git a/terraform/context.go b/terraform/context.go index 7720bd8ac..ffd85535b 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -102,6 +102,13 @@ type Context struct { // should not be mutated in any way, since the pointers are copied, not // the values themselves. func NewContext(opts *ContextOpts) (*Context, error) { + // Validate the version requirement if it is given + if opts.Module != nil { + if err := checkRequiredVersion(opts.Module); err != nil { + return nil, err + } + } + // Copy all the hooks and add our stop hook. We don't append directly // to the Config so that we're not modifying that in-place. sh := new(stopHook) diff --git a/terraform/context_test.go b/terraform/context_test.go index f849d21dc..977b9e578 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -6,9 +6,87 @@ import ( "testing" "time" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/flatmap" ) +func TestNewContextRequiredVersion(t *testing.T) { + cases := []struct { + Name string + Module string + Version string + Value string + Err bool + }{ + { + "no requirement", + "", + "0.1.0", + "", + false, + }, + + { + "doesn't match", + "", + "0.1.0", + "> 0.6.0", + true, + }, + + { + "matches", + "", + "0.7.0", + "> 0.6.0", + false, + }, + + { + "module matches", + "context-required-version-module", + "0.5.0", + "", + false, + }, + + { + "module doesn't match", + "context-required-version-module", + "0.4.0", + "", + true, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { + // Reset the version for the tests + old := SemVersion + SemVersion = version.Must(version.NewVersion(tc.Version)) + defer func() { SemVersion = old }() + + name := "context-required-version" + if tc.Module != "" { + name = tc.Module + } + mod := testModule(t, name) + if tc.Value != "" { + mod.Config().Terraform.RequiredVersion = tc.Value + } + _, err := NewContext(&ContextOpts{ + Module: mod, + }) + if (err != nil) != tc.Err { + t.Fatalf("err: %s", err) + } + if err != nil { + return + } + }) + } +} + func TestNewContextState(t *testing.T) { cases := map[string]struct { Input *ContextOpts diff --git a/terraform/test-fixtures/context-required-version-module/child/main.tf b/terraform/test-fixtures/context-required-version-module/child/main.tf new file mode 100644 index 000000000..8d915e4b7 --- /dev/null +++ b/terraform/test-fixtures/context-required-version-module/child/main.tf @@ -0,0 +1 @@ +terraform { required_version = ">= 0.5.0" } diff --git a/terraform/test-fixtures/context-required-version-module/main.tf b/terraform/test-fixtures/context-required-version-module/main.tf new file mode 100644 index 000000000..0f6991c53 --- /dev/null +++ b/terraform/test-fixtures/context-required-version-module/main.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/terraform/test-fixtures/context-required-version/main.tf b/terraform/test-fixtures/context-required-version/main.tf new file mode 100644 index 000000000..75db79290 --- /dev/null +++ b/terraform/test-fixtures/context-required-version/main.tf @@ -0,0 +1 @@ +terraform {} diff --git a/terraform/version_required.go b/terraform/version_required.go new file mode 100644 index 000000000..3cbbf5608 --- /dev/null +++ b/terraform/version_required.go @@ -0,0 +1,69 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// checkRequiredVersion verifies that any version requirements specified by +// the configuration are met. +// +// This checks the root module as well as any additional version requirements +// from child modules. +// +// This is tested in context_test.go. +func checkRequiredVersion(m *module.Tree) error { + // Check any children + for _, c := range m.Children() { + if err := checkRequiredVersion(c); err != nil { + return err + } + } + + var tf *config.Terraform + if c := m.Config(); c != nil { + tf = c.Terraform + } + + // If there is no Terraform config or the required version isn't set, + // we move on. + if tf == nil || tf.RequiredVersion == "" { + return nil + } + + // Path for errors + module := "root" + if path := normalizeModulePath(m.Path()); len(path) > 1 { + module = modulePrefixStr(path) + } + + // Check this version requirement of this module + cs, err := version.NewConstraint(tf.RequiredVersion) + if err != nil { + return fmt.Errorf( + "%s: terraform.required_version %q syntax error: %s", + module, + tf.RequiredVersion, err) + } + + if !cs.Check(SemVersion) { + return fmt.Errorf( + "The currently running version of Terraform doesn't meet the\n"+ + "version requirements explicitly specified by the configuration.\n"+ + "Please use the required version or update the configuration.\n"+ + "Note that version requirements are usually set for a reason, so\n"+ + "we recommend verifying with whoever set the version requirements\n"+ + "prior to making any manual changes.\n\n"+ + " Module: %s\n"+ + " Required version: %s\n"+ + " Current version: %s", + module, + tf.RequiredVersion, + SemVersion) + } + + return nil +}