Merge pull request #24894 from hashicorp/alisdair/013upgrade-required-version

command: 0.13upgrade command checks and updates required_version
This commit is contained in:
Alisdair McDiarmid 2020-05-08 08:50:56 -04:00 committed by GitHub
commit 0104e63c64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 239 additions and 105 deletions

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -140,14 +141,35 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
)) ))
} }
// Check Terraform required_version constraints
for _, file := range files {
for _, constraint := range file.CoreVersionConstraints {
if !constraint.Required.Check(tfversion.SemVer) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported Terraform Core version",
Detail: fmt.Sprintf(
"This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.",
tfversion.String(),
),
Subject: &constraint.DeclRange,
})
}
}
}
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
// Build up a list of required providers, uniquely by local name // Build up a list of required providers, uniquely by local name
requiredProviders := make(map[string]*configs.RequiredProvider) requiredProviders := make(map[string]*configs.RequiredProvider)
var rewritePaths []string rewritePaths := make(map[string]bool)
// Step 1: copy all explicit provider requirements across // Step 1: copy all explicit provider requirements across
for path, file := range files { for path, file := range files {
for _, rps := range file.RequiredProviders { for _, rps := range file.RequiredProviders {
rewritePaths = append(rewritePaths, path) rewritePaths[path] = true
for _, rp := range rps.RequiredProviders { for _, rp := range rps.RequiredProviders {
if previous, exist := requiredProviders[rp.Name]; exist { if previous, exist := requiredProviders[rp.Name]; exist {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
@ -225,54 +247,27 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
return 1 return 1
} }
// Default output filename is "providers.tf" // Default output filename is "versions.tf", which is also where the
filename := path.Join(dir, "providers.tf") // 0.12upgrade command added the required_version constraint.
filename := path.Join(dir, "versions.tf")
// Special case: if we only have one file with a required providers // Special case: if we only have one file with a required providers
// block, output to that file instead. // block, output to that file instead.
if len(rewritePaths) == 1 { if len(rewritePaths) == 1 {
filename = rewritePaths[0] for path := range rewritePaths {
filename = path
break
}
} }
// Remove the output file from the list of paths we want to rewrite // Remove the output file from the list of paths we want to rewrite
// later. Otherwise we'd delete the required providers block after // later. Otherwise we'd delete the required providers block after
// writing it. // writing it.
for i, path := range rewritePaths { delete(rewritePaths, filename)
if path == filename {
rewritePaths = append(rewritePaths[:i], rewritePaths[i+1:]...)
break
}
}
var out *hclwrite.File // Open or create the output file
out, openDiags := c.openOrCreateFile(filename)
// If the output file doesn't exist, just create a new empty file diags = diags.Append(openDiags)
if _, err := os.Stat(filename); os.IsNotExist(err) {
out = hclwrite.NewEmptyFile()
} else if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to read configuration file",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
} else {
// Configuration file already exists, so load and parse it
config, err := ioutil.ReadFile(filename)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to read configuration file",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
var parseDiags hcl.Diagnostics
out, parseDiags = hclwrite.ParseConfig(config, filename, hcl.InitialPos)
diags = diags.Append(parseDiags)
}
if diags.HasErrors() { if diags.HasErrors() {
c.showDiagnostics(diags) c.showDiagnostics(diags)
@ -300,14 +295,22 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
var first *hclwrite.Block var first *hclwrite.Block
var rest []*hclwrite.Block var rest []*hclwrite.Block
// First terraform block in the first file. Declared at this scope so
// that it can be used to write the version constraint later, if this
// is the "versions.tf" file.
var tfBlock *hclwrite.Block
if len(requiredProviderBlocks) > 0 { if len(requiredProviderBlocks) > 0 {
// If we already have one or more required provider blocks, we'll rewrite // If we already have one or more required provider blocks, we'll rewrite
// the first one, and remove the rest. // the first one, and remove the rest.
first, rest = requiredProviderBlocks[0], requiredProviderBlocks[1:] first, rest = requiredProviderBlocks[0], requiredProviderBlocks[1:]
// Set the terraform block here for later use to update the
// required version constraint.
tfBlock = parentBlocks[first]
} else { } else {
// Otherwise, find or a create a terraform block, and add a new // Otherwise, find or a create a terraform block, and add a new
// empty required providers block to it. // empty required providers block to it.
var tfBlock *hclwrite.Block
for _, rootBlock := range root.Blocks() { for _, rootBlock := range root.Blocks() {
if rootBlock.Type() == "terraform" { if rootBlock.Type() == "terraform" {
tfBlock = rootBlock tfBlock = rootBlock
@ -359,7 +362,7 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
expr := rp.Expr().BuildTokens(nil) expr := rp.Expr().BuildTokens(nil)
// Partition the tokens into before and after the opening brace // Partition the tokens into before and after the opening brace
before, after := partitionTokensAfter(expr, hclsyntax.TokenOBrace) before, after := c.partitionTokensAfter(expr, hclsyntax.TokenOBrace)
// If the value is an empty object, add a newline between the // If the value is an empty object, add a newline between the
// braces so that the comment is not on the same line as either // braces so that the comment is not on the same line as either
@ -382,6 +385,14 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
} }
} }
// If this is the "versions.tf" file, add a new version constraint to
// the first terraform block. If this isn't the "versions.tf" file,
// we'll update that file separately.
versionsFilename := path.Join(dir, "versions.tf")
if filename == versionsFilename {
tfBlock.Body().SetAttributeValue("required_version", cty.StringVal(">= 0.13"))
}
// Remove the rest of the blocks (and the parent block, if it's empty) // Remove the rest of the blocks (and the parent block, if it's empty)
for _, rpBlock := range rest { for _, rpBlock := range rest {
tfBlock := parentBlocks[rpBlock] tfBlock := parentBlocks[rpBlock]
@ -397,30 +408,53 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
} }
// Write the config back to the file // Write the config back to the file
f, err := os.OpenFile(filename, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) writeDiags := c.writeFile(out, filename)
if err != nil { diags = diags.Append(writeDiags)
diags = diags.Append(tfdiags.Sourceless( if diags.HasErrors() {
tfdiags.Error,
"Unable to open configuration file for writing",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
_, err = out.WriteTo(f)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to rewrite configuration file",
fmt.Sprintf("Error when rewriting configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
// If the file we just updated was not a "versions.tf" file, add or
// update that file to set the required version constraint in the first
// terraform block.
if filename != versionsFilename {
file, openDiags := c.openOrCreateFile(versionsFilename)
diags = diags.Append(openDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
// Find or create a terraform block
root := file.Body()
var tfBlock *hclwrite.Block
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() == "terraform" {
tfBlock = rootBlock
break
}
}
if tfBlock == nil {
tfBlock = root.AppendNewBlock("terraform", nil)
}
// Set the required version attribute
tfBlock.Body().SetAttributeValue("required_version", cty.StringVal(">= 0.13"))
// Write the config back to the file
writeDiags := c.writeFile(file, versionsFilename)
diags = diags.Append(writeDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
}
// After successfully writing the new configuration, remove all other // After successfully writing the new configuration, remove all other
// required provider blocks from remaining configuration files. // required provider blocks from remaining configuration files.
for _, path := range rewritePaths { for path := range rewritePaths {
// Read and parse the existing file // Read and parse the existing file
config, err := ioutil.ReadFile(path) config, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
@ -459,23 +493,9 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
} }
// Write the config back to the file // Write the config back to the file
f, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) writeDiags := c.writeFile(file, path)
if err != nil { diags = diags.Append(writeDiags)
diags = diags.Append(tfdiags.Sourceless( if diags.HasErrors() {
tfdiags.Error,
"Unable to open configuration file for writing",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
_, err = file.WriteTo(f)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to rewrite configuration file",
fmt.Sprintf("Error when rewriting configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
@ -538,11 +558,65 @@ func (c *ZeroThirteenUpgradeCommand) detectProviderSources(requiredProviders map
return diags return diags
} }
func (c *ZeroThirteenUpgradeCommand) openOrCreateFile(filename string) (*hclwrite.File, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// If the file doesn't exist, create a new empty file
if _, err := os.Stat(filename); os.IsNotExist(err) {
return hclwrite.NewEmptyFile(), diags
} else if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to read configuration file",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
return nil, diags
} else {
// File already exists, so load and parse it
config, err := ioutil.ReadFile(filename)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to read configuration file",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
return nil, diags
}
file, parseDiags := hclwrite.ParseConfig(config, filename, hcl.InitialPos)
diags = diags.Append(parseDiags)
return file, diags
}
}
func (c *ZeroThirteenUpgradeCommand) writeFile(file *hclwrite.File, filename string) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
f, err := os.OpenFile(filename, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to open configuration file for writing",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
return diags
}
_, err = file.WriteTo(f)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to rewrite configuration file",
fmt.Sprintf("Error when rewriting configuration file %q: %s", filename, err),
))
return diags
}
return diags
}
// Take a list of tokens and a separator token, and return two lists: one up to // Take a list of tokens and a separator token, and return two lists: one up to
// and including the first instance of the separator, and the rest of the // and including the first instance of the separator, and the rest of the
// tokens. If the separator is not present, return the entire list in the first // tokens. If the separator is not present, return the entire list in the first
// return value. // return value.
func partitionTokensAfter(tokens hclwrite.Tokens, separator hclsyntax.TokenType) (hclwrite.Tokens, hclwrite.Tokens) { func (c *ZeroThirteenUpgradeCommand) partitionTokensAfter(tokens hclwrite.Tokens, separator hclsyntax.TokenType) (hclwrite.Tokens, hclwrite.Tokens) {
for i := 0; i < len(tokens); i++ { for i := 0; i < len(tokens); i++ {
if tokens[i].Type == separator { if tokens[i].Type == separator {
return tokens[0 : i+1], tokens[i+1:] return tokens[0 : i+1], tokens[i+1:]
@ -578,8 +652,8 @@ func (c *ZeroThirteenUpgradeCommand) Help() string {
helpText := ` helpText := `
Usage: terraform 0.13upgrade [module-dir] Usage: terraform 0.13upgrade [module-dir]
Generates a "providers.tf" configuration file which includes source Updates module configuration files to add provider source attributes and
configuration for every non-default provider. merge multiple required_providers blocks into one.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }

View File

@ -93,7 +93,7 @@ func TestZeroThirteenUpgrade_success(t *testing.T) {
"preserves comments": "013upgrade-preserves-comments", "preserves comments": "013upgrade-preserves-comments",
"multiple blocks": "013upgrade-multiple-blocks", "multiple blocks": "013upgrade-multiple-blocks",
"multiple files": "013upgrade-multiple-files", "multiple files": "013upgrade-multiple-files",
"existing providers.tf": "013upgrade-existing-providers-tf", "existing versions.tf": "013upgrade-existing-versions-tf",
"skipped files": "013upgrade-skipped-files", "skipped files": "013upgrade-skipped-files",
} }
for name, testPath := range testCases { for name, testPath := range testCases {
@ -220,6 +220,32 @@ func TestZeroThirteenUpgrade_skippedFiles(t *testing.T) {
} }
} }
func TestZeroThirteenUpgrade_unsupportedVersion(t *testing.T) {
inputPath := testFixturePath("013upgrade-unsupported-version")
td := tempDir(t)
copy.CopyDir(inputPath, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
c := &ZeroThirteenUpgradeCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
if code := c.Run(nil); code == 0 {
t.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, `Unsupported Terraform Core version`) {
t.Fatal("missing version constraint error:", errMsg)
}
}
func TestZeroThirteenUpgrade_invalidFlags(t *testing.T) { func TestZeroThirteenUpgrade_invalidFlags(t *testing.T) {
td := tempDir(t) td := tempDir(t)
os.MkdirAll(td, 0755) os.MkdirAll(td, 0755)

View File

@ -1,3 +0,0 @@
# This is a file called providers.tf which does not originally have a
# required_providers block.
resource foo_resource a {}

View File

@ -1,7 +1,9 @@
# This is a file called providers.tf which does not originally have a # This is a file called versions.tf which does not originally have a
# required_providers block. # required_providers block.
resource foo_resource a {} resource foo_resource a {}
terraform { terraform {
required_version = ">= 0.13"
required_providers { required_providers {
bar = { bar = {
source = "hashicorp/bar" source = "hashicorp/bar"

View File

@ -0,0 +1,7 @@
# This is a file called versions.tf which does not originally have a
# required_providers block.
resource foo_resource a {}
terraform {
required_version = ">= 0.12"
}

View File

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

View File

@ -9,4 +9,5 @@ terraform {
source = "hashicorp/foo" source = "hashicorp/foo"
} }
} }
required_version = ">= 0.13"
} }

View File

@ -13,4 +13,5 @@ terraform {
# https://www.terraform.io/docs/configuration/providers.html#provider-source # https://www.terraform.io/docs/configuration/providers.html#provider-source
} }
} }
required_version = ">= 0.13"
} }

View File

@ -10,4 +10,5 @@ terraform {
source = "hashicorp/foo" source = "hashicorp/foo"
} }
} }
required_version = ">= 0.13"
} }

View File

@ -1,5 +1,2 @@
resource bar_instance a {} resource bar_instance a {}
resource baz_instance b {} resource baz_instance b {}
terraform {
required_version = "> 0.12.0"
}

View File

@ -1 +1,4 @@
terraform {
required_version = ">= 0.12"
}
resource foo_instance c {} resource foo_instance c {}

View File

@ -1,5 +1,9 @@
terraform { terraform {
required_providers { required_providers {
foo = {
source = "hashicorp/foo"
version = "0.5"
}
bar = { bar = {
source = "registry.acme.corp/acme/bar" source = "registry.acme.corp/acme/bar"
} }
@ -7,9 +11,6 @@ terraform {
source = "terraform-providers/baz" source = "terraform-providers/baz"
version = "~> 2.0.0" version = "~> 2.0.0"
} }
foo = {
source = "hashicorp/foo"
version = "0.5"
}
} }
required_version = ">= 0.13"
} }

View File

@ -1,10 +1,2 @@
resource bar_instance a {} resource bar_instance a {}
resource baz_instance b {} resource baz_instance b {}
terraform {
required_version = "> 0.12.0"
required_providers {
bar = {
source = "registry.acme.corp/acme/bar"
}
}
}

View File

@ -1,11 +1,7 @@
terraform { terraform {
required_version = ">= 0.12"
required_providers { required_providers {
baz = "~> 2.0.0" baz = "~> 2.0.0"
} }
} }
terraform {
required_providers {
foo = "0.5"
}
}
resource foo_instance c {} resource foo_instance c {}

View File

@ -0,0 +1,12 @@
terraform {
required_providers {
foo = "0.5"
}
}
terraform {
required_providers {
bar = {
source = "registry.acme.corp/acme/bar"
}
}
}

View File

@ -13,4 +13,5 @@ terraform {
version = "1.0.0" version = "1.0.0"
} }
} }
required_version = ">= 0.13"
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -4,4 +4,5 @@ terraform {
source = "hashicorp/foo" source = "hashicorp/foo"
} }
} }
required_version = ">= 0.13"
} }

View File

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

View File

@ -0,0 +1 @@
resource foo_instance a {}

View File

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