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/internal/getproviders"
"github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
"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
requiredProviders := make(map[string]*configs.RequiredProvider)
var rewritePaths []string
rewritePaths := make(map[string]bool)
// Step 1: copy all explicit provider requirements across
for path, file := range files {
for _, rps := range file.RequiredProviders {
rewritePaths = append(rewritePaths, path)
rewritePaths[path] = true
for _, rp := range rps.RequiredProviders {
if previous, exist := requiredProviders[rp.Name]; exist {
diags = diags.Append(&hcl.Diagnostic{
@ -225,54 +247,27 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
return 1
}
// Default output filename is "providers.tf"
filename := path.Join(dir, "providers.tf")
// Default output filename is "versions.tf", which is also where the
// 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
// block, output to that file instead.
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
// later. Otherwise we'd delete the required providers block after
// writing it.
for i, path := range rewritePaths {
if path == filename {
rewritePaths = append(rewritePaths[:i], rewritePaths[i+1:]...)
break
}
}
delete(rewritePaths, filename)
var out *hclwrite.File
// If the output file doesn't exist, just create a new empty file
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)
}
// Open or create the output file
out, openDiags := c.openOrCreateFile(filename)
diags = diags.Append(openDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
@ -300,14 +295,22 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
var first *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 we already have one or more required provider blocks, we'll rewrite
// the first one, and remove the rest.
first, rest = requiredProviderBlocks[0], requiredProviderBlocks[1:]
// Set the terraform block here for later use to update the
// required version constraint.
tfBlock = parentBlocks[first]
} else {
// Otherwise, find or a create a terraform block, and add a new
// empty required providers block to it.
var tfBlock *hclwrite.Block
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() == "terraform" {
tfBlock = rootBlock
@ -359,7 +362,7 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
expr := rp.Expr().BuildTokens(nil)
// 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
// 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)
for _, rpBlock := range rest {
tfBlock := parentBlocks[rpBlock]
@ -397,30 +408,53 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
}
// Write the config back to the file
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),
))
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),
))
writeDiags := c.writeFile(out, filename)
diags = diags.Append(writeDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
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
// required provider blocks from remaining configuration files.
for _, path := range rewritePaths {
for path := range rewritePaths {
// Read and parse the existing file
config, err := ioutil.ReadFile(path)
if err != nil {
@ -459,23 +493,9 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
}
// Write the config back to the file
f, err := os.OpenFile(path, 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),
))
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),
))
writeDiags := c.writeFile(file, path)
diags = diags.Append(writeDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
@ -538,11 +558,65 @@ func (c *ZeroThirteenUpgradeCommand) detectProviderSources(requiredProviders map
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
// 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
// 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++ {
if tokens[i].Type == separator {
return tokens[0 : i+1], tokens[i+1:]
@ -578,8 +652,8 @@ func (c *ZeroThirteenUpgradeCommand) Help() string {
helpText := `
Usage: terraform 0.13upgrade [module-dir]
Generates a "providers.tf" configuration file which includes source
configuration for every non-default provider.
Updates module configuration files to add provider source attributes and
merge multiple required_providers blocks into one.
`
return strings.TrimSpace(helpText)
}

View File

@ -93,7 +93,7 @@ func TestZeroThirteenUpgrade_success(t *testing.T) {
"preserves comments": "013upgrade-preserves-comments",
"multiple blocks": "013upgrade-multiple-blocks",
"multiple files": "013upgrade-multiple-files",
"existing providers.tf": "013upgrade-existing-providers-tf",
"existing versions.tf": "013upgrade-existing-versions-tf",
"skipped files": "013upgrade-skipped-files",
}
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) {
td := tempDir(t)
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.
resource foo_resource a {}
terraform {
required_version = ">= 0.13"
required_providers {
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"
}
}
required_version = ">= 0.13"
}

View File

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

View File

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

View File

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

View File

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

View File

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