command/0.13upgrade: Remove this subcommand

We only preserve these major upgrade versions for one major version after
they are added, because our upgrade path assumes moving forward only one
major version at a time. Now that our main branch is tracking towards
Terraform 0.14, we no longer need the 0.13upgrade subcommand.

This also includes some minor adjustments to the 0.12upgrade command to
align the terminology used in the output of both commands. We usually
use the word "deprecated" to mean that something is still available but
no longer recommended, but neither of these commands is actually available
so "removed" is clearer.

We could in principle have removed even the removal notice for 0.12upgrade
here, but it's relatively little code and not a big deal to keep around
to help prompt those who might try to upgrade directly from 0.11 to 0.14.
We may still remove the historical configuration upgrade commands prior to
releasing Terraform 1.0, though.
This commit is contained in:
Martin Atkins 2020-09-28 15:57:45 -07:00
parent fc94c819e5
commit f0ccee854c
4 changed files with 17 additions and 1158 deletions

View File

@ -11,9 +11,9 @@ type ZeroTwelveUpgradeCommand struct {
func (c *ZeroTwelveUpgradeCommand) Run(args []string) int {
c.Ui.Output(fmt.Sprintf(`
The 0.12upgrade command is deprecated. You must run this command with Terraform
v0.12 to upgrade your configuration syntax before upgrading to the current
version.`))
The 0.12upgrade command has been removed. You must run this command with
Terraform v0.12 to upgrade your configuration syntax before upgrading to the
current version.`))
return 0
}
@ -21,7 +21,7 @@ func (c *ZeroTwelveUpgradeCommand) Help() string {
helpText := `
Usage: terraform 0.12upgrade
The 0.12upgrade command is deprecated. You must run this command with
The 0.12upgrade command has been removed. You must run this command with
Terraform v0.12 to upgrade your configuration syntax before upgrading to
the current version.
`

View File

@ -20,7 +20,7 @@ func TestZeroTwelveUpgrade_deprecated(t *testing.T) {
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "The 0.12upgrade command is deprecated.") {
if !strings.Contains(output, "The 0.12upgrade command has been removed.") {
t.Fatal("unexpected output:", output)
}
}

View File

@ -1,25 +1,8 @@
package command
import (
"context"
"fmt"
"io/ioutil"
"os"
"path"
"sort"
"strings"
"github.com/apparentlymart/go-versions/versions"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty"
)
// ZeroThirteenUpgradeCommand upgrades configuration files for a module
@ -28,803 +11,21 @@ type ZeroThirteenUpgradeCommand struct {
Meta
}
// Warning diagnostic detail message used for JSON and override config files
const skippedConfigurationFileWarning = "The %s configuration file %q was skipped, because %s files are assumed to be generated. The program that generated this file may need to be updated for changes to the configuration language."
func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
args = c.Meta.process(args)
var skipConfirm bool
flags := c.Meta.defaultFlagSet("0.13upgrade")
flags.BoolVar(&skipConfirm, "yes", false, "skip confirmation prompt")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
var diags tfdiags.Diagnostics
var dir string
args = flags.Args()
switch len(args) {
case 0:
dir = "."
case 1:
dir = args[0]
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Too many arguments",
"The command 0.13upgrade expects only a single argument, giving the directory containing the module to upgrade.",
))
c.showDiagnostics(diags)
return 1
}
// Check for user-supplied plugin path
var err error
if c.pluginPath, err = c.loadPluginPath(); err != nil {
c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
return 1
}
dir = c.normalizePath(dir)
// Upgrade only if some configuration is present
empty, err := configs.IsEmptyDir(dir)
if err != nil {
diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err))
return 1
}
if empty {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Not a module directory",
fmt.Sprintf("The given directory %s does not contain any Terraform configuration files.", dir),
))
c.showDiagnostics(diags)
return 1
}
// Set up the config loader and find all the config files
loader, err := c.initConfigLoader()
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
parser := loader.Parser()
primary, overrides, hclDiags := parser.ConfigDirFiles(dir)
diags = diags.Append(hclDiags)
if diags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Failed to load configuration"))
c.showDiagnostics(diags)
return 1
}
// Load and parse all primary files
files := make(map[string]*configs.File)
for _, path := range primary {
// Skip JSON configuration files, because we can't rewrite them and
// they're probably generated anyway.
if strings.HasSuffix(strings.ToLower(path), ".json") {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"JSON configuration file ignored",
fmt.Sprintf(
skippedConfigurationFileWarning,
"JSON",
path,
"JSON",
),
))
continue
}
file, fileDiags := parser.LoadConfigFile(path)
diags = diags.Append(fileDiags)
if file != nil {
files[path] = file
}
}
if diags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Failed to load configuration"))
c.showDiagnostics(diags)
return 1
}
// Explain what the command does and how to use it, and ask for confirmation.
if !skipConfirm {
c.Ui.Output(fmt.Sprintf(`
This command will update the configuration files in the given directory to use
the new provider source features from Terraform v0.13. It will also highlight
any providers for which the source cannot be detected, and advise how to
proceed.
We recommend using this command in a clean version control work tree, so that
you can easily see the proposed changes as a diff against the latest commit.
If you have uncommited changes already present, we recommend aborting this
command and dealing with them before running this command again.
`))
query := "Would you like to upgrade the module in the current directory?"
if dir != "." {
query = fmt.Sprintf("Would you like to upgrade the module in %s?", dir)
}
v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
Id: "approve",
Query: query,
Description: `Only 'yes' will be accepted to confirm.`,
})
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
if v != "yes" {
c.Ui.Info("Upgrade cancelled.")
return 0
}
c.Ui.Output(`-----------------------------------------------------------------------------`)
}
// It's not clear what the correct behaviour is for upgrading override
// files. For now, just log that we're ignoring the file.
for _, path := range overrides {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Override configuration file ignored",
fmt.Sprintf(
skippedConfigurationFileWarning,
"override",
path,
"override",
),
))
}
// 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)
rewritePaths := make(map[string]bool)
allProviderConstraints := make(map[string]getproviders.VersionConstraints)
// Step 1: copy all explicit provider requirements across
for path, file := range files {
for _, rps := range file.RequiredProviders {
rewritePaths[path] = true
for _, rp := range rps.RequiredProviders {
if previous, exist := requiredProviders[rp.Name]; exist {
diags = diags.Append(&hcl.Diagnostic{
Summary: "Duplicate required provider configuration",
Detail: fmt.Sprintf("Found duplicate required provider configuration for %q.Previously configured at %s", rp.Name, previous.DeclRange),
Severity: hcl.DiagWarning,
Context: rps.DeclRange.Ptr(),
Subject: rp.DeclRange.Ptr(),
})
} else {
// We're copying the struct here to ensure that any
// mutation does not affect the original, if we rewrite
// this file
requiredProviders[rp.Name] = &configs.RequiredProvider{
Name: rp.Name,
Source: rp.Source,
Type: rp.Type,
Requirement: rp.Requirement,
DeclRange: rp.DeclRange,
}
// Parse and store version constraints for later use when
// processing the provider redirect
constraints, err := getproviders.ParseVersionConstraints(rp.Requirement.Required.String())
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
// The errors returned by ParseVersionConstraint
// already include the section of input that was
// incorrect, so we don't need to
// include that here.
Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()),
Subject: rp.Requirement.DeclRange.Ptr(),
})
} else {
allProviderConstraints[rp.Name] = append(allProviderConstraints[rp.Name], constraints...)
}
}
}
}
}
for _, file := range files {
// Step 2: add missing provider requirements from provider blocks
for _, p := range file.ProviderConfigs {
// Skip internal providers
if p.Name == "terraform" {
continue
}
// If no explicit provider configuration exists for the
// provider configuration's local name, add one with a legacy
// provider address.
if _, exist := requiredProviders[p.Name]; !exist {
requiredProviders[p.Name] = &configs.RequiredProvider{
Name: p.Name,
}
}
// Parse and store version constraints for later use when
// processing the provider redirect
constraints, err := getproviders.ParseVersionConstraints(p.Version.Required.String())
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
// The errors returned by ParseVersionConstraint
// already include the section of input that was
// incorrect, so we don't need to
// include that here.
Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()),
Subject: p.Version.DeclRange.Ptr(),
})
} else {
allProviderConstraints[p.Name] = append(allProviderConstraints[p.Name], constraints...)
}
}
// Step 3: add missing provider requirements from resources
resources := [][]*configs.Resource{file.ManagedResources, file.DataResources}
for _, rs := range resources {
for _, r := range rs {
// Find the appropriate provider local name for this resource
var localName string
// If there's a provider config, use that to determine the
// local name. Otherwise use the implied provider local name
// based on the resource's address.
if r.ProviderConfigRef != nil {
localName = r.ProviderConfigRef.Name
} else {
localName = r.Addr().ImpliedProvider()
}
// Skip internal providers
if localName == "terraform" {
continue
}
// If no explicit provider configuration exists for this local
// name, add one with a legacy provider address.
if _, exist := requiredProviders[localName]; !exist {
requiredProviders[localName] = &configs.RequiredProvider{
Name: localName,
}
}
}
}
}
// We should now have a complete understanding of the provider requirements
// stated in the config. If there are any providers, attempt to detect
// their sources, and rewrite the config.
if len(requiredProviders) > 0 {
detectDiags := c.detectProviderSources(requiredProviders, allProviderConstraints)
diags = diags.Append(detectDiags)
if diags.HasErrors() {
c.Ui.Error("Unable to detect sources for providers")
c.showDiagnostics(diags)
return 1
}
// 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 {
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.
delete(rewritePaths, filename)
// Open or create the output file
out, openDiags := c.openOrCreateFile(filename)
diags = diags.Append(openDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
// Find all required_providers blocks, and store them alongside a map
// back to the parent terraform block.
var requiredProviderBlocks []*hclwrite.Block
parentBlocks := make(map[*hclwrite.Block]*hclwrite.Block)
root := out.Body()
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() != "terraform" {
continue
}
for _, childBlock := range rootBlock.Body().Blocks() {
if childBlock.Type() == "required_providers" {
requiredProviderBlocks = append(requiredProviderBlocks, childBlock)
parentBlocks[childBlock] = rootBlock
}
}
}
// First required provider block, and the rest found in this file.
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.
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() == "terraform" {
tfBlock = rootBlock
break
}
}
if tfBlock == nil {
tfBlock = root.AppendNewBlock("terraform", nil)
}
first = tfBlock.Body().AppendNewBlock("required_providers", nil)
}
// Find the body of the first block to prepare for rewriting it
body := first.Body()
// Build a sorted list of provider local names, for consistent ordering
var localNames []string
for localName := range requiredProviders {
localNames = append(localNames, localName)
}
sort.Strings(localNames)
// Populate the required providers block
for _, localName := range localNames {
requiredProvider := requiredProviders[localName]
var attributes = make(map[string]cty.Value)
if !requiredProvider.Type.IsZero() {
attributes["source"] = cty.StringVal(requiredProvider.Type.ForDisplay())
}
if version := requiredProvider.Requirement.Required.String(); version != "" {
attributes["version"] = cty.StringVal(version)
}
var attributesObject cty.Value
if len(attributes) > 0 {
attributesObject = cty.ObjectVal(attributes)
} else {
attributesObject = cty.EmptyObjectVal
}
// If this block already has an entry for this local name, we only
// want to replace it if it's semantically different
if existing := body.GetAttribute(localName); existing != nil {
bytes := existing.Expr().BuildTokens(nil).Bytes()
expr, _ := hclsyntax.ParseExpression(bytes, "", hcl.InitialPos)
value, _ := expr.Value(nil)
if !attributesObject.RawEquals(value) {
body.SetAttributeValue(localName, attributesObject)
}
} else {
body.SetAttributeValue(localName, attributesObject)
}
// If we don't have a source attribute, manually construct a commented
// block explaining what to do
if _, hasSource := attributes["source"]; !hasSource {
// Generate the token stream for the required provider
rp := body.GetAttribute(localName)
expr := rp.Expr().BuildTokens(nil)
// Partition the tokens into before and after the opening brace
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
// brace.
if len(before) == 1 && len(after) == 1 {
newline := &hclwrite.Token{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
}
after = append(hclwrite.Tokens{newline}, after...)
}
// Generate the comment and insert it at the start of the object
comment := noSourceDetectedComment(localName)
commentedBlock := append(before, comment...)
commentedBlock = append(commentedBlock, after...)
// Set the required provider object to this raw token stream
body.SetAttributeRaw(localName, commentedBlock)
}
}
// 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]
tfBody := tfBlock.Body()
tfBody.RemoveBlock(rpBlock)
// If the terraform block has no blocks and no attributes, it's
// basically empty (aside from comments and whitespace), so it's
// more useful to remove it than leave it in.
if len(tfBody.Blocks()) == 0 && len(tfBody.Attributes()) == 0 {
root.RemoveBlock(tfBlock)
}
}
// Write the config back to the file
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 {
// Read and parse the existing file
config, err := ioutil.ReadFile(path)
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
}
file, parseDiags := hclwrite.ParseConfig(config, filename, hcl.InitialPos)
diags = diags.Append(parseDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
// Find and remove all terraform.required_providers blocks
root := file.Body()
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() != "terraform" {
continue
}
tfBody := rootBlock.Body()
for _, childBlock := range tfBody.Blocks() {
if childBlock.Type() == "required_providers" {
rootBlock.Body().RemoveBlock(childBlock)
// If the terraform block is now empty, remove it
if len(tfBody.Blocks()) == 0 && len(tfBody.Attributes()) == 0 {
root.RemoveBlock(rootBlock)
}
}
}
}
// Write the config back to the file
writeDiags := c.writeFile(file, path)
diags = diags.Append(writeDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
}
}
c.showDiagnostics(diags)
if diags.HasErrors() {
return 1
}
if len(diags) != 0 {
c.Ui.Output(`-----------------------------------------------------------------------------`)
}
c.Ui.Output(c.Colorize().Color(`
[bold][green]Upgrade complete![reset]
Use your version control system to review the proposed changes, make any
necessary adjustments, and then commit.
`))
c.Ui.Output(fmt.Sprintf(`
The 0.13upgrade command has been removed. You must run this command with
Terraform v0.13 to upgrade your provider requirements before upgrading to the
current version.`))
return 0
}
// For providers which need a source attribute, detect the source
func (c *ZeroThirteenUpgradeCommand) detectProviderSources(requiredProviders map[string]*configs.RequiredProvider, allProviderConstraints map[string]getproviders.VersionConstraints) tfdiags.Diagnostics {
source := c.providerInstallSource()
var diags tfdiags.Diagnostics
providers:
for name, rp := range requiredProviders {
// If there's already an explicit source, skip it
if rp.Source != "" {
continue
}
// Construct a legacy provider FQN using the required provider local
// name. This ignores any auto-generated provider FQN from the load &
// parse process, because we know that without an explicit source it is
// not explicitly specified.
addr := addrs.NewLegacyProvider(name)
p, moved, err := getproviders.LookupLegacyProvider(addr, source)
if err == nil {
rp.Type = p
if !moved.IsZero() {
constraints, ok := allProviderConstraints[name]
// If there's no version constraint, always use the redirect
// target as there should be at least one version we can
// install
if !ok {
rp.Type = moved
continue providers
}
// Check that the redirect target has a version meeting our
// constraints
acceptable := versions.MeetingConstraints(constraints)
available, _, err := source.AvailableVersions(moved)
// If something goes wrong with the registry lookup here, fall
// back to the non-redirect provider
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s",
moved.ForDisplay(), err),
))
continue providers
}
// Walk backwards to consider newer versions first
for i := len(available) - 1; i >= 0; i-- {
if acceptable.Has(available[i]) {
// Success! Provider redirect target has a version
// meeting our constraints, so we can use it
rp.Type = moved
continue providers
}
}
// Find the last version available at the old location
oldAvailable, _, err := source.AvailableVersions(p)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to query available provider packages",
fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s",
p.ForDisplay(), err),
))
continue providers
}
lastAvailable := oldAvailable[len(oldAvailable)-1]
// If we fall through here, no versions at the target meet our
// version constraints, so warn the user
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Provider has moved",
fmt.Sprintf(
"Provider %q has moved to %q. No action is required to continue using %q (%s), but if you want to upgrade beyond version %s, you must also update the source.",
moved.Type, moved.ForDisplay(), p.ForDisplay(),
getproviders.VersionConstraintsString(constraints), lastAvailable),
))
}
} else {
// Setting the provider address to a zero value struct
// indicates that there is no known FQN for this provider,
// which will cause us to write an explanatory comment in the
// HCL output advising the user what to do about this.
rp.Type = addrs.Provider{}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Could not detect provider source",
fmt.Sprintf("Error looking up provider source for %q: %s", name, err),
))
}
}
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 (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:]
}
}
return tokens, nil
}
// Generate a list of tokens for a comment explaining that a provider source
// could not be detected.
func noSourceDetectedComment(name string) hclwrite.Tokens {
comment := fmt.Sprintf(`# TF-UPGRADE-TODO
#
# No source detected for this provider. You must add a source address
# in the following format:
#
# source = "your-registry.example.com/organization/%s"
#
# For more information, see the provider source documentation:
#
# https://www.terraform.io/docs/configuration/providers.html#provider-source`, name)
var tokens hclwrite.Tokens
for _, line := range strings.Split(comment, "\n") {
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}})
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenComment, Bytes: []byte(line)})
}
return tokens
}
func (c *ZeroThirteenUpgradeCommand) Help() string {
helpText := `
Usage: terraform 0.13upgrade [options] [module-dir]
Usage: terraform 0.13upgrade
Updates module configuration files to add provider source attributes and
merge multiple required_providers blocks into one.
By default, 0.13upgrade rewrites the files in the current working directory.
However, a path to a different directory can be provided. The command will
prompt for confirmation interactively unless the -yes option is given.
Options:
-yes Skip the initial introduction messages and interactive
confirmation. This can be used to run this command in
batch from a script.
The 0.13upgrade command has been removed. You must run this command with
Terraform v0.13 to upgrade your provider requirements before upgrading to
the current version.
`
return strings.TrimSpace(helpText)
}

