command: remove 0.12upgrade (#24403)

* command: remove 0.12upgrade and related `configupgrade` library
* leave deprecation warning for 0.12upgrade to point users to v0.12
This commit is contained in:
Kristin Laemmert 2020-03-19 08:01:16 -04:00 committed by GitHub
parent 109c4bf6ef
commit 5f313a65ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
138 changed files with 104 additions and 5670 deletions

View File

@ -1,256 +1,15 @@
package command package command
import ( import "fmt"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs/configupgrade"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
)
// ZeroTwelveUpgradeCommand is a Command implementation that can upgrade
// the configuration files for a module from pre-0.11 syntax to new 0.12
// idiom, while also flagging any suspicious constructs that will require
// human review.
type ZeroTwelveUpgradeCommand struct { type ZeroTwelveUpgradeCommand struct {
Meta Meta
} }
func (c *ZeroTwelveUpgradeCommand) Run(args []string) int { func (c *ZeroTwelveUpgradeCommand) Run(args []string) int {
args, err := c.Meta.process(args, true)
if err != nil {
return 1
}
var skipConfirm, force bool
flags := c.Meta.extendedFlagSet("0.12upgrade")
flags.BoolVar(&skipConfirm, "yes", false, "skip confirmation prompt")
flags.BoolVar(&force, "force", false, "override duplicate upgrade heuristic")
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.12upgrade expects only a single argument, giving the directory containing the module to upgrade.",
))
c.showDiagnostics(diags)
return 1
}
// Check for user-supplied plugin path
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)
sources, err := configupgrade.LoadModule(dir)
if err != nil {
if os.IsNotExist(err) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Module directory not found",
fmt.Sprintf("The given directory %s does not exist.", dir),
))
} else {
diags = diags.Append(err)
}
c.showDiagnostics(diags)
return 1
}
if len(sources) == 0 {
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
}
// The config loader doesn't naturally populate our sources
// map, so we'll do it manually so our diagnostics can have
// source code snippets inside them.
// This is weird, but this whole upgrade codepath is pretty
// weird and temporary, so we'll accept it.
if loader, err := c.initConfigLoader(); err == nil {
parser := loader.Parser()
for name, src := range sources {
parser.ForceFileSource(filepath.Join(dir, name), src)
}
}
if !force {
// We'll check first if this directory already looks upgraded, so we
// don't waste the user's time dealing with an interactive prompt
// immediately followed by an error.
if already, rng := sources.MaybeAlreadyUpgraded(); already {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Module already upgraded",
Detail: fmt.Sprintf("The module in directory %s has a version constraint that suggests it has already been upgraded for v0.12. If this is incorrect, either remove this constraint or override this heuristic with the -force argument. Upgrading a module that was already upgraded may change the meaning of that module.", dir),
Subject: rng.ToHCL().Ptr(),
})
c.showDiagnostics(diags)
return 1
}
}
if !skipConfirm {
c.Ui.Output(fmt.Sprintf(` c.Ui.Output(fmt.Sprintf(`
This command will rewrite the configuration files in the given directory so The 0.12upgrade command is deprecated. You must run this command with Terraform
that they use the new syntax features from Terraform v0.12, and will identify v0.12 to upgrade your configuration syntax before upgrading to the current
any constructs that may need to be adjusted for correct operation with version.`))
Terraform v0.12.
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 return 0
} }
c.Ui.Output(`-----------------------------------------------------------------------------`)
}
upgrader := &configupgrade.Upgrader{
Providers: c.providerResolver(),
Provisioners: c.provisionerFactories(),
}
newSources, upgradeDiags := upgrader.Upgrade(sources, dir)
diags = diags.Append(upgradeDiags)
if upgradeDiags.HasErrors() {
c.showDiagnostics(diags)
return 2
}
// Now we'll write the contents of newSources into the filesystem.
for name, src := range newSources {
fn := filepath.Join(dir, name)
if src == nil {
// indicates a file to be deleted
err := os.Remove(fn)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to remove file",
fmt.Sprintf("The file %s must be renamed as part of the upgrade process, but the old file could not be deleted: %s.", fn, err),
))
}
continue
}
err := ioutil.WriteFile(fn, src, 0644)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to write file",
fmt.Sprintf("The file %s must be updated or created as part of the upgrade process, but there was an error while writing: %s.", fn, err),
))
}
}
c.showDiagnostics(diags)
if diags.HasErrors() {
return 2
}
if !skipConfirm {
if len(diags) != 0 {
c.Ui.Output(`-----------------------------------------------------------------------------`)
}
c.Ui.Output(c.Colorize().Color(`
[bold][green]Upgrade complete![reset]
The configuration files were upgraded successfully. Use your version control
system to review the proposed changes, make any necessary adjustments, and
then commit.
`))
if len(diags) != 0 {
// We checked for errors above, so these must be warnings.
c.Ui.Output(`Some warnings were generated during the upgrade, as shown above. These
indicate situations where Terraform could not decide on an appropriate course
of action without further human input.
Where possible, these have also been marked with TF-UPGRADE-TODO comments to
mark the locations where a decision must be made. After reviewing and adjusting
these, manually remove the TF-UPGRADE-TODO comment before continuing.
`)
}
}
return 0
}
func (c *ZeroTwelveUpgradeCommand) Help() string {
helpText := `
Usage: terraform 0.12upgrade [module-dir]
Rewrites the .tf files for a single module that was written for a Terraform
version prior to v0.12 so that it uses new syntax features from v0.12
and later.
Also rewrites constructs that behave differently after v0.12, and flags any
suspicious constructs that require human review,
By default, 0.12upgrade 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.
-force Override the heuristic that attempts to detect if a
configuration is already written for v0.12 or later.
Some of the transformations made by this command are
not idempotent, so re-running against the same module
may change the meanings expressions in the module.
`
return strings.TrimSpace(helpText)
}
func (c *ZeroTwelveUpgradeCommand) Synopsis() string {
return "Rewrites pre-0.12 module source code for v0.12"
}

View File

@ -0,0 +1,26 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestZeroTwelveUpgrade_deprecated(t *testing.T) {
ui := new(cli.MockUi)
c := &ZeroTwelveUpgradeCommand{
Meta: Meta{
Ui: ui,
},
}
if code := c.Run([]string{}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "The 0.12upgrade command is deprecated.") {
t.Fatal("unexpected output:", output)
}
}

View File

@ -18,7 +18,6 @@ import (
backendInit "github.com/hashicorp/terraform/backend/init" backendInit "github.com/hashicorp/terraform/backend/init"
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/configs/configupgrade"
"github.com/hashicorp/terraform/internal/earlyconfig" "github.com/hashicorp/terraform/internal/earlyconfig"
"github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/plugin/discovery"
@ -171,18 +170,9 @@ func (c *InitCommand) Run(args []string) int {
// Before we do anything else, we'll try loading configuration with both // Before we do anything else, we'll try loading configuration with both
// our "normal" and "early" configuration codepaths. If early succeeds // our "normal" and "early" configuration codepaths. If early succeeds
// while normal fails, that strongly suggests that the configuration is // while normal fails, that strongly suggests that the configuration is
// using syntax that worked in 0.11 but no longer in 0.12, which requires // using syntax that worked in 0.11 but no longer in v0.12.
// some special behavior here to get the directory initialized just enough
// to run "terraform 0.12upgrade".
//
// FIXME: Once we reach 0.13 and remove 0.12upgrade, we should rework this
// so that we first use the early config to do a general compatibility
// check with dependencies, producing version-oriented error messages if
// dependencies aren't right, and only then use the real loader to deal
// with the backend configuration.
rootMod, confDiags := c.loadSingleModule(path) rootMod, confDiags := c.loadSingleModule(path)
rootModEarly, earlyConfDiags := c.loadSingleModuleEarly(path) rootModEarly, earlyConfDiags := c.loadSingleModuleEarly(path)
configUpgradeProbablyNeeded := false
if confDiags.HasErrors() { if confDiags.HasErrors() {
if earlyConfDiags.HasErrors() { if earlyConfDiags.HasErrors() {
// If both parsers produced errors then we'll assume the config // If both parsers produced errors then we'll assume the config
@ -194,33 +184,25 @@ func (c *InitCommand) Run(args []string) int {
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
// If _only_ the main loader produced errors then that suggests the
// If _only_ the main loader produced errors then that suggests an // configuration is written in 0.11-style syntax. We will return an
// upgrade may help. To give us more certainty here, we'll use the // error suggesting the user upgrade their config manually or with
// same heuristic that "terraform 0.12upgrade" uses to guess if a // Terraform v0.12
// configuration has already been upgraded, to reduce the risk that c.Ui.Error(strings.TrimSpace(errInitConfigErrorMaybeLegacySyntax))
// we'll produce a misleading message if the problem is just a regular c.showDiagnostics(earlyConfDiags)
// syntax error that the early loader just didn't catch.
sources, err := configupgrade.LoadModule(path)
if err == nil {
if already, _ := sources.MaybeAlreadyUpgraded(); already {
// Just report the errors as normal, then.
c.Ui.Error(strings.TrimSpace(errInitConfigError))
diags = diags.Append(confDiags)
c.showDiagnostics(diags)
return 1 return 1
} }
}
configUpgradeProbablyNeeded = true
}
if earlyConfDiags.HasErrors() {
// If _only_ the early loader encountered errors then that's unusual // If _only_ the early loader encountered errors then that's unusual
// (it should generally be a superset of the normal loader) but we'll // (it should generally be a superset of the normal loader) but we'll
// return those errors anyway since otherwise we'll probably get // return those errors anyway since otherwise we'll probably get
// some weird behavior downstream. Errors from the early loader are // some weird behavior downstream. Errors from the early loader are
// generally not as high-quality since it has less context to work with. // generally not as high-quality since it has less context to work with.
if earlyConfDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace(errInitConfigError)) c.Ui.Error(strings.TrimSpace(errInitConfigError))
diags = diags.Append(earlyConfDiags) // Errors from the early loader are generally not as high-quality since
// it has less context to work with.
diags = diags.Append(confDiags)
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
@ -237,21 +219,15 @@ func (c *InitCommand) Run(args []string) int {
} }
} }
// With all of the modules (hopefully) installed, we can now try to load // With all of the modules (hopefully) installed, we can now try to load the
// the whole configuration tree. // whole configuration tree.
// //
// Just as above, we'll try loading both with the early and normal config // Just as above, we'll try loading both with the early and normal config
// loaders here. Subsequent work will only use the early config, but // loaders here. Subsequent work will only use the early config, but loading
// loading both gives us an opportunity to prefer the better error messages // both gives us an opportunity to prefer the better error messages from the
// from the normal loader if both fail. // normal loader if both fail.
_, confDiags = c.loadConfig(path)
earlyConfig, earlyConfDiags := c.loadConfigEarly(path) earlyConfig, earlyConfDiags := c.loadConfigEarly(path)
if confDiags.HasErrors() && !configUpgradeProbablyNeeded {
c.Ui.Error(strings.TrimSpace(errInitConfigError))
diags = diags.Append(confDiags)
c.showDiagnostics(diags)
return 1
}
if earlyConfDiags.HasErrors() { if earlyConfDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace(errInitConfigError)) c.Ui.Error(strings.TrimSpace(errInitConfigError))
diags = diags.Append(earlyConfDiags) diags = diags.Append(earlyConfDiags)
@ -259,32 +235,28 @@ func (c *InitCommand) Run(args []string) int {
return 1 return 1
} }
{ _, confDiags = c.loadConfig(path)
// Before we go further, we'll check to make sure none of the modules if confDiags.HasErrors() {
// in the configuration declare that they don't support this Terraform c.Ui.Error(strings.TrimSpace(errInitConfigError))
// version, so we can produce a version-related error message rather diags = diags.Append(confDiags)
// than potentially-confusing downstream errors. c.showDiagnostics(diags)
return 1
}
// Before we go further, we'll check to make sure none of the modules in the
// configuration declare that they don't support this Terraform version, so
// we can produce a version-related error message rather than
// potentially-confusing downstream errors.
versionDiags := initwd.CheckCoreVersionRequirements(earlyConfig) versionDiags := initwd.CheckCoreVersionRequirements(earlyConfig)
diags = diags.Append(versionDiags) diags = diags.Append(versionDiags)
if versionDiags.HasErrors() { if versionDiags.HasErrors() {
c.showDiagnostics(diags) c.showDiagnostics(diags)
return 1 return 1
} }
}
var back backend.Backend var back backend.Backend
if flagBackend { if flagBackend {
switch {
case configUpgradeProbablyNeeded:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Skipping backend initialization pending configuration upgrade",
// The "below" in this message is referring to the special
// note about running "terraform 0.12upgrade" that we'll
// print out at the end when configUpgradeProbablyNeeded is set.
"The root module configuration contains errors that may be fixed by running the configuration upgrade tool, so Terraform is skipping backend initialization. See below for more information.",
))
default:
be, backendOutput, backendDiags := c.initBackend(rootMod, flagConfigExtra) be, backendOutput, backendDiags := c.initBackend(rootMod, flagConfigExtra)
diags = diags.Append(backendDiags) diags = diags.Append(backendDiags)
if backendDiags.HasErrors() { if backendDiags.HasErrors() {
@ -295,7 +267,6 @@ func (c *InitCommand) Run(args []string) int {
header = true header = true
} }
back = be back = be
}
} else { } else {
// load the previously-stored backend config // load the previously-stored backend config
be, backendDiags := c.Meta.backendFromState() be, backendDiags := c.Meta.backendFromState()
@ -365,16 +336,6 @@ func (c *InitCommand) Run(args []string) int {
// by errors then we'll output them here so that the success message is // by errors then we'll output them here so that the success message is
// still the final thing shown. // still the final thing shown.
c.showDiagnostics(diags) c.showDiagnostics(diags)
if configUpgradeProbablyNeeded {
switch {
case c.RunningInAutomation:
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessConfigUpgrade)))
default:
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessConfigUpgradeCLI)))
}
return 0
}
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess))) c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess)))
if !c.RunningInAutomation { if !c.RunningInAutomation {
// If we're not running in an automation wrapper, give the user // If we're not running in an automation wrapper, give the user
@ -382,7 +343,6 @@ func (c *InitCommand) Run(args []string) int {
// shell usage. // shell usage.
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessCLI))) c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessCLI)))
} }
return 0 return 0
} }
@ -853,6 +813,19 @@ The Terraform configuration must be valid before initialization so that
Terraform can determine which modules and providers need to be installed. Terraform can determine which modules and providers need to be installed.
` `
const errInitConfigErrorMaybeLegacySyntax = `
There are some problems with the configuration, described below.
Terraform found syntax errors in the configuration that prevented full
initialization. If you've recently upgraded to Terraform v0.13 from Terraform
v0.11, this may be because your configuration uses syntax constructs that are no
longer valid, and so must be updated before full initialization is possible.
Manually update your configuration syntax, or install Terraform v0.12 and run
terraform init for this configuration at a shell prompt for more information
on how to update it for Terraform v0.12+ compatibility.
`
const errInitCopyNotEmpty = ` const errInitCopyNotEmpty = `
The working directory already contains files. The -from-module option requires The working directory already contains files. The -from-module option requires
an empty directory into which a copy of the referenced module will be placed. an empty directory into which a copy of the referenced module will be placed.
@ -882,34 +855,6 @@ rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary. commands will detect it and remind you to do so if necessary.
` `
const outputInitSuccessConfigUpgrade = `
[reset][bold]Terraform has initialized, but configuration upgrades may be needed.[reset]
Terraform found syntax errors in the configuration that prevented full
initialization. If you've recently upgraded to Terraform v0.12, this may be
because your configuration uses syntax constructs that are no longer valid,
and so must be updated before full initialization is possible.
Run terraform init for this configuration at a shell prompt for more information
on how to update it for Terraform v0.12 compatibility.
`
const outputInitSuccessConfigUpgradeCLI = `[reset][green]
[reset][bold]Terraform has initialized, but configuration upgrades may be needed.[reset]
Terraform found syntax errors in the configuration that prevented full
initialization. If you've recently upgraded to Terraform v0.12, this may be
because your configuration uses syntax constructs that are no longer valid,
and so must be updated before full initialization is possible.
Terraform has installed the required providers to support the configuration
upgrade process. To begin upgrading your configuration, run the following:
terraform 0.12upgrade
To see the full set of errors that led to this message, run:
terraform validate
`
const outputInitProvidersUnconstrained = ` const outputInitProvidersUnconstrained = `
The following providers do not have any version constraints in configuration, The following providers do not have any version constraints in configuration,
so the latest version was installed. so the latest version was installed.

View File

@ -1424,86 +1424,14 @@ func TestInit_pluginWithInternal(t *testing.T) {
} }
} }
func TestInit_012UpgradeNeeded(t *testing.T) { // The module in this test uses terraform 0.11-style syntax. We expect that the
td := tempDir(t) // earlyconfig will succeed but the main loader fail, and return an error that
copy.CopyDir(testFixturePath("init-012upgrade"), td) // indicates that syntax upgrades may be required.
defer os.RemoveAll(td) func TestInit_syntaxErrorUpgradeHint(t *testing.T) {
defer testChdir(t, td)()
ui := cli.NewMockUi()
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
}
installer := &mockProviderInstaller{
Providers: map[string][]string{
"null": []string{"1.0.0"},
},
Dir: m.pluginDir(),
}
c := &InitCommand{
Meta: m,
providerInstaller: installer,
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Errorf("wrong exit status %d; want 0\nerror output:\n%s", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "terraform 0.12upgrade") {
t.Errorf("doesn't look like we detected the need for config upgrade:\n%s", output)
}
}
func TestInit_012UpgradeNeededInAutomation(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("init-012upgrade"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
ui := cli.NewMockUi()
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
RunningInAutomation: true,
}
installer := &mockProviderInstaller{
Providers: map[string][]string{
"null": []string{"1.0.0"},
},
Dir: m.pluginDir(),
}
c := &InitCommand{
Meta: m,
providerInstaller: installer,
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Errorf("wrong exit status %d; want 0\nerror output:\n%s", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "Run terraform init for this configuration at a shell prompt") {
t.Errorf("doesn't look like we instructed to run Terraform locally:\n%s", output)
}
if strings.Contains(output, "terraform 0.12upgrade") {
// We don't prompt with an exact command in automation mode, since
// the upgrade process is interactive and so it cannot be run in
// automation.
t.Errorf("looks like we incorrectly gave an upgrade command to run:\n%s", output)
}
}
func TestInit_syntaxErrorVersionSniff(t *testing.T) {
// Create a temporary working directory that is empty // Create a temporary working directory that is empty
td := tempDir(t) td := tempDir(t)
// This module
copy.CopyDir(testFixturePath("init-sniff-version-error"), td) copy.CopyDir(testFixturePath("init-sniff-version-error"), td)
defer os.RemoveAll(td) defer os.RemoveAll(td)
defer testChdir(t, td)() defer testChdir(t, td)()
@ -1517,16 +1445,13 @@ func TestInit_syntaxErrorVersionSniff(t *testing.T) {
} }
args := []string{} args := []string{}
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
// Check output. // Check output.
// Currently, this lands in the "upgrade may be needed" codepath, because output := ui.ErrorWriter.String()
// the intentional syntax error in our test fixture is something that if got, want := output, "If you've recently upgraded to Terraform v0.13 from Terraform\nv0.11, this may be because your configuration uses syntax constructs that are no\nlonger valid"; !strings.Contains(got, want) {
// "terraform 0.12upgrade" could fix.
output := ui.OutputWriter.String()
if got, want := output, "Terraform has initialized, but configuration upgrades may be needed"; !strings.Contains(got, want) {
t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want) t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want)
} }
} }

View File

@ -1,10 +0,0 @@
resource "null_resource" "foo" {
# This construct trips up the HCL2 parser because it looks like a nested block
# but has quoted keys like a map. The upgrade tool would add an equals sign
# here to turn this into a map attribute, but "terraform init" must first
# be able to install the null provider so the upgrade tool can know that
# "triggers" is a map attribute.
triggers {
"foo" = "bar"
}
}

View File

@ -308,7 +308,7 @@ func initCommands(config *cliconfig.Config, services *disco.Disco, providerSrc g
//----------------------------------------------------------- //-----------------------------------------------------------
"0.12upgrade": func() (cli.Command, error) { "0.12upgrade": func() (cli.Command, error) {
return &command.ZeroTwelveUpgradeCommand{ return &command.ZeroThirteenUpgradeCommand{
Meta: meta, Meta: meta,
}, nil }, nil
}, },

View File

@ -1,290 +0,0 @@
package configupgrade
import (
"fmt"
"log"
"strings"
hcl1 "github.com/hashicorp/hcl"
hcl1ast "github.com/hashicorp/hcl/hcl/ast"
hcl1parser "github.com/hashicorp/hcl/hcl/parser"
hcl1token "github.com/hashicorp/hcl/hcl/token"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/moduledeps"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/terraform"
)
// analysis is a container for the various different information gathered
// by Upgrader.analyze.
type analysis struct {
ProviderSchemas map[string]*terraform.ProviderSchema
ProvisionerSchemas map[string]*configschema.Block
ResourceProviderType map[addrs.Resource]string
ResourceHasCount map[addrs.Resource]bool
VariableTypes map[string]string
ModuleDir string
}
// analyze processes the configuration files included inside the receiver
// and returns an assortment of information required to make decisions during
// a configuration upgrade.
func (u *Upgrader) analyze(ms ModuleSources) (*analysis, error) {
ret := &analysis{
ProviderSchemas: make(map[string]*terraform.ProviderSchema),
ProvisionerSchemas: make(map[string]*configschema.Block),
ResourceProviderType: make(map[addrs.Resource]string),
ResourceHasCount: make(map[addrs.Resource]bool),
VariableTypes: make(map[string]string),
}
m := &moduledeps.Module{
Providers: make(moduledeps.Providers),
}
// This is heavily based on terraform.ModuleTreeDependencies but
// differs in that it works directly with the HCL1 AST rather than
// the legacy config structs (and can thus outlive those) and that
// it only works on one module at a time, and so doesn't need to
// recurse into child calls.
for name, src := range ms {
if ext := fileExt(name); ext != ".tf" {
continue
}
log.Printf("[TRACE] configupgrade: Analyzing %q", name)
f, err := hcl1parser.Parse(src)
if err != nil {
// If we encounter a syntax error then we'll just skip for now
// and assume that we'll catch this again when we do the upgrade.
// If not, we'll break the upgrade step of renaming .tf files to
// .tf.json if they seem to be JSON syntax.
log.Printf("[ERROR] Failed to parse %q: %s", name, err)
continue
}
list, ok := f.Node.(*hcl1ast.ObjectList)
if !ok {
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
}
if providersList := list.Filter("provider"); len(providersList.Items) > 0 {
providerObjs := providersList.Children()
for _, providerObj := range providerObjs.Items {
if len(providerObj.Keys) != 1 {
return nil, fmt.Errorf("provider block has wrong number of labels")
}
name := providerObj.Keys[0].Token.Value().(string)
var listVal *hcl1ast.ObjectList
if ot, ok := providerObj.Val.(*hcl1ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("provider %q: must be a block", name)
}
var versionStr string
if a := listVal.Filter("version"); len(a.Items) > 0 {
err := hcl1.DecodeObject(&versionStr, a.Items[0].Val)
if err != nil {
return nil, fmt.Errorf("Error reading version for provider %q: %s", name, err)
}
}
var constraints discovery.Constraints
if versionStr != "" {
constraints, err = discovery.ConstraintStr(versionStr).Parse()
if err != nil {
return nil, fmt.Errorf("Error parsing version for provider %q: %s", name, err)
}
}
var alias string
if a := listVal.Filter("alias"); len(a.Items) > 0 {
err := hcl1.DecodeObject(&alias, a.Items[0].Val)
if err != nil {
return nil, fmt.Errorf("Error reading alias for provider %q: %s", name, err)
}
}
fqn := addrs.NewLegacyProvider(name)
log.Printf("[TRACE] Provider block requires provider %q", fqn.LegacyString())
m.Providers[fqn] = moduledeps.ProviderDependency{
Constraints: constraints,
Reason: moduledeps.ProviderDependencyExplicit,
}
}
}
{
resourceConfigsList := list.Filter("resource")
dataResourceConfigsList := list.Filter("data")
// list.Filter annoyingly strips off the key used for matching,
// so we'll put it back here so we can distinguish our two types
// of blocks below.
for _, obj := range resourceConfigsList.Items {
obj.Keys = append([]*hcl1ast.ObjectKey{
{Token: hcl1token.Token{Type: hcl1token.IDENT, Text: "resource"}},
}, obj.Keys...)
}
for _, obj := range dataResourceConfigsList.Items {
obj.Keys = append([]*hcl1ast.ObjectKey{
{Token: hcl1token.Token{Type: hcl1token.IDENT, Text: "data"}},
}, obj.Keys...)
}
// Now we can merge the two lists together, since we can distinguish
// them just by their keys[0].
resourceConfigsList.Items = append(resourceConfigsList.Items, dataResourceConfigsList.Items...)
resourceObjs := resourceConfigsList.Children()
for _, resourceObj := range resourceObjs.Items {
if len(resourceObj.Keys) != 3 {
return nil, fmt.Errorf("resource or data block has wrong number of labels")
}
typeName := resourceObj.Keys[1].Token.Value().(string)
name := resourceObj.Keys[2].Token.Value().(string)
rAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: typeName,
Name: name,
}
if resourceObj.Keys[0].Token.Value() == "data" {
rAddr.Mode = addrs.DataResourceMode
}
var listVal *hcl1ast.ObjectList
if ot, ok := resourceObj.Val.(*hcl1ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("config for %q must be a block", rAddr)
}
if o := listVal.Filter("count"); len(o.Items) > 0 {
ret.ResourceHasCount[rAddr] = true
} else {
ret.ResourceHasCount[rAddr] = false
}
var providerKey string
if o := listVal.Filter("provider"); len(o.Items) > 0 {
err := hcl1.DecodeObject(&providerKey, o.Items[0].Val)
if err != nil {
return nil, fmt.Errorf("Error reading provider for resource %s: %s", rAddr, err)
}
}
var fqn addrs.Provider
if providerKey == "" {
// we are assuming a default, legacy provider for 012
// configurations
fqn = addrs.NewLegacyProvider(rAddr.ImpliedProvider())
} else {
// ProviderDependencies only need to know the provider FQN
// strip any alias from the providerKey
parts := strings.Split(providerKey, ".")
fqn = addrs.NewLegacyProvider(parts[0])
}
log.Printf("[TRACE] Resource block for %s requires provider %q", rAddr, fqn)
if _, exists := m.Providers[fqn]; !exists {
m.Providers[fqn] = moduledeps.ProviderDependency{
Reason: moduledeps.ProviderDependencyImplicit,
}
}
ret.ResourceProviderType[rAddr] = fqn.Type
}
}
if variablesList := list.Filter("variable"); len(variablesList.Items) > 0 {
variableObjs := variablesList.Children()
for _, variableObj := range variableObjs.Items {
if len(variableObj.Keys) != 1 {
return nil, fmt.Errorf("variable block has wrong number of labels")
}
name := variableObj.Keys[0].Token.Value().(string)
var listVal *hcl1ast.ObjectList
if ot, ok := variableObj.Val.(*hcl1ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("variable %q: must be a block", name)
}
var typeStr string
if a := listVal.Filter("type"); len(a.Items) > 0 {
err := hcl1.DecodeObject(&typeStr, a.Items[0].Val)
if err != nil {
return nil, fmt.Errorf("Error reading type for variable %q: %s", name, err)
}
} else if a := listVal.Filter("default"); len(a.Items) > 0 {
switch a.Items[0].Val.(type) {
case *hcl1ast.ObjectType:
typeStr = "map"
case *hcl1ast.ListType:
typeStr = "list"
default:
typeStr = "string"
}
} else {
typeStr = "string"
}
ret.VariableTypes[name] = strings.TrimSpace(typeStr)
}
}
}
providerFactories, errs := u.Providers.ResolveProviders(m.PluginRequirements())
if len(errs) > 0 {
var errorsMsg string
for _, err := range errs {
errorsMsg += fmt.Sprintf("\n- %s", err)
}
return nil, fmt.Errorf("error resolving providers:\n%s", errorsMsg)
}
for fqn, fn := range providerFactories {
log.Printf("[TRACE] Fetching schema from provider %q", fqn.LegacyString())
provider, err := fn()
if err != nil {
return nil, fmt.Errorf("failed to load provider %q: %s", fqn.LegacyString(), err)
}
resp := provider.GetSchema()
if resp.Diagnostics.HasErrors() {
return nil, resp.Diagnostics.Err()
}
schema := &terraform.ProviderSchema{
Provider: resp.Provider.Block,
ResourceTypes: map[string]*configschema.Block{},
DataSources: map[string]*configschema.Block{},
}
for t, s := range resp.ResourceTypes {
schema.ResourceTypes[t] = s.Block
}
for t, s := range resp.DataSources {
schema.DataSources[t] = s.Block
}
ret.ProviderSchemas[fqn.LegacyString()] = schema
}
for name, fn := range u.Provisioners {
log.Printf("[TRACE] Fetching schema from provisioner %q", name)
provisioner, err := fn()
if err != nil {
return nil, fmt.Errorf("failed to load provisioner %q: %s", name, err)
}
resp := provisioner.GetSchema()
if resp.Diagnostics.HasErrors() {
return nil, resp.Diagnostics.Err()
}
ret.ProvisionerSchemas[name] = resp.Provisioner
}
return ret, nil
}

View File

@ -1,162 +0,0 @@
package configupgrade
import (
"log"
hcl2 "github.com/hashicorp/hcl/v2"
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/tfdiags"
)
// InferExpressionType attempts to determine a result type for the given
// expression source code, which should already have been upgraded to new
// expression syntax.
//
// If self is non-nil, it will determine the meaning of the special "self"
// reference.
//
// If such an inference isn't possible, either because of limitations of
// static analysis or because of errors in the expression, the result is
// cty.DynamicPseudoType indicating "unknown".
func (an *analysis) InferExpressionType(src []byte, self addrs.Referenceable) cty.Type {
expr, diags := hcl2syntax.ParseExpression(src, "", hcl2.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
// If there's a syntax error then analysis is impossible.
return cty.DynamicPseudoType
}
data := analysisData{an}
scope := &lang.Scope{
Data: data,
SelfAddr: self,
PureOnly: false,
BaseDir: ".",
}
val, _ := scope.EvalExpr(expr, cty.DynamicPseudoType)
// Value will be cty.DynamicVal if either inference was impossible or
// if there was an error, leading to cty.DynamicPseudoType here.
return val.Type()
}
// analysisData is an implementation of lang.Data that returns unknown values
// of suitable types in order to achieve approximate dynamic analysis of
// expression result types, which we need for some upgrade rules.
//
// Unlike a usual implementation of this interface, this one never returns
// errors and will instead just return cty.DynamicVal if it can't produce
// an exact type for any reason. This can then allow partial upgrading to
// proceed and the caller can emit warning comments for ambiguous situations.
//
// N.B.: Source ranges in the data methods are meaningless, since they are
// just relative to the byte array passed to InferExpressionType, not to
// any real input file.
type analysisData struct {
an *analysis
}
var _ lang.Data = (*analysisData)(nil)
func (d analysisData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics {
// This implementation doesn't do any static validation.
return nil
}
func (d analysisData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// All valid count attributes are numbers
return cty.UnknownVal(cty.Number), nil
}
func (d analysisData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return cty.DynamicVal, nil
}
func (d analysisData) GetResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
log.Printf("[TRACE] configupgrade: Determining type for %s", addr)
// Our analysis pass should've found a suitable schema for every resource
// type in the module.
providerType, ok := d.an.ResourceProviderType[addr]
if !ok {
// Should not be possible, since analysis visits every resource block.
log.Printf("[TRACE] configupgrade: analysis.GetResource doesn't have a provider type for %s", addr)
return cty.DynamicVal, nil
}
providerSchema, ok := d.an.ProviderSchemas[providerType]
if !ok {
// Should not be possible, since analysis loads schema for every provider.
log.Printf("[TRACE] configupgrade: analysis.GetResource doesn't have a provider schema for for %q", providerType)
return cty.DynamicVal, nil
}
schema, _ := providerSchema.SchemaForResourceAddr(addr)
if schema == nil {
// Should not be possible, since analysis loads schema for every provider.
log.Printf("[TRACE] configupgrade: analysis.GetResource doesn't have a schema for for %s", addr)
return cty.DynamicVal, nil
}
objTy := schema.ImpliedType()
// We'll emulate the normal evaluator's behavor of deciding whether to
// return a list or a single object type depending on whether count is
// set and whether an instance key is given in the address.
if d.an.ResourceHasCount[addr] {
log.Printf("[TRACE] configupgrade: %s refers to counted instance, so result is a list of %#v", addr, objTy)
return cty.UnknownVal(cty.List(objTy)), nil
}
log.Printf("[TRACE] configupgrade: %s refers to non-counted instance, so result is single object", addr)
return cty.UnknownVal(objTy), nil
}
func (d analysisData) GetLocalValue(addrs.LocalValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// We can only predict these in general by recursively evaluating their
// expressions, which creates some undesirable complexity here so for
// now we'll just skip analyses with locals and see if this complexity
// is warranted with real-world testing.
return cty.DynamicVal, nil
}
func (d analysisData) GetModuleInstance(addrs.ModuleCallInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// We only work on one module at a time during upgrading, so we have no
// information about the outputs of a child module.
return cty.DynamicVal, nil
}
func (d analysisData) GetModuleInstanceOutput(addrs.ModuleCallOutput, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// We only work on one module at a time during upgrading, so we have no
// information about the outputs of a child module.
return cty.DynamicVal, nil
}
func (d analysisData) GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// All valid path attributes are strings
return cty.UnknownVal(cty.String), nil
}
func (d analysisData) GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// All valid "terraform" attributes are strings
return cty.UnknownVal(cty.String), nil
}
func (d analysisData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO: Collect shallow type information (list vs. map vs. string vs. unknown)
// in analysis and then return a similarly-approximate type here.
log.Printf("[TRACE] configupgrade: Determining type for %s", addr)
name := addr.Name
typeName := d.an.VariableTypes[name]
switch typeName {
case "list":
return cty.UnknownVal(cty.List(cty.DynamicPseudoType)), nil
case "map":
return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), nil
case "string":
return cty.UnknownVal(cty.String), nil
default:
return cty.DynamicVal, nil
}
}

View File

@ -1,16 +0,0 @@
// Package configupgrade upgrades configurations targeting our legacy
// configuration loader (in package "config") to be compatible with and
// idiomatic for the newer configuration loader (in package "configs").
//
// It works on one module directory at a time, producing new content for
// each existing .tf file and possibly creating new files as needed. The
// legacy HCL and HIL parsers are used to read the existing configuration
// for maximum compatibility with any non-idiomatic constructs that were
// accepted by those implementations but not accepted by the new HCL parsers.
//
// Unlike the loaders and validators elsewhere in Terraform, this package
// always generates diagnostics with paths relative to the module directory
// currently being upgraded, with no intermediate paths. This means that the
// filenames in these ranges can be used directly as keys into the ModuleSources
// map that the file was parsed from.
package configupgrade

View File

@ -1,216 +0,0 @@
package configupgrade
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/hcl/v2"
hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax"
version "github.com/hashicorp/go-version"
)
type ModuleSources map[string][]byte
// LoadModule looks for Terraform configuration files in the given directory
// and loads each of them into memory as source code, in preparation for
// further analysis and conversion.
//
// At this stage the files are not parsed at all. Instead, we just read the
// raw bytes from the file so that they can be passed into a parser in a
// separate step.
//
// If the given directory or any of the files cannot be read, an error is
// returned. It is not safe to proceed with processing in that case because
// we cannot "see" all of the source code for the configuration.
func LoadModule(dir string) (ModuleSources, error) {
entries, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
ret := make(ModuleSources)
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
continue
}
if configs.IsIgnoredFile(name) {
continue
}
ext := fileExt(name)
if ext == "" {
continue
}
fullPath := filepath.Join(dir, name)
src, err := ioutil.ReadFile(fullPath)
if err != nil {
return nil, err
}
ret[name] = src
}
return ret, nil
}
// UnusedFilename finds a filename that isn't already used by a file in
// the receiving sources and returns it.
//
// The given "proposed" name is returned verbatim if it isn't already used.
// Otherwise, the function will try appending incrementing integers to the
// proposed name until an unused name is found. Callers should propose names
// that they do not expect to already be in use so that numeric suffixes are
// only used in rare cases.
//
// The proposed name must end in either ".tf" or ".tf.json" because a
// ModuleSources only has visibility into such files. This function will
// panic if given a file whose name does not end with one of these
// extensions.
//
// A ModuleSources only works on one directory at a time, so the proposed
// name must not contain any directory separator characters.
func (ms ModuleSources) UnusedFilename(proposed string) string {
ext := fileExt(proposed)
if ext == "" {
panic(fmt.Errorf("method UnusedFilename used with invalid proposal %q", proposed))
}
if _, exists := ms[proposed]; !exists {
return proposed
}
base := proposed[:len(proposed)-len(ext)]
for i := 1; ; i++ {
try := fmt.Sprintf("%s-%d%s", base, i, ext)
if _, exists := ms[try]; !exists {
return try
}
}
}
// MaybeAlreadyUpgraded is a heuristic to see if a given module may have
// already been upgraded by this package.
//
// The heuristic used is to look for a Terraform Core version constraint in
// any of the given sources that seems to be requiring a version greater than
// or equal to v0.12.0. If true is returned then the source range of the found
// version constraint is returned in case the caller wishes to present it to
// the user as context for a warning message. The returned range is not
// meaningful if false is returned.
func (ms ModuleSources) MaybeAlreadyUpgraded() (bool, tfdiags.SourceRange) {
for name, src := range ms {
f, diags := hcl2syntax.ParseConfig(src, name, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
// If we can't parse at all then that's a reasonable signal that
// we _haven't_ been upgraded yet, but we'll continue checking
// other files anyway.
continue
}
content, _, diags := f.Body.PartialContent(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "terraform",
},
},
})
if diags.HasErrors() {
// Suggests that the file has an invalid "terraform" block, such
// as one with labels.
continue
}
for _, block := range content.Blocks {
content, _, diags := block.Body.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "required_version",
},
},
})
if diags.HasErrors() {
continue
}
attr, present := content.Attributes["required_version"]
if !present {
continue
}
constraintVal, diags := attr.Expr.Value(nil)
if diags.HasErrors() {
continue
}
if constraintVal.Type() != cty.String || constraintVal.IsNull() {
continue
}
constraints, err := version.NewConstraint(constraintVal.AsString())
if err != nil {
continue
}
// The go-version package doesn't actually let us see the details
// of the parsed constraints here, so we now need a bit of an
// abstraction inversion to decide if any of the given constraints
// match our heuristic. However, we do at least get to benefit
// from go-version's ability to extract multiple constraints from
// a single string and the fact that it's already validated each
// constraint to match its expected pattern.
Constraints:
for _, constraint := range constraints {
str := strings.TrimSpace(constraint.String())
// Want to match >, >= and ~> here.
if !(strings.HasPrefix(str, ">") || strings.HasPrefix(str, "~>")) {
continue
}
// Try to find something in this string that'll parse as a version.
for i := 1; i < len(str); i++ {
candidate := str[i:]
v, err := version.NewVersion(candidate)
if err != nil {
continue
}
if v.Equal(firstVersionWithNewParser) || v.GreaterThan(firstVersionWithNewParser) {
// This constraint appears to be preventing the old
// parser from being used, so we suspect it was
// already upgraded.
return true, tfdiags.SourceRangeFromHCL(attr.Range)
}
// If we fall out here then we _did_ find something that
// parses as a version, so we'll stop and move on to the
// next constraint. (Otherwise we'll pass by 0.7.0 and find
// 7.0, which is also a valid version.)
continue Constraints
}
}
}
}
return false, tfdiags.SourceRange{}
}
var firstVersionWithNewParser = version.Must(version.NewVersion("0.12.0"))
// fileExt returns the Terraform configuration extension of the given
// path, or a blank string if it is not a recognized extension.
func fileExt(path string) string {
if strings.HasSuffix(path, ".tf") {
return ".tf"
} else if strings.HasSuffix(path, ".tf.json") {
return ".tf.json"
} else {
return ""
}
}

View File

@ -1,42 +0,0 @@
package configupgrade
import (
"reflect"
"testing"
"github.com/hashicorp/hcl/v2"
)
func TestMaybeAlreadyUpgraded(t *testing.T) {
t.Run("already upgraded", func(t *testing.T) {
sources, err := LoadModule("testdata/already-upgraded")
if err != nil {
t.Fatal(err)
}
got, rng := sources.MaybeAlreadyUpgraded()
if !got {
t.Fatal("result is false, but want true")
}
gotRange := rng.ToHCL()
wantRange := hcl.Range{
Filename: "versions.tf",
Start: hcl.Pos{Line: 3, Column: 3, Byte: 15},
End: hcl.Pos{Line: 3, Column: 33, Byte: 45},
}
if !reflect.DeepEqual(gotRange, wantRange) {
t.Errorf("wrong range\ngot: %#v\nwant: %#v", gotRange, wantRange)
}
})
t.Run("not yet upgraded", func(t *testing.T) {
sources, err := LoadModule("testdata/valid/noop/input")
if err != nil {
t.Fatal(err)
}
got, _ := sources.MaybeAlreadyUpgraded()
if got {
t.Fatal("result is true, but want false")
}
})
}

View File

@ -1,4 +0,0 @@
terraform {
required_version = ">= 0.13.0"
}

View File

@ -1,7 +0,0 @@
locals {
foo = "bar", baz = "boop"
}
resource "test_instance" "foo" {
image = "b", type = "d"
}

View File

@ -1,9 +0,0 @@
locals {
foo = "bar"
baz = "boop"
}
resource "test_instance" "foo" {
image = "b"
type = "d"
}

View File

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

View File

@ -1,10 +0,0 @@
resource "test_instance" "foo" {
network = [
{
cidr_block = "10.1.0.0/16"
},
{
cidr_block = "10.2.0.0/16"
},
]
}

View File

@ -1,8 +0,0 @@
resource "test_instance" "foo" {
network {
cidr_block = "10.1.0.0/16"
}
network {
cidr_block = "10.2.0.0/16"
}
}

View File

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

View File

@ -1,8 +0,0 @@
resource "test_instance" "foo" {
network = [
{
cidr_block = "10.1.2.0/24"
},
"${var.baz}"
]
}

View File

@ -1,23 +0,0 @@
resource "test_instance" "foo" {
network {
cidr_block = "10.1.2.0/24"
}
dynamic "network" {
for_each = [var.baz]
content {
# TF-UPGRADE-TODO: The automatic upgrade tool can't predict
# which keys might be set in maps assigned here, so it has
# produced a comprehensive set here. Consider simplifying
# this after confirming which keys can be set in practice.
cidr_block = lookup(network.value, "cidr_block", null)
dynamic "subnet" {
for_each = lookup(network.value, "subnet", [])
content {
number = subnet.value.number
}
}
}
}
}

View File

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

View File

@ -1,5 +0,0 @@
resource "test_instance" "foo" {
network {
subnet = "${var.baz}"
}
}

View File

@ -1,15 +0,0 @@
resource "test_instance" "foo" {
network {
dynamic "subnet" {
for_each = var.baz
content {
# TF-UPGRADE-TODO: The automatic upgrade tool can't predict
# which keys might be set in maps assigned here, so it has
# produced a comprehensive set here. Consider simplifying
# this after confirming which keys can be set in practice.
number = subnet.value.number
}
}
}
}

View File

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

View File

@ -1,3 +0,0 @@
resource "test_instance" "foo" {
network = "${var.baz}"
}

View File

@ -1,20 +0,0 @@
resource "test_instance" "foo" {
dynamic "network" {
for_each = var.baz
content {
# TF-UPGRADE-TODO: The automatic upgrade tool can't predict
# which keys might be set in maps assigned here, so it has
# produced a comprehensive set here. Consider simplifying
# this after confirming which keys can be set in practice.
cidr_block = lookup(network.value, "cidr_block", null)
dynamic "subnet" {
for_each = lookup(network.value, "subnet", [])
content {
number = subnet.value.number
}
}
}
}
}

View File

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

View File

@ -1,5 +0,0 @@
resource "test_instance" "foo" {
network = {
cidr_block = "10.1.0.0/16"
}
}

View File

@ -1,5 +0,0 @@
resource "test_instance" "foo" {
network {
cidr_block = "10.1.0.0/16"
}
}

View File

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

View File

@ -1,17 +0,0 @@
resource "test_instance" "foo" {
depends_on = [
"test_instance.bar",
"test_instance.bar.0",
"test_instance.bar.*",
"test_instance.bar.invalid",
"data.test_instance.baz",
"data.test_instance.baz.invalid",
"module.foo.bar",
"module.foo",
]
}
output "foo" {
value = "a"
depends_on = ["test_instance.foo"]
}

View File

@ -1,17 +0,0 @@
resource "test_instance" "foo" {
depends_on = [
test_instance.bar,
test_instance.bar[0],
test_instance.bar,
test_instance.bar,
data.test_instance.baz,
data.test_instance.baz,
module.foo.bar,
module.foo,
]
}
output "foo" {
value = "a"
depends_on = [test_instance.foo]
}

View File

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

View File

@ -1,7 +0,0 @@
resource "test_instance" "a" {
subnet_ids = ["boop"] # this attribute takes a set of strings
}
output "b" {
value = "${element(test_instance.a.subnet_ids, 0)}"
}

View File

@ -1,7 +0,0 @@
resource "test_instance" "a" {
subnet_ids = ["boop"] # this attribute takes a set of strings
}
output "b" {
value = element(tolist(test_instance.a.subnet_ids), 0)
}

View File

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

View File

@ -1,26 +0,0 @@
locals {
list = "${list("a", "b", "c")}"
list_concat = "${concat(list("a", "b"), list("c"))}"
list_empty = "${list()}"
map = "${map("a", "b", "c", "d")}"
map_merge = "${merge(map("a", "b"), map("c", "d"))}"
map_empty = "${map()}"
map_invalid = "${map("a", "b", "c")}"
list_of_map = "${list(map("a", "b"))}"
map_of_list = "${map("a", list("b"))}"
lookup_literal = "${lookup(map("a", "b"), "a")}"
lookup_ref = "${lookup(local.map, "a")}"
# Undocumented HIL implementation details that some users nonetheless relied on.
conv_bool_to_string = "${__builtin_BoolToString(true)}"
conv_float_to_int = "${__builtin_FloatToInt(1.5)}"
conv_float_to_string = "${__builtin_FloatToString(1.5)}"
conv_int_to_float = "${__builtin_IntToFloat(1)}"
conv_int_to_string = "${__builtin_IntToString(1)}"
conv_string_to_int = "${__builtin_StringToInt("1")}"
conv_string_to_float = "${__builtin_StringToFloat("1.5")}"
conv_string_to_bool = "${__builtin_StringToBool("true")}"
}

View File

@ -1,44 +0,0 @@
locals {
list = ["a", "b", "c"]
list_concat = concat(["a", "b"], ["c"])
list_empty = []
map = {
"a" = "b"
"c" = "d"
}
map_merge = merge(
{
"a" = "b"
},
{
"c" = "d"
},
)
map_empty = {}
map_invalid = map("a", "b", "c")
list_of_map = [
{
"a" = "b"
},
]
map_of_list = {
"a" = ["b"]
}
lookup_literal = {
"a" = "b"
}["a"]
lookup_ref = local.map["a"]
# Undocumented HIL implementation details that some users nonetheless relied on.
conv_bool_to_string = tostring(tobool(true))
conv_float_to_int = floor(1.5)
conv_float_to_string = tostring(tonumber(1.5))
conv_int_to_float = floor(1)
conv_int_to_string = tostring(floor(1))
conv_string_to_int = floor(tostring("1"))
conv_string_to_float = tonumber(tostring("1.5"))
conv_string_to_bool = tobool(tostring("true"))
}

View File

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

View File

@ -1,3 +0,0 @@
resource "test_instance" "foo" {
image = "${sha256(file("foo.txt"))}"
}

View File

@ -1,3 +0,0 @@
resource "test_instance" "foo" {
image = filesha256("foo.txt")
}

View File

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

View File

@ -1,12 +0,0 @@
locals {
baz = { "greeting" = "hello" }
cert_options = <<-EOF
A
B ${lookup(local.baz, "greeting")}
C
EOF
}
output "local" {
value = "${local.cert_options}"
}

View File

@ -1,15 +0,0 @@
locals {
baz = {
"greeting" = "hello"
}
cert_options = <<-EOF
A
B ${local.baz["greeting"]}
C
EOF
}
output "local" {
value = local.cert_options
}

View File

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

View File

@ -1,6 +0,0 @@
variable "foo" {
default = <<EOT
Interpolation sequences ${are not allowed} in here.
EOT
}

View File

@ -1,6 +0,0 @@
variable "foo" {
default = <<EOT
Interpolation sequences $${are not allowed} in here.
EOT
}

View File

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

View File

@ -1,10 +0,0 @@
locals {
cert_options = <<EOF
--cert-file=/etc/ssl/etcd/server.crt \
--peer-trusted-ca-file=/etc/ssl/etcd/ca.crt \
--peer-client-cert-auth=trueEOF
}
output "local" {
value = "${local.cert_options}"
}

View File

@ -1,12 +0,0 @@
locals {
cert_options = <<EOF
--cert-file=/etc/ssl/etcd/server.crt \
--peer-trusted-ca-file=/etc/ssl/etcd/ca.crt \
--peer-client-cert-auth=true
EOF
}
output "local" {
value = local.cert_options
}

View File

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

View File

@ -1,12 +0,0 @@
locals {
baz = { "greeting" = "hello" }
cert_options = <<EOF
A
B ${lookup(local.baz, "greeting")}
C
EOF
}
output "local" {
value = "${local.cert_options}"
}

View File

@ -1,16 +0,0 @@
locals {
baz = {
"greeting" = "hello"
}
cert_options = <<EOF
A
B ${local.baz["greeting"]}
C
EOF
}
output "local" {
value = local.cert_options
}

View File

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

View File

@ -1,8 +0,0 @@
resource "test_instance" "foo" {
lifecycle {
ignore_changes = [
"a.%",
"b.#",
]
}
}

View File

@ -1,8 +0,0 @@
resource "test_instance" "foo" {
lifecycle {
ignore_changes = [
a,
b,
]
}
}

View File

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

View File

@ -1,16 +0,0 @@
resource "test_instance" "first_many" {
count = 2
}
resource "test_instance" "one" {
image = "${test_instance.first_many.*.id[0]}"
}
resource "test_instance" "splat_of_one" {
image = "${test_instance.one.*.id[0]}"
}
resource "test_instance" "second_many" {
count = "${length(test_instance.first_many)}"
security_groups = "${test_instance.first_many.*.id[count.index]}"
}

View File

@ -1,16 +0,0 @@
resource "test_instance" "first_many" {
count = 2
}
resource "test_instance" "one" {
image = test_instance.first_many[0].id
}
resource "test_instance" "splat_of_one" {
image = test_instance.one.*.id[0]
}
resource "test_instance" "second_many" {
count = length(test_instance.first_many)
security_groups = test_instance.first_many[count.index].id
}

View File

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

View File

@ -1,25 +0,0 @@
variable "list" {
type = "list"
default = [
"foo", # I am a comment
"bar", # I am also a comment
"baz",
]
}
variable "list2" {
type = "list"
default = [
"foo",
"bar",
"baz",
]
}
variable "list_the_third" {
type = "list"
default = ["foo", "bar", "baz"]
}

View File

@ -1,25 +0,0 @@
variable "list" {
type = list(string)
default = [
"foo", # I am a comment
"bar", # I am also a comment
"baz",
]
}
variable "list2" {
type = list(string)
default = [
"foo",
"bar",
"baz",
]
}
variable "list_the_third" {
type = list(string)
default = ["foo", "bar", "baz"]
}

View File

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

View File

@ -1,3 +0,0 @@
output "foo" {
value = "${path.module}"
}

View File

@ -1,3 +0,0 @@
output "foo" {
value = path.module
}

View File

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

View File

@ -1,2 +0,0 @@
resource "test_instance" "1_invalid_resource_name" {
}

View File

@ -1,8 +0,0 @@
# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to begin a
# resource name with a number, but it is no longer possible in Terraform v0.12.
#
# Rename the resource and run `terraform state mv` to apply the rename in the
# state. Detailed information on the `state move` command can be found in the
# documentation online: https://www.terraform.io/docs/commands/state/mv.html
resource "test_instance" "1_invalid_resource_name" {
}

View File

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

View File

@ -1,29 +0,0 @@
resource "test_instance" "foo" {
lifecycle {
create_before_destroy = true
prevent_destroy = true
}
}
resource "test_instance" "bar" {
lifecycle {
ignore_changes = ["*"]
}
}
resource "test_instance" "baz" {
lifecycle {
ignore_changes = [
"image",
"tags.name",
]
}
}
resource "test_instance" "boop" {
lifecycle {
ignore_changes = [
"image",
]
}
}

View File

@ -1,27 +0,0 @@
resource "test_instance" "foo" {
lifecycle {
create_before_destroy = true
prevent_destroy = true
}
}
resource "test_instance" "bar" {
lifecycle {
ignore_changes = all
}
}
resource "test_instance" "baz" {
lifecycle {
ignore_changes = [
image,
tags.name,
]
}
}
resource "test_instance" "boop" {
lifecycle {
ignore_changes = [image]
}
}

View File

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

View File

@ -1,15 +0,0 @@
resource "test_instance" "from_list" {
list_of_obj = [
{},
{},
]
}
resource "test_instance" "already_blocks" {
list_of_obj {}
list_of_obj {}
}
resource "test_instance" "empty" {
list_of_obj = []
}

View File

@ -1,17 +0,0 @@
resource "test_instance" "from_list" {
list_of_obj {
}
list_of_obj {
}
}
resource "test_instance" "already_blocks" {
list_of_obj {
}
list_of_obj {
}
}
resource "test_instance" "empty" {
list_of_obj = []
}

View File

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

View File

@ -1,7 +0,0 @@
resource "test_instance" "foo" {
type = "z1.weedy"
image = "image-abcd"
tags {
name = "boop"
}
}

View File

@ -1,7 +0,0 @@
resource "test_instance" "foo" {
type = "z1.weedy"
image = "image-abcd"
tags = {
name = "boop"
}
}

View File

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

View File

@ -1,7 +0,0 @@
// Test note: the configupgrade tool will ignore this possibly-relative module
// source because it does not find a local directory "foo". The example where
// the configupgrade tool makes a recommendation about relative module sources
// is is in relative-module-source.
module "foo" {
source = "foo"
}

View File

@ -1,7 +0,0 @@
// Test note: the configupgrade tool will ignore this possibly-relative module
// source because it does not find a local directory "foo". The example where
// the configupgrade tool makes a recommendation about relative module sources
// is is in relative-module-source.
module "foo" {
source = "foo"
}

View File

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

View File

@ -1,18 +0,0 @@
locals {
in_map = {
foo = "${var.baz}"
}
in_list = [
"${var.baz}",
"${var.bar}",
]
in_list_oneline = ["${var.baz}", "${var.bar}"]
in_map_of_list = {
foo = ["${var.baz}"]
}
in_list_of_map = [
{
foo = "${var.baz}"
}
]
}

View File

@ -1,18 +0,0 @@
locals {
in_map = {
foo = var.baz
}
in_list = [
var.baz,
var.bar,
]
in_list_oneline = [var.baz, var.bar]
in_map_of_list = {
foo = [var.baz]
}
in_list_of_map = [
{
foo = var.baz
},
]
}

View File

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

View File

@ -1,33 +0,0 @@
locals {
# Arithmetic
add = "${1 + 2}"
sub = "${1 - 2}"
mul = "${1 * 2}"
mod = "${4 % 2}"
and = "${true && true}"
or = "${true || true}"
equal = "${1 == 2}"
not_equal = "${1 != 2}"
less_than = "${1 < 2}"
greater_than = "${1 > 2}"
less_than_eq = "${1 <= 2}"
greater_than_eq = "${1 >= 2}"
neg = "${- local.add}"
# Call
call_no_args = "${foo()}"
call_one_arg = "${foo(1)}"
call_two_args = "${foo(1, 2)}"
# Conditional
cond = "${true ? 1 : 2}"
# Index
index_str = "${foo["a"]}"
index_num = "${foo[1]}"
# Variable Access
var_access_single = "${foo}"
var_access_dot = "${foo.bar}"
var_access_splat = "${foo.bar.*.baz}"
}

View File

@ -1,33 +0,0 @@
locals {
# Arithmetic
add = 1 + 2
sub = 1 - 2
mul = 1 * 2
mod = 4 % 2
and = true && true
or = true || true
equal = 1 == 2
not_equal = 1 != 2
less_than = 1 < 2
greater_than = 1 > 2
less_than_eq = 1 <= 2
greater_than_eq = 1 >= 2
neg = -local.add
# Call
call_no_args = foo()
call_one_arg = foo(1)
call_two_args = foo(1, 2)
# Conditional
cond = true ? 1 : 2
# Index
index_str = foo["a"]
index_num = foo[1]
# Variable Access
var_access_single = foo
var_access_dot = foo.bar
var_access_splat = foo.bar.*.baz
}

View File

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

View File

@ -1,3 +0,0 @@
module "foo" {
source = "./foo"
}

View File

@ -1,4 +0,0 @@
output "foo" {
value = "jeepers ${var.bar}"
}

View File

@ -1,11 +0,0 @@
terraform {
required_version = ">= 0.7.0, <0.13.0"
backend "local" {
path = "foo.tfstate"
}
}
provider "test" {
}

View File

@ -1,16 +0,0 @@
resource "test_instance" "example" {
tags = {
# Thingy thing
name = "foo bar baz" # this is a terrible name
}
connection {
host = "127.0.0.1"
}
provisioner "test" {
connection {
host = "127.0.0.2"
}
}
}

View File

@ -1,18 +0,0 @@
/* This multi-line comment
should survive */
# This comment should survive
variable "foo" {
default = 1 // This comment should also survive
}
// These adjacent comments should remain adjacent
// to one another.
variable "bar" {
/* This comment should survive too */
description = "bar the baz"
}
// This comment that isn't attached to anything should survive.

View File

@ -1,3 +0,0 @@
module "foo" {
source = "./foo"
}

View File

@ -1,4 +0,0 @@
output "foo" {
value = "jeepers ${var.bar}"
}

View File

@ -1,11 +0,0 @@
terraform {
required_version = ">= 0.7.0, <0.13.0"
backend "local" {
path = "foo.tfstate"
}
}
provider "test" {
}

View File

@ -1,16 +0,0 @@
resource "test_instance" "example" {
tags = {
# Thingy thing
name = "foo bar baz" # this is a terrible name
}
connection {
host = "127.0.0.1"
}
provisioner "test" {
connection {
host = "127.0.0.2"
}
}
}

View File

@ -1,17 +0,0 @@
/* This multi-line comment
should survive */
# This comment should survive
variable "foo" {
default = 1 // This comment should also survive
}
// These adjacent comments should remain adjacent
// to one another.
variable "bar" {
/* This comment should survive too */
description = "bar the baz"
}
// This comment that isn't attached to anything should survive.

View File

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

View File

@ -1,7 +0,0 @@
locals {
decimal_int = 1
decimal_float = 1.5
decimal_float_tricky = 0.1
hex_int = 0xff
octal_int = 0777
}

View File

@ -1,7 +0,0 @@
locals {
decimal_int = 1
decimal_float = 1.5
decimal_float_tricky = 0.1
hex_int = 255
octal_int = 511
}

Some files were not shown because too many files have changed in this diff Show More