mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-27 17:06:27 -06:00
Merge pull request #10080 from hashicorp/f-tf-version
terraform: support version requirement in configuration
This commit is contained in:
commit
25d19ef3d0
@ -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
|
||||
@ -237,6 +245,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 {
|
||||
|
@ -182,6 +182,27 @@ func TestConfigValidate_table(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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -1,3 +1,7 @@
|
||||
terraform {
|
||||
required_version = "foo"
|
||||
}
|
||||
|
||||
variable "foo" {
|
||||
default = "bar"
|
||||
description = "bar"
|
||||
|
3
config/test-fixtures/validate-bad-tf-version/main.tf
Normal file
3
config/test-fixtures/validate-bad-tf-version/main.tf
Normal file
@ -0,0 +1,3 @@
|
||||
terraform {
|
||||
required_version = "nope"
|
||||
}
|
3
config/test-fixtures/validate-tf-version-interp/main.tf
Normal file
3
config/test-fixtures/validate-tf-version-interp/main.tf
Normal file
@ -0,0 +1,3 @@
|
||||
terraform {
|
||||
required_version = "${var.foo}"
|
||||
}
|
3
config/test-fixtures/validate-tf-version/main.tf
Normal file
3
config/test-fixtures/validate-tf-version/main.tf
Normal file
@ -0,0 +1,3 @@
|
||||
terraform {
|
||||
required_version = "> 0.7.0"
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -0,0 +1 @@
|
||||
terraform { required_version = ">= 0.5.0" }
|
@ -0,0 +1,3 @@
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
1
terraform/test-fixtures/context-required-version/main.tf
Normal file
1
terraform/test-fixtures/context-required-version/main.tf
Normal file
@ -0,0 +1 @@
|
||||
terraform {}
|
69
terraform/version_required.go
Normal file
69
terraform/version_required.go
Normal file
@ -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
|
||||
}
|
77
website/source/docs/configuration/terraform.html.md
Normal file
77
website/source/docs/configuration/terraform.html.md
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Configuring Terraform"
|
||||
sidebar_current: "docs-config-terraform"
|
||||
description: |-
|
||||
The `terraform` configuration section is used to configure Terraform itself, such as requiring a minimum Terraform version to execute a configuration.
|
||||
---
|
||||
|
||||
# Terraform Configuration
|
||||
|
||||
The `terraform` configuration section is used to configure Terraform itself,
|
||||
such as requiring a minimum Terraform version to execute a configuration.
|
||||
|
||||
This page assumes you're familiar with the
|
||||
[configuration syntax](/docs/configuration/syntax.html)
|
||||
already.
|
||||
|
||||
## Example
|
||||
|
||||
Terraform configuration looks like the following:
|
||||
|
||||
```
|
||||
terraform {
|
||||
required_version = "> 0.7.0"
|
||||
}
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
The `terraform` block configures the behavior of Terraform itself.
|
||||
|
||||
The currently only allowed configuration within this block is
|
||||
`required_version`. This setting specifies a set of version constraints
|
||||
that must me bet to perform operations on this configuration. If the
|
||||
running Terraform version doesn't meet these constraints, an error
|
||||
is shown. See the section below dedicated to this option.
|
||||
|
||||
**No value within the `terraform` block can use interpolations.** The
|
||||
`terraform` block is loaded very early in the execution of Terraform
|
||||
and interpolations are not yet available.
|
||||
|
||||
## Specifying a Required Terraform Version
|
||||
|
||||
The `required_version` setting can be used to require a specific version
|
||||
of Terraform. If the running version of Terraform doesn't match the
|
||||
constraints specified, Terraform will show an error and exit.
|
||||
|
||||
When [modules](/docs/configuration/modules.html) are used, all Terraform
|
||||
version requirements specified by the complete module tree must be
|
||||
satisified. This means that the `required_version` setting can be used
|
||||
by a module to require that all consumers of a module also use a specific
|
||||
version.
|
||||
|
||||
The value of this configuration is a comma-separated list of constraints.
|
||||
A constraint is an operator followed by a version, such as `> 0.7.0`.
|
||||
Constraints support the following operations:
|
||||
|
||||
* `=` (or no operator): exact version equality
|
||||
* `!=`: version not equal
|
||||
* `>`, `>=`, `<`, `<=`: version comparison, where "greater than" is
|
||||
a larger version number.
|
||||
* `~>`: pessimistic constraint operator. Example: for `~> 0.9`, this means
|
||||
`>= 0.9, < 1.0`. Example: for `~> 0.8.4`, this means `>= 0.8.4, < 0.9`
|
||||
|
||||
For modules, a minimum version is recommended, such as `> 0.8.0`. This
|
||||
minimum version ensures that a module operates as expected, but gives
|
||||
the consumer flexibility to use newer versions.
|
||||
|
||||
## Syntax
|
||||
|
||||
The full syntax is:
|
||||
|
||||
```
|
||||
terraform {
|
||||
required_version = VALUE
|
||||
}
|
||||
```
|
@ -49,6 +49,10 @@
|
||||
<a href="/docs/configuration/modules.html">Modules</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-config-terraform") %>>
|
||||
<a href="/docs/configuration/terraform.html">Terraform</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-config-atlas") %>>
|
||||
<a href="/docs/configuration/atlas.html">Atlas</a>
|
||||
</li>
|
||||
|
Loading…
Reference in New Issue
Block a user