View File

@ -1,368 +1,26 @@
package command
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/helper/copy"
"github.com/mitchellh/cli"
)
func verifyExpectedFiles(t *testing.T, expectedPath string) {
// Compare output and expected file trees
var outputFiles, expectedFiles []string
// Gather list of output files in the current working directory
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
outputFiles = append(outputFiles, path)
}
return nil
})
if err != nil {
t.Fatal("error listing output files:", err)
}
// Gather list of expected files
revertChdir := testChdir(t, expectedPath)
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
expectedFiles = append(expectedFiles, path)
}
return nil
})
if err != nil {
t.Fatal("error listing expected files:", err)
}
revertChdir()
// If the file trees don't match, give up early
if diff := cmp.Diff(expectedFiles, outputFiles); diff != "" {
t.Fatalf("expected and output file trees do not match\n%s", diff)
}
// Check that the contents of each file is correct
for _, filePath := range outputFiles {
output, err := ioutil.ReadFile(path.Join(".", filePath))
if err != nil {
t.Fatalf("failed to read output %s: %s", filePath, err)
}
expected, err := ioutil.ReadFile(path.Join(expectedPath, filePath))
if err != nil {
t.Fatalf("failed to read expected %s: %s", filePath, err)
}
if diff := cmp.Diff(string(expected), string(output)); diff != "" {
t.Fatalf("expected and output file for %s do not match\n%s", filePath, diff)
}
}
}
func TestZeroThirteenUpgrade_success(t *testing.T) {
registrySource, close := testRegistrySource(t)
defer close()
testCases := map[string]string{
"implicit": "013upgrade-implicit-providers",
"explicit": "013upgrade-explicit-providers",
"provider not found": "013upgrade-provider-not-found",
"implicit not found": "013upgrade-implicit-not-found",
"file exists": "013upgrade-file-exists",
"no providers": "013upgrade-no-providers",
"submodule": "013upgrade-submodule",
"providers with source": "013upgrade-providers-with-source",
"preserves comments": "013upgrade-preserves-comments",
"multiple blocks": "013upgrade-multiple-blocks",
"multiple files": "013upgrade-multiple-files",
"existing versions.tf": "013upgrade-existing-versions-tf",
"skipped files": "013upgrade-skipped-files",
"provider redirect": "013upgrade-provider-redirect",
"version unavailable": "013upgrade-provider-redirect-version-unavailable",
}
for name, testPath := range testCases {
t.Run(name, func(t *testing.T) {
inputPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "input")))
if err != nil {
t.Fatalf("failed to find input path %s: %s", testPath, err)
}
expectedPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "expected")))
if err != nil {
t.Fatalf("failed to find expected path %s: %s", testPath, err)
}
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()),
ProviderSource: registrySource,
Ui: ui,
},
}
if code := c.Run([]string{"-yes"}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "Upgrade complete") {
t.Fatal("unexpected output:", output)
}
verifyExpectedFiles(t, expectedPath)
})
}
}
// Ensure that non-default upgrade paths are supported, and that the output is
// in the correct place. This test is very similar to the table tests above,
// but with a different expected output path, and with an argument passed to
// the Run call.
func TestZeroThirteenUpgrade_submodule(t *testing.T) {
registrySource, close := testRegistrySource(t)
defer close()
testPath := "013upgrade-submodule"
inputPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "input")))
if err != nil {
t.Fatalf("failed to find input path %s: %s", testPath, err)
}
// The expected output for processing a submodule is different
expectedPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "expected-module")))
if err != nil {
t.Fatalf("failed to find expected path %s: %s", testPath, err)
}
td := tempDir(t)
copy.CopyDir(inputPath, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
func TestZeroThirteenUpgrade(t *testing.T) {
ui := new(cli.MockUi)
c := &ZeroThirteenUpgradeCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
ProviderSource: registrySource,
Ui: ui,
Ui: ui,
},
}
// Here we pass a target module directory to process
if code := c.Run([]string{"-yes", "module"}); code != 0 {
if code := c.Run([]string{}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "Upgrade complete") {
t.Fatal("unexpected output:", output)
}
verifyExpectedFiles(t, expectedPath)
}
// Verify that JSON and override files are skipped with a warning. Generated
// output for this config is verified in the table driven tests above.
func TestZeroThirteenUpgrade_skippedFiles(t *testing.T) {
inputPath := testFixturePath(path.Join("013upgrade-skipped-files", "input"))
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([]string{"-yes"}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "Upgrade complete") {
t.Fatal("unexpected output:", output)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, `The JSON configuration file "variables.tf.json" was skipped`) {
t.Fatal("missing JSON skipped file warning:", errMsg)
}
if !strings.Contains(errMsg, `The override configuration file "bar_override.tf" was skipped`) {
t.Fatal("missing override skipped file warning:", errMsg)
}
}
func TestZeroThirteenUpgrade_confirm(t *testing.T) {
inputPath := testFixturePath(path.Join("013upgrade-explicit-providers", "input"))
td := tempDir(t)
copy.CopyDir(inputPath, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Ask input
defer testInteractiveInput(t, []string{"yes"})()
ui := new(cli.MockUi)
c := &ZeroThirteenUpgradeCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
if code := c.Run(nil); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "Upgrade complete") {
if !strings.Contains(output, "The 0.13upgrade command has been removed.") {
t.Fatal("unexpected output:", output)
}
}
func TestZeroThirteenUpgrade_cancel(t *testing.T) {
inputPath := testFixturePath(path.Join("013upgrade-explicit-providers", "input"))
td := tempDir(t)
copy.CopyDir(inputPath, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// Ask input
defer testInteractiveInput(t, []string{"no"})()
ui := new(cli.MockUi)
c := &ZeroThirteenUpgradeCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
if code := c.Run(nil); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "Upgrade cancelled") {
t.Fatal("unexpected output:", output)
}
if strings.Contains(output, "Upgrade complete") {
t.Fatal("unexpected output:", output)
}
}
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([]string{"-yes"}); 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)
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([]string{"--whoops"}); code == 0 {
t.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Usage: terraform 0.13upgrade") {
t.Fatal("unexpected error:", errMsg)
}
}
func TestZeroThirteenUpgrade_tooManyArguments(t *testing.T) {
td := tempDir(t)
os.MkdirAll(td, 0755)
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([]string{".", "./modules/test"}); code == 0 {
t.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Error: Too many arguments") {
t.Fatal("unexpected error:", errMsg)
}
}
func TestZeroThirteenUpgrade_empty(t *testing.T) {
td := tempDir(t)
os.MkdirAll(td, 0755)
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([]string{"-yes"}); code == 0 {
t.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Not a module directory") {
t.Fatal("unexpected error:", errMsg)
}
}