diff --git a/command/012_config_upgrade.go b/command/012_config_upgrade.go index d4f73e67c..497e0d1b7 100644 --- a/command/012_config_upgrade.go +++ b/command/012_config_upgrade.go @@ -1,256 +1,15 @@ package command -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" +import "fmt" - "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 { Meta } 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(` -This command will rewrite the configuration files in the given directory so -that they use the new syntax features from Terraform v0.12, and will identify -any constructs that may need to be adjusted for correct operation with -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 - } - - 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. -`) - } - - } + 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.`)) 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" -} diff --git a/command/012_config_upgrade_test.go b/command/012_config_upgrade_test.go new file mode 100644 index 000000000..b79c6e26f --- /dev/null +++ b/command/012_config_upgrade_test.go @@ -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) + } +} diff --git a/command/init.go b/command/init.go index 3db440f10..8cb0145ae 100644 --- a/command/init.go +++ b/command/init.go @@ -18,7 +18,6 @@ import ( backendInit "github.com/hashicorp/terraform/backend/init" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs/configupgrade" "github.com/hashicorp/terraform/internal/earlyconfig" "github.com/hashicorp/terraform/internal/initwd" "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 // our "normal" and "early" configuration codepaths. If early succeeds // 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 - // 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. + // using syntax that worked in 0.11 but no longer in v0.12. rootMod, confDiags := c.loadSingleModule(path) rootModEarly, earlyConfDiags := c.loadSingleModuleEarly(path) - configUpgradeProbablyNeeded := false if confDiags.HasErrors() { if earlyConfDiags.HasErrors() { // 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) return 1 } - - // If _only_ the main loader produced errors then that suggests an - // upgrade may help. To give us more certainty here, we'll use the - // same heuristic that "terraform 0.12upgrade" uses to guess if a - // configuration has already been upgraded, to reduce the risk that - // we'll produce a misleading message if the problem is just a regular - // 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 - } - } - configUpgradeProbablyNeeded = true + // If _only_ the main loader produced errors then that suggests the + // configuration is written in 0.11-style syntax. We will return an + // error suggesting the user upgrade their config manually or with + // Terraform v0.12 + c.Ui.Error(strings.TrimSpace(errInitConfigErrorMaybeLegacySyntax)) + c.showDiagnostics(earlyConfDiags) + return 1 } + + // If _only_ the early loader encountered errors then that's unusual + // (it should generally be a superset of the normal loader) but we'll + // return those errors anyway since otherwise we'll probably get + // some weird behavior downstream. Errors from the early loader are + // generally not as high-quality since it has less context to work with. if earlyConfDiags.HasErrors() { - // If _only_ the early loader encountered errors then that's unusual - // (it should generally be a superset of the normal loader) but we'll - // return those errors anyway since otherwise we'll probably get - // some weird behavior downstream. Errors from the early loader are - // generally not as high-quality since it has less context to work with. 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) 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 - // the whole configuration tree. + // With all of the modules (hopefully) installed, we can now try to load the + // whole configuration tree. // // 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 - // loading both gives us an opportunity to prefer the better error messages - // from the normal loader if both fail. - _, confDiags = c.loadConfig(path) + // loaders here. Subsequent work will only use the early config, but loading + // both gives us an opportunity to prefer the better error messages from the + // normal loader if both fail. + 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() { c.Ui.Error(strings.TrimSpace(errInitConfigError)) diags = diags.Append(earlyConfDiags) @@ -259,43 +235,38 @@ func (c *InitCommand) Run(args []string) int { 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) - diags = diags.Append(versionDiags) - if versionDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } + _, confDiags = c.loadConfig(path) + if confDiags.HasErrors() { + c.Ui.Error(strings.TrimSpace(errInitConfigError)) + diags = diags.Append(confDiags) + 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) + diags = diags.Append(versionDiags) + if versionDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 } var back backend.Backend 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) - diags = diags.Append(backendDiags) - if backendDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - if backendOutput { - header = true - } - back = be + + be, backendOutput, backendDiags := c.initBackend(rootMod, flagConfigExtra) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 } + if backendOutput { + header = true + } + back = be } else { // load the previously-stored backend config 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 // still the final thing shown. 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))) if !c.RunningInAutomation { // 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. c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessCLI))) } - 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. ` +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 = ` 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. @@ -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. ` -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 = ` The following providers do not have any version constraints in configuration, so the latest version was installed. diff --git a/command/init_test.go b/command/init_test.go index 1ea527594..432f933b3 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -1424,86 +1424,14 @@ func TestInit_pluginWithInternal(t *testing.T) { } } -func TestInit_012UpgradeNeeded(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, - } - - 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) { +// The module in this test uses terraform 0.11-style syntax. We expect that the +// earlyconfig will succeed but the main loader fail, and return an error that +// indicates that syntax upgrades may be required. +func TestInit_syntaxErrorUpgradeHint(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) + + // This module copy.CopyDir(testFixturePath("init-sniff-version-error"), td) defer os.RemoveAll(td) defer testChdir(t, td)() @@ -1517,16 +1445,13 @@ func TestInit_syntaxErrorVersionSniff(t *testing.T) { } args := []string{} - if code := c.Run(args); code != 0 { + if code := c.Run(args); code != 1 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } // Check output. - // Currently, this lands in the "upgrade may be needed" codepath, because - // the intentional syntax error in our test fixture is something that - // "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) { + output := ui.ErrorWriter.String() + 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) { t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want) } } diff --git a/command/testdata/init-012upgrade/main.tf b/command/testdata/init-012upgrade/main.tf deleted file mode 100644 index 5b77095a7..000000000 --- a/command/testdata/init-012upgrade/main.tf +++ /dev/null @@ -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" - } -} diff --git a/commands.go b/commands.go index 899cb11e6..afcf8581c 100644 --- a/commands.go +++ b/commands.go @@ -308,7 +308,7 @@ func initCommands(config *cliconfig.Config, services *disco.Disco, providerSrc g //----------------------------------------------------------- "0.12upgrade": func() (cli.Command, error) { - return &command.ZeroTwelveUpgradeCommand{ + return &command.ZeroThirteenUpgradeCommand{ Meta: meta, }, nil }, diff --git a/configs/configupgrade/analysis.go b/configs/configupgrade/analysis.go deleted file mode 100644 index d6b025020..000000000 --- a/configs/configupgrade/analysis.go +++ /dev/null @@ -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 -} diff --git a/configs/configupgrade/analysis_expr.go b/configs/configupgrade/analysis_expr.go deleted file mode 100644 index e34e8dd17..000000000 --- a/configs/configupgrade/analysis_expr.go +++ /dev/null @@ -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 - } -} diff --git a/configs/configupgrade/doc.go b/configs/configupgrade/doc.go deleted file mode 100644 index aa3267434..000000000 --- a/configs/configupgrade/doc.go +++ /dev/null @@ -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 diff --git a/configs/configupgrade/module_sources.go b/configs/configupgrade/module_sources.go deleted file mode 100644 index 247fbb47e..000000000 --- a/configs/configupgrade/module_sources.go +++ /dev/null @@ -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 "" - } -} diff --git a/configs/configupgrade/module_sources_test.go b/configs/configupgrade/module_sources_test.go deleted file mode 100644 index ba73afe34..000000000 --- a/configs/configupgrade/module_sources_test.go +++ /dev/null @@ -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") - } - }) -} diff --git a/configs/configupgrade/testdata/already-upgraded/versions.tf b/configs/configupgrade/testdata/already-upgraded/versions.tf deleted file mode 100644 index 1a1cb594f..000000000 --- a/configs/configupgrade/testdata/already-upgraded/versions.tf +++ /dev/null @@ -1,4 +0,0 @@ - -terraform { - required_version = ">= 0.13.0" -} diff --git a/configs/configupgrade/testdata/valid/argument-commas/input/argument-commas.tf b/configs/configupgrade/testdata/valid/argument-commas/input/argument-commas.tf deleted file mode 100644 index 9c15a6fdb..000000000 --- a/configs/configupgrade/testdata/valid/argument-commas/input/argument-commas.tf +++ /dev/null @@ -1,7 +0,0 @@ -locals { - foo = "bar", baz = "boop" -} - -resource "test_instance" "foo" { - image = "b", type = "d" -} diff --git a/configs/configupgrade/testdata/valid/argument-commas/want/argument-commas.tf b/configs/configupgrade/testdata/valid/argument-commas/want/argument-commas.tf deleted file mode 100644 index c0a514e1c..000000000 --- a/configs/configupgrade/testdata/valid/argument-commas/want/argument-commas.tf +++ /dev/null @@ -1,9 +0,0 @@ -locals { - foo = "bar" - baz = "boop" -} - -resource "test_instance" "foo" { - image = "b" - type = "d" -} diff --git a/configs/configupgrade/testdata/valid/argument-commas/want/versions.tf b/configs/configupgrade/testdata/valid/argument-commas/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/argument-commas/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-attr/input/block-as-list-attr.tf b/configs/configupgrade/testdata/valid/block-as-list-attr/input/block-as-list-attr.tf deleted file mode 100644 index 30ccae8b5..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-attr/input/block-as-list-attr.tf +++ /dev/null @@ -1,10 +0,0 @@ -resource "test_instance" "foo" { - network = [ - { - cidr_block = "10.1.0.0/16" - }, - { - cidr_block = "10.2.0.0/16" - }, - ] -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-attr/want/block-as-list-attr.tf b/configs/configupgrade/testdata/valid/block-as-list-attr/want/block-as-list-attr.tf deleted file mode 100644 index 00d13d13c..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-attr/want/block-as-list-attr.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "test_instance" "foo" { - network { - cidr_block = "10.1.0.0/16" - } - network { - cidr_block = "10.2.0.0/16" - } -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-attr/want/versions.tf b/configs/configupgrade/testdata/valid/block-as-list-attr/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-attr/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/input/block-as-list-dynamic-item.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/input/block-as-list-dynamic-item.tf deleted file mode 100644 index bfbb18622..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/input/block-as-list-dynamic-item.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "test_instance" "foo" { - network = [ - { - cidr_block = "10.1.2.0/24" - }, - "${var.baz}" - ] -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/want/block-as-list-dynamic-item.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/want/block-as-list-dynamic-item.tf deleted file mode 100644 index d5b982b4f..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/want/block-as-list-dynamic-item.tf +++ /dev/null @@ -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 - } - } - } - } -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/want/versions.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic-item/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/input/block-as-list-dynamic-nested.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/input/block-as-list-dynamic-nested.tf deleted file mode 100644 index df96a77c1..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/input/block-as-list-dynamic-nested.tf +++ /dev/null @@ -1,5 +0,0 @@ -resource "test_instance" "foo" { - network { - subnet = "${var.baz}" - } -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/want/block-as-list-dynamic-nested.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/want/block-as-list-dynamic-nested.tf deleted file mode 100644 index f32824819..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/want/block-as-list-dynamic-nested.tf +++ /dev/null @@ -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 - } - } - } -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/want/versions.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic-nested/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic/input/block-as-list-dynamic.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic/input/block-as-list-dynamic.tf deleted file mode 100644 index e436ad0db..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic/input/block-as-list-dynamic.tf +++ /dev/null @@ -1,3 +0,0 @@ -resource "test_instance" "foo" { - network = "${var.baz}" -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic/want/block-as-list-dynamic.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic/want/block-as-list-dynamic.tf deleted file mode 100644 index 1db25aba3..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic/want/block-as-list-dynamic.tf +++ /dev/null @@ -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 - } - } - } - } -} diff --git a/configs/configupgrade/testdata/valid/block-as-list-dynamic/want/versions.tf b/configs/configupgrade/testdata/valid/block-as-list-dynamic/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/block-as-list-dynamic/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/block-as-map-attr/input/block-as-map-attr.tf b/configs/configupgrade/testdata/valid/block-as-map-attr/input/block-as-map-attr.tf deleted file mode 100644 index a6843b0bc..000000000 --- a/configs/configupgrade/testdata/valid/block-as-map-attr/input/block-as-map-attr.tf +++ /dev/null @@ -1,5 +0,0 @@ -resource "test_instance" "foo" { - network = { - cidr_block = "10.1.0.0/16" - } -} diff --git a/configs/configupgrade/testdata/valid/block-as-map-attr/want/block-as-map-attr.tf b/configs/configupgrade/testdata/valid/block-as-map-attr/want/block-as-map-attr.tf deleted file mode 100644 index dacb14d87..000000000 --- a/configs/configupgrade/testdata/valid/block-as-map-attr/want/block-as-map-attr.tf +++ /dev/null @@ -1,5 +0,0 @@ -resource "test_instance" "foo" { - network { - cidr_block = "10.1.0.0/16" - } -} diff --git a/configs/configupgrade/testdata/valid/block-as-map-attr/want/versions.tf b/configs/configupgrade/testdata/valid/block-as-map-attr/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/block-as-map-attr/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/depends-on/input/depends-on.tf b/configs/configupgrade/testdata/valid/depends-on/input/depends-on.tf deleted file mode 100644 index fc3d1c938..000000000 --- a/configs/configupgrade/testdata/valid/depends-on/input/depends-on.tf +++ /dev/null @@ -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"] -} diff --git a/configs/configupgrade/testdata/valid/depends-on/want/depends-on.tf b/configs/configupgrade/testdata/valid/depends-on/want/depends-on.tf deleted file mode 100644 index b8cbebc30..000000000 --- a/configs/configupgrade/testdata/valid/depends-on/want/depends-on.tf +++ /dev/null @@ -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] -} diff --git a/configs/configupgrade/testdata/valid/depends-on/want/versions.tf b/configs/configupgrade/testdata/valid/depends-on/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/depends-on/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/element-of-set/input/element-of-set.tf b/configs/configupgrade/testdata/valid/element-of-set/input/element-of-set.tf deleted file mode 100644 index 7d64a6956..000000000 --- a/configs/configupgrade/testdata/valid/element-of-set/input/element-of-set.tf +++ /dev/null @@ -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)}" -} diff --git a/configs/configupgrade/testdata/valid/element-of-set/want/element-of-set.tf b/configs/configupgrade/testdata/valid/element-of-set/want/element-of-set.tf deleted file mode 100644 index fcc6c4edf..000000000 --- a/configs/configupgrade/testdata/valid/element-of-set/want/element-of-set.tf +++ /dev/null @@ -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) -} diff --git a/configs/configupgrade/testdata/valid/element-of-set/want/versions.tf b/configs/configupgrade/testdata/valid/element-of-set/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/element-of-set/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/funcs-replaced/input/funcs-replaced.tf b/configs/configupgrade/testdata/valid/funcs-replaced/input/funcs-replaced.tf deleted file mode 100644 index 9f7336f69..000000000 --- a/configs/configupgrade/testdata/valid/funcs-replaced/input/funcs-replaced.tf +++ /dev/null @@ -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")}" -} diff --git a/configs/configupgrade/testdata/valid/funcs-replaced/want/funcs-replaced.tf b/configs/configupgrade/testdata/valid/funcs-replaced/want/funcs-replaced.tf deleted file mode 100644 index 1664ee9b4..000000000 --- a/configs/configupgrade/testdata/valid/funcs-replaced/want/funcs-replaced.tf +++ /dev/null @@ -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")) -} diff --git a/configs/configupgrade/testdata/valid/funcs-replaced/want/versions.tf b/configs/configupgrade/testdata/valid/funcs-replaced/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/funcs-replaced/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/hash-of-file/input/hash-of-file.tf b/configs/configupgrade/testdata/valid/hash-of-file/input/hash-of-file.tf deleted file mode 100644 index 3b4bf3379..000000000 --- a/configs/configupgrade/testdata/valid/hash-of-file/input/hash-of-file.tf +++ /dev/null @@ -1,3 +0,0 @@ -resource "test_instance" "foo" { - image = "${sha256(file("foo.txt"))}" -} diff --git a/configs/configupgrade/testdata/valid/hash-of-file/want/hash-of-file.tf b/configs/configupgrade/testdata/valid/hash-of-file/want/hash-of-file.tf deleted file mode 100644 index 00672b75d..000000000 --- a/configs/configupgrade/testdata/valid/hash-of-file/want/hash-of-file.tf +++ /dev/null @@ -1,3 +0,0 @@ -resource "test_instance" "foo" { - image = filesha256("foo.txt") -} diff --git a/configs/configupgrade/testdata/valid/hash-of-file/want/versions.tf b/configs/configupgrade/testdata/valid/hash-of-file/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/hash-of-file/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/heredoc-flush/input/heredoc.tf b/configs/configupgrade/testdata/valid/heredoc-flush/input/heredoc.tf deleted file mode 100644 index 918bbb66b..000000000 --- a/configs/configupgrade/testdata/valid/heredoc-flush/input/heredoc.tf +++ /dev/null @@ -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}" -} diff --git a/configs/configupgrade/testdata/valid/heredoc-flush/want/heredoc.tf b/configs/configupgrade/testdata/valid/heredoc-flush/want/heredoc.tf deleted file mode 100644 index b17763f36..000000000 --- a/configs/configupgrade/testdata/valid/heredoc-flush/want/heredoc.tf +++ /dev/null @@ -1,15 +0,0 @@ -locals { - baz = { - "greeting" = "hello" - } - cert_options = <<-EOF - A - B ${local.baz["greeting"]} - C -EOF - -} - -output "local" { - value = local.cert_options -} diff --git a/configs/configupgrade/testdata/valid/heredoc-flush/want/versions.tf b/configs/configupgrade/testdata/valid/heredoc-flush/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/heredoc-flush/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/heredoc-no-interp/input/heredoc.tf b/configs/configupgrade/testdata/valid/heredoc-no-interp/input/heredoc.tf deleted file mode 100644 index b80973754..000000000 --- a/configs/configupgrade/testdata/valid/heredoc-no-interp/input/heredoc.tf +++ /dev/null @@ -1,6 +0,0 @@ -variable "foo" { - default = < 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 -} diff --git a/configs/configupgrade/testdata/valid/noop-exprs/want/versions.tf b/configs/configupgrade/testdata/valid/noop-exprs/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/noop-exprs/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/noop/input/modules.tf b/configs/configupgrade/testdata/valid/noop/input/modules.tf deleted file mode 100644 index 9ae1dbec4..000000000 --- a/configs/configupgrade/testdata/valid/noop/input/modules.tf +++ /dev/null @@ -1,3 +0,0 @@ -module "foo" { - source = "./foo" -} diff --git a/configs/configupgrade/testdata/valid/noop/input/outputs.tf b/configs/configupgrade/testdata/valid/noop/input/outputs.tf deleted file mode 100644 index e6706feea..000000000 --- a/configs/configupgrade/testdata/valid/noop/input/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ - -output "foo" { - value = "jeepers ${var.bar}" -} diff --git a/configs/configupgrade/testdata/valid/noop/input/providers.tf b/configs/configupgrade/testdata/valid/noop/input/providers.tf deleted file mode 100644 index b5cdd2295..000000000 --- a/configs/configupgrade/testdata/valid/noop/input/providers.tf +++ /dev/null @@ -1,11 +0,0 @@ - -terraform { - required_version = ">= 0.7.0, <0.13.0" - - backend "local" { - path = "foo.tfstate" - } -} - -provider "test" { -} diff --git a/configs/configupgrade/testdata/valid/noop/input/resources.tf b/configs/configupgrade/testdata/valid/noop/input/resources.tf deleted file mode 100644 index 17f3f8c88..000000000 --- a/configs/configupgrade/testdata/valid/noop/input/resources.tf +++ /dev/null @@ -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" - } - } -} diff --git a/configs/configupgrade/testdata/valid/noop/input/variables.tf b/configs/configupgrade/testdata/valid/noop/input/variables.tf deleted file mode 100644 index 6f2da3b5c..000000000 --- a/configs/configupgrade/testdata/valid/noop/input/variables.tf +++ /dev/null @@ -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. diff --git a/configs/configupgrade/testdata/valid/noop/want/modules.tf b/configs/configupgrade/testdata/valid/noop/want/modules.tf deleted file mode 100644 index 9ae1dbec4..000000000 --- a/configs/configupgrade/testdata/valid/noop/want/modules.tf +++ /dev/null @@ -1,3 +0,0 @@ -module "foo" { - source = "./foo" -} diff --git a/configs/configupgrade/testdata/valid/noop/want/outputs.tf b/configs/configupgrade/testdata/valid/noop/want/outputs.tf deleted file mode 100644 index e6706feea..000000000 --- a/configs/configupgrade/testdata/valid/noop/want/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ - -output "foo" { - value = "jeepers ${var.bar}" -} diff --git a/configs/configupgrade/testdata/valid/noop/want/providers.tf b/configs/configupgrade/testdata/valid/noop/want/providers.tf deleted file mode 100644 index b5cdd2295..000000000 --- a/configs/configupgrade/testdata/valid/noop/want/providers.tf +++ /dev/null @@ -1,11 +0,0 @@ - -terraform { - required_version = ">= 0.7.0, <0.13.0" - - backend "local" { - path = "foo.tfstate" - } -} - -provider "test" { -} diff --git a/configs/configupgrade/testdata/valid/noop/want/resources.tf b/configs/configupgrade/testdata/valid/noop/want/resources.tf deleted file mode 100644 index 17f3f8c88..000000000 --- a/configs/configupgrade/testdata/valid/noop/want/resources.tf +++ /dev/null @@ -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" - } - } -} diff --git a/configs/configupgrade/testdata/valid/noop/want/variables.tf b/configs/configupgrade/testdata/valid/noop/want/variables.tf deleted file mode 100644 index 333bc1149..000000000 --- a/configs/configupgrade/testdata/valid/noop/want/variables.tf +++ /dev/null @@ -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. diff --git a/configs/configupgrade/testdata/valid/noop/want/versions.tf b/configs/configupgrade/testdata/valid/noop/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/noop/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/number-literals/input/number-literals.tf b/configs/configupgrade/testdata/valid/number-literals/input/number-literals.tf deleted file mode 100644 index efae79c65..000000000 --- a/configs/configupgrade/testdata/valid/number-literals/input/number-literals.tf +++ /dev/null @@ -1,7 +0,0 @@ -locals { - decimal_int = 1 - decimal_float = 1.5 - decimal_float_tricky = 0.1 - hex_int = 0xff - octal_int = 0777 -} diff --git a/configs/configupgrade/testdata/valid/number-literals/want/number-literals.tf b/configs/configupgrade/testdata/valid/number-literals/want/number-literals.tf deleted file mode 100644 index 19e7376ab..000000000 --- a/configs/configupgrade/testdata/valid/number-literals/want/number-literals.tf +++ /dev/null @@ -1,7 +0,0 @@ -locals { - decimal_int = 1 - decimal_float = 1.5 - decimal_float_tricky = 0.1 - hex_int = 255 - octal_int = 511 -} diff --git a/configs/configupgrade/testdata/valid/number-literals/want/versions.tf b/configs/configupgrade/testdata/valid/number-literals/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/number-literals/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/provider-addrs/input/provider-addrs.tf b/configs/configupgrade/testdata/valid/provider-addrs/input/provider-addrs.tf deleted file mode 100644 index 7be7dc8c7..000000000 --- a/configs/configupgrade/testdata/valid/provider-addrs/input/provider-addrs.tf +++ /dev/null @@ -1,16 +0,0 @@ -provider "test" { - alias = "baz" -} - -resource "test_instance" "foo" { - provider = "test.baz" -} - -module "bar" { - source = "./baz" - - providers = { - test = "test.baz" - test.foo = "test" - } -} diff --git a/configs/configupgrade/testdata/valid/provider-addrs/want/provider-addrs.tf b/configs/configupgrade/testdata/valid/provider-addrs/want/provider-addrs.tf deleted file mode 100644 index 729452c3c..000000000 --- a/configs/configupgrade/testdata/valid/provider-addrs/want/provider-addrs.tf +++ /dev/null @@ -1,16 +0,0 @@ -provider "test" { - alias = "baz" -} - -resource "test_instance" "foo" { - provider = test.baz -} - -module "bar" { - source = "./baz" - - providers = { - test = test.baz - test.foo = test - } -} diff --git a/configs/configupgrade/testdata/valid/provider-addrs/want/versions.tf b/configs/configupgrade/testdata/valid/provider-addrs/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/provider-addrs/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/provisioner/input/provisioner.tf b/configs/configupgrade/testdata/valid/provisioner/input/provisioner.tf deleted file mode 100644 index ee3973e86..000000000 --- a/configs/configupgrade/testdata/valid/provisioner/input/provisioner.tf +++ /dev/null @@ -1,19 +0,0 @@ -variable "login_username" {} - -resource "aws_instance" "foo" { - connection { - user = "${var.login_username}" - } - - provisioner "test" { - commands = "${list("a", "b", "c")}" - - when = "create" - on_failure = "fail" - - connection { - type = "winrm" - user = "${var.login_username}" - } - } -} diff --git a/configs/configupgrade/testdata/valid/provisioner/want/provisioner.tf b/configs/configupgrade/testdata/valid/provisioner/want/provisioner.tf deleted file mode 100644 index dea8065c0..000000000 --- a/configs/configupgrade/testdata/valid/provisioner/want/provisioner.tf +++ /dev/null @@ -1,23 +0,0 @@ -variable "login_username" { -} - -resource "aws_instance" "foo" { - connection { - host = coalesce(self.public_ip, self.private_ip) - type = "ssh" - user = var.login_username - } - - provisioner "test" { - commands = ["a", "b", "c"] - - when = create - on_failure = fail - - connection { - host = coalesce(self.public_ip, self.private_ip) - type = "winrm" - user = var.login_username - } - } -} diff --git a/configs/configupgrade/testdata/valid/provisioner/want/versions.tf b/configs/configupgrade/testdata/valid/provisioner/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/provisioner/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/redundant-list/input/redundant-list.tf b/configs/configupgrade/testdata/valid/redundant-list/input/redundant-list.tf deleted file mode 100644 index 670c9569d..000000000 --- a/configs/configupgrade/testdata/valid/redundant-list/input/redundant-list.tf +++ /dev/null @@ -1,64 +0,0 @@ -variable "listy" { - type = "list" -} - -resource "test_instance" "other" { - count = 2 -} - -resource "test_instance" "bad1" { - security_groups = ["${test_instance.other.*.id}"] -} - -resource "test_instance" "bad2" { - security_groups = ["${var.listy}"] -} - -resource "test_instance" "bad3" { - security_groups = ["${module.foo.outputs_always_dynamic}"] -} - -resource "test_instance" "bad4" { - security_groups = ["${list("a", "b", "c")}"] -} - -resource "test_instance" "bad5" { - security_groups = ["${test_instance.bad1.subnet_ids}"] # this one references a set -} - -resource "test_instance" "bad6" { - subnet_ids = ["${test_instance.bad1.security_groups}"] # this one defines a set -} - -resource "test_instance" "bad7" { - subnet_ids = ["${test_instance.bad1.*.id}"] # this one defines a set -} - -# The rest of these should keep the same amount of list-ness - -resource "test_instance" "ok1" { - security_groups = [] -} - -resource "test_instance" "ok2" { - security_groups = ["notalist"] -} - -resource "test_instance" "ok3" { - security_groups = ["${path.module}"] -} - -resource "test_instance" "ok4" { - security_groups = [["foo"], ["bar"]] -} - -resource "test_instance" "ok5" { - security_groups = "${test_instance.other.*.id}" -} - -resource "test_instance" "ok6" { - security_groups = [ - "${test_instance.other1.*.id}", - "${test_instance.other2.*.id}", - ] -} diff --git a/configs/configupgrade/testdata/valid/redundant-list/want/redundant-list.tf b/configs/configupgrade/testdata/valid/redundant-list/want/redundant-list.tf deleted file mode 100644 index 6edecb379..000000000 --- a/configs/configupgrade/testdata/valid/redundant-list/want/redundant-list.tf +++ /dev/null @@ -1,72 +0,0 @@ -variable "listy" { - type = list(string) -} - -resource "test_instance" "other" { - count = 2 -} - -resource "test_instance" "bad1" { - security_groups = test_instance.other.*.id -} - -resource "test_instance" "bad2" { - security_groups = var.listy -} - -resource "test_instance" "bad3" { - # TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to - # force an interpolation expression to be interpreted as a list by wrapping it - # in an extra set of list brackets. That form was supported for compatibility in - # v0.11, but is no longer supported in Terraform v0.12. - # - # If the expression in the following list itself returns a list, remove the - # brackets to avoid interpretation as a list of lists. If the expression - # returns a single list item then leave it as-is and remove this TODO comment. - security_groups = [module.foo.outputs_always_dynamic] -} - -resource "test_instance" "bad4" { - security_groups = ["a", "b", "c"] -} - -resource "test_instance" "bad5" { - security_groups = test_instance.bad1.subnet_ids # this one references a set -} - -resource "test_instance" "bad6" { - subnet_ids = test_instance.bad1.security_groups # this one defines a set -} - -resource "test_instance" "bad7" { - subnet_ids = test_instance.bad1.*.id # this one defines a set -} - -# The rest of these should keep the same amount of list-ness - -resource "test_instance" "ok1" { - security_groups = [] -} - -resource "test_instance" "ok2" { - security_groups = ["notalist"] -} - -resource "test_instance" "ok3" { - security_groups = [path.module] -} - -resource "test_instance" "ok4" { - security_groups = [["foo"], ["bar"]] -} - -resource "test_instance" "ok5" { - security_groups = test_instance.other.*.id -} - -resource "test_instance" "ok6" { - security_groups = [ - test_instance.other1.*.id, - test_instance.other2.*.id, - ] -} diff --git a/configs/configupgrade/testdata/valid/redundant-list/want/versions.tf b/configs/configupgrade/testdata/valid/redundant-list/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/redundant-list/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/relative-module-source/input/main.tf b/configs/configupgrade/testdata/valid/relative-module-source/input/main.tf deleted file mode 100644 index 804345838..000000000 --- a/configs/configupgrade/testdata/valid/relative-module-source/input/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -module "foo" { - source = "module" -} diff --git a/configs/configupgrade/testdata/valid/relative-module-source/input/module/main.tf b/configs/configupgrade/testdata/valid/relative-module-source/input/module/main.tf deleted file mode 100644 index 4fa198f17..000000000 --- a/configs/configupgrade/testdata/valid/relative-module-source/input/module/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "foo" { - value = "hello" -} diff --git a/configs/configupgrade/testdata/valid/relative-module-source/want/main.tf b/configs/configupgrade/testdata/valid/relative-module-source/want/main.tf deleted file mode 100644 index d5ec98f2b..000000000 --- a/configs/configupgrade/testdata/valid/relative-module-source/want/main.tf +++ /dev/null @@ -1,10 +0,0 @@ -module "foo" { - # TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to - # reference a relative module source without a preceding ./, but it is no - # longer supported in Terraform v0.12. - # - # If the below module source is indeed a relative local path, add ./ to the - # start of the source string. If that is not the case, then leave it as-is - # and remove this TODO comment. - source = "module" -} diff --git a/configs/configupgrade/testdata/valid/relative-module-source/want/module/main.tf b/configs/configupgrade/testdata/valid/relative-module-source/want/module/main.tf deleted file mode 100644 index 4fa198f17..000000000 --- a/configs/configupgrade/testdata/valid/relative-module-source/want/module/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "foo" { - value = "hello" -} diff --git a/configs/configupgrade/testdata/valid/relative-module-source/want/versions.tf b/configs/configupgrade/testdata/valid/relative-module-source/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/relative-module-source/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/rename-json-conflict/input/foo.tf b/configs/configupgrade/testdata/valid/rename-json-conflict/input/foo.tf deleted file mode 100644 index 0967ef424..000000000 --- a/configs/configupgrade/testdata/valid/rename-json-conflict/input/foo.tf +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/configs/configupgrade/testdata/valid/rename-json-conflict/input/foo.tf.json b/configs/configupgrade/testdata/valid/rename-json-conflict/input/foo.tf.json deleted file mode 100644 index 2e2809093..000000000 --- a/configs/configupgrade/testdata/valid/rename-json-conflict/input/foo.tf.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "terraform": {} -} diff --git a/configs/configupgrade/testdata/valid/rename-json-conflict/want/foo-1.tf.json b/configs/configupgrade/testdata/valid/rename-json-conflict/want/foo-1.tf.json deleted file mode 100644 index 0967ef424..000000000 --- a/configs/configupgrade/testdata/valid/rename-json-conflict/want/foo-1.tf.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/configs/configupgrade/testdata/valid/rename-json-conflict/want/foo.tf.json b/configs/configupgrade/testdata/valid/rename-json-conflict/want/foo.tf.json deleted file mode 100644 index 2e2809093..000000000 --- a/configs/configupgrade/testdata/valid/rename-json-conflict/want/foo.tf.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "terraform": {} -} diff --git a/configs/configupgrade/testdata/valid/rename-json-conflict/want/versions.tf b/configs/configupgrade/testdata/valid/rename-json-conflict/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/rename-json-conflict/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/rename-json/input/misnamed-json.tf b/configs/configupgrade/testdata/valid/rename-json/input/misnamed-json.tf deleted file mode 100644 index 2e2809093..000000000 --- a/configs/configupgrade/testdata/valid/rename-json/input/misnamed-json.tf +++ /dev/null @@ -1,3 +0,0 @@ -{ - "terraform": {} -} diff --git a/configs/configupgrade/testdata/valid/rename-json/want/misnamed-json.tf.json b/configs/configupgrade/testdata/valid/rename-json/want/misnamed-json.tf.json deleted file mode 100644 index 2e2809093..000000000 --- a/configs/configupgrade/testdata/valid/rename-json/want/misnamed-json.tf.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "terraform": {} -} diff --git a/configs/configupgrade/testdata/valid/rename-json/want/versions.tf b/configs/configupgrade/testdata/valid/rename-json/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/rename-json/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/resource-count-ref/input/resource-count-ref.tf b/configs/configupgrade/testdata/valid/resource-count-ref/input/resource-count-ref.tf deleted file mode 100644 index 6b4647c3b..000000000 --- a/configs/configupgrade/testdata/valid/resource-count-ref/input/resource-count-ref.tf +++ /dev/null @@ -1,29 +0,0 @@ -resource "test_instance" "one" { -} - -resource "test_instance" "many" { - count = 2 -} - -data "terraform_remote_state" "one" { -} - -data "terraform_remote_state" "many" { - count = 2 -} - -output "managed_one" { - value = "${test_instance.one.count}" -} - -output "managed_many" { - value = "${test_instance.many.count}" -} - -output "data_one" { - value = "${data.terraform_remote_state.one.count}" -} - -output "data_many" { - value = "${data.terraform_remote_state.many.count}" -} diff --git a/configs/configupgrade/testdata/valid/resource-count-ref/want/resource-count-ref.tf b/configs/configupgrade/testdata/valid/resource-count-ref/want/resource-count-ref.tf deleted file mode 100644 index a20b45d88..000000000 --- a/configs/configupgrade/testdata/valid/resource-count-ref/want/resource-count-ref.tf +++ /dev/null @@ -1,29 +0,0 @@ -resource "test_instance" "one" { -} - -resource "test_instance" "many" { - count = 2 -} - -data "terraform_remote_state" "one" { -} - -data "terraform_remote_state" "many" { - count = 2 -} - -output "managed_one" { - value = 1 -} - -output "managed_many" { - value = length(test_instance.many) -} - -output "data_one" { - value = 1 -} - -output "data_many" { - value = length(data.terraform_remote_state.many) -} diff --git a/configs/configupgrade/testdata/valid/resource-count-ref/want/versions.tf b/configs/configupgrade/testdata/valid/resource-count-ref/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/resource-count-ref/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/traversals/input/traversals.tf b/configs/configupgrade/testdata/valid/traversals/input/traversals.tf deleted file mode 100644 index ec2aa2ff5..000000000 --- a/configs/configupgrade/testdata/valid/traversals/input/traversals.tf +++ /dev/null @@ -1,41 +0,0 @@ -locals { - simple = "${test_instance.foo.bar}" - splat = "${test_instance.foo.*.bar}" - index = "${test_instance.foo.1.bar}" - - after_simple = "${test_instance.foo.bar.0.baz}" - after_splat = "${test_instance.foo.*.bar.0.baz}" - after_index = "${test_instance.foo.1.bar.2.baz}" - - non_ident_attr = "${test_instance.foo.bar.1baz}" - - remote_state_output = "${data.terraform_remote_state.foo.bar}" - remote_state_attr = "${data.terraform_remote_state.foo.backend}" - remote_state_idx_output = "${data.terraform_remote_state.foo.1.bar}" - remote_state_idx_attr = "${data.terraform_remote_state.foo.1.backend}" - remote_state_splat_output = "${data.terraform_remote_state.foo.*.bar}" - remote_state_splat_attr = "${data.terraform_remote_state.foo.*.backend}" - - has_index_should = "${test_instance.b.0.id}" - has_index_shouldnt = "${test_instance.c.0.id}" - no_index_should = "${test_instance.a.id}" - no_index_shouldnt = "${test_instance.c.id}" - - has_index_shouldnt_data = "${data.terraform_remote_state.foo.0.backend}" -} - -data "terraform_remote_state" "foo" { - # This is just here to make sure the schema for this gets loaded to - # support the remote_state_* checks above. -} - -resource "test_instance" "a" { - count = 1 -} - -resource "test_instance" "b" { - count = "${var.count}" -} - -resource "test_instance" "c" { -} diff --git a/configs/configupgrade/testdata/valid/traversals/want/traversals.tf b/configs/configupgrade/testdata/valid/traversals/want/traversals.tf deleted file mode 100644 index 80b24de21..000000000 --- a/configs/configupgrade/testdata/valid/traversals/want/traversals.tf +++ /dev/null @@ -1,41 +0,0 @@ -locals { - simple = test_instance.foo.bar - splat = test_instance.foo.*.bar - index = test_instance.foo[1].bar - - after_simple = test_instance.foo.bar[0].baz - after_splat = test_instance.foo.*.bar.0.baz - after_index = test_instance.foo[1].bar[2].baz - - non_ident_attr = test_instance.foo.bar["1baz"] - - remote_state_output = data.terraform_remote_state.foo.outputs.bar - remote_state_attr = data.terraform_remote_state.foo.backend - remote_state_idx_output = data.terraform_remote_state.foo[1].outputs.bar - remote_state_idx_attr = data.terraform_remote_state.foo[1].backend - remote_state_splat_output = data.terraform_remote_state.foo.*.outputs.bar - remote_state_splat_attr = data.terraform_remote_state.foo.*.backend - - has_index_should = test_instance.b[0].id - has_index_shouldnt = test_instance.c.id - no_index_should = test_instance.a[0].id - no_index_shouldnt = test_instance.c.id - - has_index_shouldnt_data = data.terraform_remote_state.foo.backend -} - -data "terraform_remote_state" "foo" { - # This is just here to make sure the schema for this gets loaded to - # support the remote_state_* checks above. -} - -resource "test_instance" "a" { - count = 1 -} - -resource "test_instance" "b" { - count = var.count -} - -resource "test_instance" "c" { -} diff --git a/configs/configupgrade/testdata/valid/traversals/want/versions.tf b/configs/configupgrade/testdata/valid/traversals/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/traversals/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/testdata/valid/variable-type/input/variables.tf b/configs/configupgrade/testdata/valid/variable-type/input/variables.tf deleted file mode 100644 index 59b3c585d..000000000 --- a/configs/configupgrade/testdata/valid/variable-type/input/variables.tf +++ /dev/null @@ -1,15 +0,0 @@ -variable "s" { - type = "string" -} - -variable "l" { - type = "list" - - default = [] -} - -variable "m" { - type = "map" - - default = {} -} diff --git a/configs/configupgrade/testdata/valid/variable-type/want/variables.tf b/configs/configupgrade/testdata/valid/variable-type/want/variables.tf deleted file mode 100644 index 2059d9e2d..000000000 --- a/configs/configupgrade/testdata/valid/variable-type/want/variables.tf +++ /dev/null @@ -1,15 +0,0 @@ -variable "s" { - type = string -} - -variable "l" { - type = list(string) - - default = [] -} - -variable "m" { - type = map(string) - - default = {} -} diff --git a/configs/configupgrade/testdata/valid/variable-type/want/versions.tf b/configs/configupgrade/testdata/valid/variable-type/want/versions.tf deleted file mode 100644 index d9b6f790b..000000000 --- a/configs/configupgrade/testdata/valid/variable-type/want/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.12" -} diff --git a/configs/configupgrade/upgrade.go b/configs/configupgrade/upgrade.go deleted file mode 100644 index dc2511d33..000000000 --- a/configs/configupgrade/upgrade.go +++ /dev/null @@ -1,123 +0,0 @@ -package configupgrade - -import ( - "bytes" - "fmt" - - "github.com/hashicorp/terraform/tfdiags" - - hcl2 "github.com/hashicorp/hcl/v2" - hcl2write "github.com/hashicorp/hcl/v2/hclwrite" -) - -// Upgrade takes some input module sources and produces a new ModuleSources -// that should be equivalent to the input but use the configuration idioms -// associated with the new configuration loader. -// -// The result of this function will probably not be accepted by this function, -// because it will contain constructs that are known only to the new -// loader. -// -// The result may include additional files that were not present in the -// input. The result may also include nil entries for filenames that were -// present in the input, indicating that these files should be deleted. -// In particular, file renames are represented as a new entry accompanied -// by a nil entry for the old name. -// -// If the returned diagnostics contains errors, the caller should not write -// the resulting sources to disk since they will probably be incomplete. If -// only warnings are present then the files may be written to disk. Most -// warnings are also represented as "TF-UPGRADE-TODO:" comments in the -// generated source files so that users can visit them all and decide what to -// do with them. -func (u *Upgrader) Upgrade(input ModuleSources, dir string) (ModuleSources, tfdiags.Diagnostics) { - ret := make(ModuleSources) - var diags tfdiags.Diagnostics - - an, err := u.analyze(input) - if err != nil { - diags = diags.Append(err) - return ret, diags - } - an.ModuleDir = dir - - for name, src := range input { - ext := fileExt(name) - if ext == "" { - // This should never happen because we ignore files that don't - // have our conventional extensions during LoadModule, but we'll - // silently pass through such files assuming that the caller - // has been tampering with the sources map somehow. - ret[name] = src - continue - } - - isJSON := (ext == ".tf.json") - - // The legacy loader allowed JSON syntax inside files named just .tf, - // so we'll detect that case and rename them here so that the new - // loader will accept the JSON. However, JSON files are usually - // generated so we'll also generate a warning to the user to update - // whatever program generated the file to use the new name. - if !isJSON { - trimSrc := bytes.TrimSpace(src) - if len(trimSrc) > 0 && (trimSrc[0] == '{' || trimSrc[0] == '[') { - isJSON = true - - // Rename in the output - ret[name] = nil // mark for deletion - oldName := name - name = input.UnusedFilename(name + ".json") - ret[name] = src - - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagWarning, - Summary: "JSON configuration file was renamed", - Detail: fmt.Sprintf( - "The file %q appears to be in JSON format, so it was renamed to %q. If this file is generated by another program, that program must be updated to use this new name.", - oldName, name, - ), - }) - continue - } - } - - if isJSON { - // We don't do any automatic rewriting for JSON files, since they - // are usually generated and thus it's the generating program that - // needs to be updated, rather than its output. - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagWarning, - Summary: "JSON configuration file was not rewritten", - Detail: fmt.Sprintf( - "The JSON configuration file %q was skipped, because JSON files are assumed to be generated. The program that generated this file may need to be updated for changes to the configuration language.", - name, - ), - }) - ret[name] = src // unchanged - continue - } - - // TODO: Actually rewrite this .tf file. - result, fileDiags := u.upgradeNativeSyntaxFile(name, src, an) - diags = diags.Append(fileDiags) - if fileDiags.HasErrors() { - // Leave unchanged, then. - ret[name] = src - continue - } - - ret[name] = hcl2write.Format(result.Content) - } - - versionsName := ret.UnusedFilename("versions.tf") - ret[versionsName] = []byte(newVersionConstraint) - - return ret, diags -} - -const newVersionConstraint = ` -terraform { - required_version = ">= 0.12" -} -` diff --git a/configs/configupgrade/upgrade_body.go b/configs/configupgrade/upgrade_body.go deleted file mode 100644 index 0f02c69d9..000000000 --- a/configs/configupgrade/upgrade_body.go +++ /dev/null @@ -1,989 +0,0 @@ -package configupgrade - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - - hcl1ast "github.com/hashicorp/hcl/hcl/ast" - hcl1token "github.com/hashicorp/hcl/hcl/token" - hcl2 "github.com/hashicorp/hcl/v2" - hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/lang/blocktoattr" - "github.com/hashicorp/terraform/registry/regsrc" - "github.com/hashicorp/terraform/terraform" - "github.com/hashicorp/terraform/tfdiags" - "github.com/zclconf/go-cty/cty" -) - -// bodyContentRules is a mapping from item names (argument names and block type -// names) to a "rule" function defining what to do with an item of that type. -type bodyContentRules map[string]bodyItemRule - -// bodyItemRule is just a function to write an upgraded representation of a -// particular given item to the given buffer. This is generic to handle various -// different mapping rules, though most values will be those constructed by -// other helper functions below. -type bodyItemRule func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics - -func normalAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule { - exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { - return upgradeExpr(val, filename, true, an) - } - return attributeRule(filename, wantTy, an, exprRule) -} - -func noInterpAttributeRule(filename string, wantTy cty.Type, an *analysis) bodyItemRule { - exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { - return upgradeExpr(val, filename, false, an) - } - return attributeRule(filename, wantTy, an, exprRule) -} - -func maybeBareKeywordAttributeRule(filename string, an *analysis, specials map[string]string) bodyItemRule { - exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { - // If the expression is a literal that would be valid as a naked keyword - // then we'll turn it into one. - if lit, isLit := val.(*hcl1ast.LiteralType); isLit { - if lit.Token.Type == hcl1token.STRING { - kw := lit.Token.Value().(string) - if hcl2syntax.ValidIdentifier(kw) { - - // If we have a special mapping rule for this keyword, - // we'll let that override what the user gave. - if override := specials[kw]; override != "" { - kw = override - } - - return []byte(kw), nil - } - } - } - - return upgradeExpr(val, filename, false, an) - } - return attributeRule(filename, cty.String, an, exprRule) -} - -func maybeBareTraversalAttributeRule(filename string, an *analysis) bodyItemRule { - exprRule := func(val interface{}) ([]byte, tfdiags.Diagnostics) { - return upgradeTraversalExpr(val, filename, an) - } - return attributeRule(filename, cty.String, an, exprRule) -} - -func dependsOnAttributeRule(filename string, an *analysis) bodyItemRule { - return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - val, ok := item.Val.(*hcl1ast.ListType) - if !ok { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid depends_on argument", - Detail: `The "depends_on" argument must be a list of strings containing references to resources and modules.`, - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - return diags - } - - var exprBuf bytes.Buffer - multiline := len(val.List) > 1 - exprBuf.WriteByte('[') - if multiline { - exprBuf.WriteByte('\n') - } - for _, node := range val.List { - lit, ok := node.(*hcl1ast.LiteralType) - if (!ok) || lit.Token.Type != hcl1token.STRING { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid depends_on argument", - Detail: `The "depends_on" argument must be a list of strings containing references to resources and modules.`, - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - continue - } - refStr := lit.Token.Value().(string) - if refStr == "" { - continue - } - refParts := strings.Split(refStr, ".") - var maxNames int - switch refParts[0] { - case "data", "module": - maxNames = 3 - default: // resource references - maxNames = 2 - } - - exprBuf.WriteString(refParts[0]) - for i, part := range refParts[1:] { - if part == "*" { - // We used to allow test_instance.foo.* as a reference - // but now that's expressed instead as test_instance.foo, - // referring to the tuple of instances. This also - // always marks the end of the reference part of the - // traversal, so anything after this would be resource - // attributes that don't belong on depends_on. - break - } - if i, err := strconv.Atoi(part); err == nil { - fmt.Fprintf(&exprBuf, "[%d]", i) - // An index always marks the end of the reference part. - break - } - if (i + 1) >= maxNames { - // We've reached the end of the reference part, so anything - // after this would be invalid in 0.12. - break - } - exprBuf.WriteByte('.') - exprBuf.WriteString(part) - } - - if multiline { - exprBuf.WriteString(",\n") - } - } - exprBuf.WriteByte(']') - - printAttribute(buf, item.Keys[0].Token.Value().(string), exprBuf.Bytes(), item.LineComment) - - return diags - } -} - -func attributeRule(filename string, wantTy cty.Type, an *analysis, exprRule func(val interface{}) ([]byte, tfdiags.Diagnostics)) bodyItemRule { - return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - name := item.Keys[0].Token.Value().(string) - - // We'll tolerate a block with no labels here as a degenerate - // way to assign a map, but we can't migrate a block that has - // labels. In practice this should never happen because - // nested blocks in resource blocks did not accept labels - // prior to v0.12. - if len(item.Keys) != 1 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Block where attribute was expected", - Detail: fmt.Sprintf("Within %s the name %q is an attribute name, not a block type.", blockAddr, name), - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - return diags - } - - val := item.Val - - if typeIsSettableFromTupleCons(wantTy) && !typeIsSettableFromTupleCons(wantTy.ElementType()) { - // In Terraform circa 0.10 it was required to wrap any expression - // that produces a list in HCL list brackets to allow type analysis - // to complete successfully, even though logically that ought to - // have produced a list of lists. - // - // In Terraform 0.12 this construct _does_ produce a list of lists, so - // we need to update expressions that look like older usage. We can't - // do this exactly with static analysis, but we can make a best-effort - // and produce a warning if type inference is impossible for a - // particular expression. This should give good results for common - // simple examples, like splat expressions. - // - // There are four possible cases here: - // - The value isn't an HCL1 list expression at all, or is one that - // contains more than one item, in which case this special case - // does not apply. - // - The inner expression after upgrading can be proven to return - // a sequence type, in which case we must definitely remove - // the wrapping brackets. - // - The inner expression after upgrading can be proven to return - // a non-sequence type, in which case we fall through and treat - // the whole item like a normal expression. - // - Static type analysis is impossible (it returns cty.DynamicPseudoType), - // in which case we will make no changes but emit a warning and - // a TODO comment for the user to decide whether a change needs - // to be made in practice. - if list, ok := val.(*hcl1ast.ListType); ok { - if len(list.List) == 1 { - maybeAlsoList := list.List[0] - if exprSrc, diags := upgradeExpr(maybeAlsoList, filename, true, an); !diags.HasErrors() { - // Ideally we would set "self" here but we don't have - // enough context to set it and in practice not setting - // it only affects expressions inside provisioner and - // connection blocks, and the list-wrapping thing isn't - // common there. - gotTy := an.InferExpressionType(exprSrc, nil) - if typeIsSettableFromTupleCons(gotTy) { - // Since this expression was already inside HCL list brackets, - // the ultimate result would be a list of lists and so we - // need to unwrap it by taking just the portion within - // the brackets here. - val = maybeAlsoList - } - if gotTy == cty.DynamicPseudoType { - // User must decide. - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Possible legacy dynamic list usage", - Detail: "This list may be using the legacy redundant-list expression style from Terraform v0.10 and earlier. If the expression within these brackets returns a list itself, remove these brackets.", - Subject: hcl1PosRange(filename, list.Lbrack).Ptr(), - }) - buf.WriteString( - "# TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to\n" + - "# force an interpolation expression to be interpreted as a list by wrapping it\n" + - "# in an extra set of list brackets. That form was supported for compatibility in\n" + - "# v0.11, but is no longer supported in Terraform v0.12.\n" + - "#\n" + - "# If the expression in the following list itself returns a list, remove the\n" + - "# brackets to avoid interpretation as a list of lists. If the expression\n" + - "# returns a single list item then leave it as-is and remove this TODO comment.\n", - ) - } - } - } - } - } - - valSrc, valDiags := exprRule(val) - diags = diags.Append(valDiags) - printAttribute(buf, item.Keys[0].Token.Value().(string), valSrc, item.LineComment) - - return diags - } -} - -func nestedBlockRule(filename string, nestedRules bodyContentRules, an *analysis, adhocComments *commentQueue) bodyItemRule { - return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - // This simpler nestedBlockRule is for contexts where the special - // "dynamic" block type is not accepted and so only HCL1 object - // constructs can be accepted. Attempts to assign arbitrary HIL - // expressions will be rejected as errors. - - var diags tfdiags.Diagnostics - declRange := hcl1PosRange(filename, item.Keys[0].Pos()) - blockType := item.Keys[0].Token.Value().(string) - labels := make([]string, len(item.Keys)-1) - for i, key := range item.Keys[1:] { - labels[i] = key.Token.Value().(string) - } - - var blockItems []*hcl1ast.ObjectType - - switch val := item.Val.(type) { - - case *hcl1ast.ObjectType: - blockItems = []*hcl1ast.ObjectType{val} - - case *hcl1ast.ListType: - for _, node := range val.List { - switch listItem := node.(type) { - case *hcl1ast.ObjectType: - blockItems = append(blockItems, listItem) - default: - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid value for nested block", - Detail: fmt.Sprintf("In %s the name %q is a nested block type, so any value assigned to it must be an object.", blockAddr, blockType), - Subject: hcl1PosRange(filename, node.Pos()).Ptr(), - }) - } - } - - default: - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid value for nested block", - Detail: fmt.Sprintf("In %s the name %q is a nested block type, so any value assigned to it must be an object.", blockAddr, blockType), - Subject: &declRange, - }) - return diags - } - - for _, blockItem := range blockItems { - printBlockOpen(buf, blockType, labels, item.LineComment) - bodyDiags := upgradeBlockBody( - filename, fmt.Sprintf("%s.%s", blockAddr, blockType), buf, - blockItem.List.Items, blockItem.Rbrace, nestedRules, adhocComments, - ) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n") - } - - return diags - } -} - -func nestedBlockRuleWithDynamic(filename string, nestedRules bodyContentRules, nestedSchema *configschema.NestedBlock, emptyAsAttr bool, an *analysis, adhocComments *commentQueue) bodyItemRule { - return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - // In Terraform v0.11 it was possible in some cases to trick Terraform - // and providers into accepting HCL's attribute syntax and some HIL - // expressions in places where blocks or sequences of blocks were - // expected, since the information about the heritage of the values - // was lost during decoding and interpolation. - // - // In order to avoid all of the weird rough edges that resulted from - // those misinterpretations, Terraform v0.12 is stricter and requires - // the use of block syntax for blocks in all cases. However, because - // various abuses of attribute syntax _did_ work (with some caveats) - // in v0.11 we will upgrade them as best we can to use proper block - // syntax. - // - // There are a few different permutations supported by this code: - // - // - Assigning a single HCL1 "object" using attribute syntax. This is - // straightforward to migrate just by dropping the equals sign. - // - // - Assigning a HCL1 list of objects using attribute syntax. Each - // object in that list can be translated to a separate block. - // - // - Assigning a HCL1 list containing HIL expressions that evaluate - // to maps. This is a hard case because we can't know the internal - // structure of those maps during static analysis, and so we must - // generate a worst-case dynamic block structure for it. - // - // - Assigning a single HIL expression that evaluates to a list of - // maps. This is just like the previous case except additionally - // we cannot even predict the number of generated blocks, so we must - // generate a single "dynamic" block to iterate over the list at - // runtime. - - var diags tfdiags.Diagnostics - blockType := item.Keys[0].Token.Value().(string) - labels := make([]string, len(item.Keys)-1) - for i, key := range item.Keys[1:] { - labels[i] = key.Token.Value().(string) - } - - var blockItems []hcl1ast.Node - - switch val := item.Val.(type) { - - case *hcl1ast.ObjectType: - blockItems = append(blockItems, val) - - case *hcl1ast.ListType: - for _, node := range val.List { - switch listItem := node.(type) { - case *hcl1ast.ObjectType: - blockItems = append(blockItems, listItem) - default: - // We're going to cheat a bit here and construct a synthetic - // HCL1 list just because that makes our logic - // simpler below where we can just treat all non-objects - // in the same way when producing "dynamic" blocks. - synthList := &hcl1ast.ListType{ - List: []hcl1ast.Node{listItem}, - Lbrack: listItem.Pos(), - Rbrack: hcl1NodeEndPos(listItem), - } - blockItems = append(blockItems, synthList) - } - } - - default: - blockItems = append(blockItems, item.Val) - } - - if len(blockItems) == 0 && emptyAsAttr { - // Terraform v0.12's config decoder allows using block syntax for - // certain attribute types, which we prefer as idiomatic usage - // causing us to end up in this function in such cases, but as - // a special case users can still use the attribute syntax to - // explicitly write an empty list. For more information, see - // the lang/blocktoattr package. - printAttribute(buf, item.Keys[0].Token.Value().(string), []byte{'[', ']'}, item.LineComment) - return diags - } - - for _, blockItem := range blockItems { - switch ti := blockItem.(type) { - case *hcl1ast.ObjectType: - // If we have an object then we'll pass through its content - // as a block directly. This is the most straightforward mapping - // from the source input, since we know exactly which keys - // are present. - printBlockOpen(buf, blockType, labels, item.LineComment) - bodyDiags := upgradeBlockBody( - filename, fmt.Sprintf("%s.%s", blockAddr, blockType), buf, - ti.List.Items, ti.Rbrace, nestedRules, adhocComments, - ) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n") - default: - // For any other sort of value we can't predict what shape it - // will have at runtime, so we must generate a very conservative - // "dynamic" block that tries to assign everything from the - // schema. The result of this is likely to be pretty ugly. - printBlockOpen(buf, "dynamic", []string{blockType}, item.LineComment) - eachSrc, eachDiags := upgradeExpr(blockItem, filename, true, an) - diags = diags.Append(eachDiags) - printAttribute(buf, "for_each", eachSrc, nil) - if nestedSchema.Nesting == configschema.NestingMap { - // This is a pretty odd situation since map-based blocks - // didn't exist prior to Terraform v0.12, but we'll support - // this anyway in case we decide to add support in a later - // SDK release that is still somehow compatible with - // Terraform v0.11. - printAttribute(buf, "labels", []byte(fmt.Sprintf(`[%s.key]`, blockType)), nil) - } - printBlockOpen(buf, "content", nil, nil) - buf.WriteString("# TF-UPGRADE-TODO: The automatic upgrade tool can't predict\n") - buf.WriteString("# which keys might be set in maps assigned here, so it has\n") - buf.WriteString("# produced a comprehensive set here. Consider simplifying\n") - buf.WriteString("# this after confirming which keys can be set in practice.\n\n") - printDynamicBlockBody(buf, blockType, &nestedSchema.Block) - buf.WriteString("}\n") - buf.WriteString("}\n") - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagWarning, - Summary: "Approximate migration of invalid block type assignment", - Detail: fmt.Sprintf("In %s the name %q is a nested block type, but this configuration is exploiting some missing validation rules from Terraform v0.11 and prior to trick Terraform into creating blocks dynamically.\n\nThis has been upgraded to use the new Terraform v0.12 dynamic blocks feature, but since the upgrade tool cannot predict which map keys will be present a fully-comprehensive set has been generated.", blockAddr, blockType), - Subject: hcl1PosRange(filename, blockItem.Pos()).Ptr(), - }) - } - } - - return diags - } -} - -// schemaDefaultBodyRules constructs standard body content rules for the given -// schema. Each call is guaranteed to produce a distinct object so that -// callers can safely mutate the result in order to impose custom rules -// in addition to or instead of those created by default, for situations -// where schema-based and predefined items mix in a single body. -func schemaDefaultBodyRules(filename string, schema *configschema.Block, an *analysis, adhocComments *commentQueue) bodyContentRules { - ret := make(bodyContentRules) - if schema == nil { - // Shouldn't happen in any real case, but often crops up in tests - // where the mock schemas tend to be incomplete. - return ret - } - - for name, attrS := range schema.Attributes { - if aty := attrS.Type; blocktoattr.TypeCanBeBlocks(aty) { - // Terraform's standard body processing rules for arbitrary schemas - // have a special case where list-of-object or set-of-object - // attributes can be specified as a sequence of nested blocks - // instead of a single list attribute. We prefer that form during - // upgrade for historical reasons, to avoid making large changes - // to existing configurations that were following documented idiom. - synthSchema := blocktoattr.SchemaForCtyContainerType(aty) - nestedRules := schemaDefaultBodyRules(filename, &synthSchema.Block, an, adhocComments) - ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, synthSchema, true, an, adhocComments) - continue - } - ret[name] = normalAttributeRule(filename, attrS.Type, an) - } - for name, blockS := range schema.BlockTypes { - nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an, adhocComments) - ret[name] = nestedBlockRuleWithDynamic(filename, nestedRules, blockS, false, an, adhocComments) - } - - return ret -} - -// schemaNoInterpBodyRules constructs standard body content rules for the given -// schema. Each call is guaranteed to produce a distinct object so that -// callers can safely mutate the result in order to impose custom rules -// in addition to or instead of those created by default, for situations -// where schema-based and predefined items mix in a single body. -func schemaNoInterpBodyRules(filename string, schema *configschema.Block, an *analysis, adhocComments *commentQueue) bodyContentRules { - ret := make(bodyContentRules) - if schema == nil { - // Shouldn't happen in any real case, but often crops up in tests - // where the mock schemas tend to be incomplete. - return ret - } - - for name, attrS := range schema.Attributes { - ret[name] = noInterpAttributeRule(filename, attrS.Type, an) - } - for name, blockS := range schema.BlockTypes { - nestedRules := schemaDefaultBodyRules(filename, &blockS.Block, an, adhocComments) - ret[name] = nestedBlockRule(filename, nestedRules, an, adhocComments) - } - - return ret -} - -// justAttributesBodyRules constructs body content rules that just use the -// standard interpolated attribute mapping for every name already present -// in the given body object. -// -// This is a little weird vs. just processing directly the attributes, but -// has the advantage that the caller can then apply overrides to the result -// as necessary to deal with any known names that need special handling. -// -// Any attribute rules created by this function do not have a specific wanted -// value type specified, instead setting it to just cty.DynamicPseudoType. -func justAttributesBodyRules(filename string, body *hcl1ast.ObjectType, an *analysis) bodyContentRules { - rules := make(bodyContentRules, len(body.List.Items)) - args := body.List.Items - for _, arg := range args { - name := arg.Keys[0].Token.Value().(string) - rules[name] = normalAttributeRule(filename, cty.DynamicPseudoType, an) - } - return rules -} - -func lifecycleBlockBodyRules(filename string, an *analysis) bodyContentRules { - return bodyContentRules{ - "create_before_destroy": noInterpAttributeRule(filename, cty.Bool, an), - "prevent_destroy": noInterpAttributeRule(filename, cty.Bool, an), - "ignore_changes": func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - val, ok := item.Val.(*hcl1ast.ListType) - if !ok { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid ignore_changes argument", - Detail: `The "ignore_changes" argument must be a list of attribute expressions relative to this resource.`, - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - return diags - } - - // As a special case, we'll map the single-element list ["*"] to - // the new keyword "all". - if len(val.List) == 1 { - if lit, ok := val.List[0].(*hcl1ast.LiteralType); ok { - if lit.Token.Value() == "*" { - printAttribute(buf, item.Keys[0].Token.Value().(string), []byte("all"), item.LineComment) - return diags - } - } - } - - var exprBuf bytes.Buffer - multiline := len(val.List) > 1 - exprBuf.WriteByte('[') - if multiline { - exprBuf.WriteByte('\n') - } - for _, node := range val.List { - itemSrc, moreDiags := upgradeTraversalExpr(node, filename, an) - diags = diags.Append(moreDiags) - exprBuf.Write(itemSrc) - if multiline { - exprBuf.WriteString(",\n") - } - } - exprBuf.WriteByte(']') - - printAttribute(buf, item.Keys[0].Token.Value().(string), exprBuf.Bytes(), item.LineComment) - - return diags - }, - } -} - -func provisionerBlockRule(filename string, resourceType string, an *analysis, adhocComments *commentQueue) bodyItemRule { - // Unlike some other examples above, this is a rule for the entire - // provisioner block, rather than just for its contents. Therefore it must - // also produce the block header and body delimiters. - return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - body := item.Val.(*hcl1ast.ObjectType) - declRange := hcl1PosRange(filename, item.Keys[0].Pos()) - - if len(item.Keys) < 2 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid provisioner block", - Detail: "A provisioner block must have one label: the provisioner type.", - Subject: &declRange, - }) - return diags - } - - typeName := item.Keys[1].Token.Value().(string) - schema := an.ProvisionerSchemas[typeName] - if schema == nil { - // This message is assuming that if the user _is_ using a third-party - // provisioner plugin they already know how to install it for normal - // use and so we don't need to spell out those instructions in detail - // here. - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Unknown provisioner type", - Detail: fmt.Sprintf("The provisioner type %q is not supported. If this is a third-party plugin, make sure its plugin executable is available in one of the usual plugin search paths.", typeName), - Subject: &declRange, - }) - return diags - } - - rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) - rules["when"] = maybeBareTraversalAttributeRule(filename, an) - rules["on_failure"] = maybeBareTraversalAttributeRule(filename, an) - rules["connection"] = connectionBlockRule(filename, resourceType, an, adhocComments) - - printComments(buf, item.LeadComment) - printBlockOpen(buf, "provisioner", []string{typeName}, item.LineComment) - bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("%s.provisioner[%q]", blockAddr, typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n") - - return diags - } -} - -func connectionBlockRule(filename string, resourceType string, an *analysis, adhocComments *commentQueue) bodyItemRule { - // Unlike some other examples above, this is a rule for the entire - // connection block, rather than just for its contents. Therefore it must - // also produce the block header and body delimiters. - return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - body := item.Val.(*hcl1ast.ObjectType) - - // TODO: For the few resource types that were setting ConnInfo in - // state after create/update in prior versions, generate the additional - // explicit connection settings that are now required if and only if - // there's at least one provisioner block. - // For now, we just pass this through as-is. - - schema := terraform.ConnectionBlockSupersetSchema() - rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) - rules["type"] = noInterpAttributeRule(filename, cty.String, an) // type is processed early in the config loader, so cannot interpolate - - printComments(buf, item.LeadComment) - printBlockOpen(buf, "connection", nil, item.LineComment) - - // Terraform 0.12 no longer supports "magical" configuration of defaults - // in the connection block from logic in the provider because explicit - // is better than implicit, but for backward-compatibility we'll populate - // an existing connection block with any settings that would've been - // previously set automatically for a set of instance types we know - // had this behavior in versions prior to the v0.12 release. - if defaults := resourceTypeAutomaticConnectionExprs[resourceType]; len(defaults) > 0 { - names := make([]string, 0, len(defaults)) - for name := range defaults { - names = append(names, name) - } - sort.Strings(names) - for _, name := range names { - exprSrc := defaults[name] - if existing := body.List.Filter(name); len(existing.Items) > 0 { - continue // Existing explicit value, so no need for a default - } - printAttribute(buf, name, []byte(exprSrc), nil) - } - } - - bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("%s.connection", blockAddr), buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n") - - return diags - } -} - -func moduleSourceRule(filename string, an *analysis) bodyItemRule { - return func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - val, ok := item.Val.(*hcl1ast.LiteralType) - if !ok { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid source argument", - Detail: `The "source" argument must be a single string containing the module source.`, - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - return diags - } - if val.Token.Type != hcl1token.STRING { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid source argument", - Detail: `The "source" argument must be a single string containing the module source.`, - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - return diags - } - - litVal := val.Token.Value().(string) - - if isMaybeRelativeLocalPath(litVal, an.ModuleDir) { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagWarning, - Summary: "Possible relative module source", - Detail: "Terraform cannot determine the given module source, but it appears to be a relative path", - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - buf.WriteString( - "# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to\n" + - "# reference a relative module source without a preceding ./, but it is no\n" + - "# longer supported in Terraform v0.12.\n" + - "#\n" + - "# If the below module source is indeed a relative local path, add ./ to the\n" + - "# start of the source string. If that is not the case, then leave it as-is\n" + - "# and remove this TODO comment.\n", - ) - } - newVal, exprDiags := upgradeExpr(val, filename, false, an) - diags = diags.Append(exprDiags) - buf.WriteString("source = " + string(newVal) + "\n") - return diags - } -} - -// Prior to Terraform 0.12 providers were able to supply default connection -// settings that would partially populate the "connection" block with -// automatically-selected values. -// -// In practice, this feature was often confusing in that the provider would not -// have enough information to select a suitable host address or protocol from -// multiple possible options and so would just make an arbitrary decision. -// -// With our principle of "explicit is better than implicit", as of Terraform 0.12 -// we now require all connection settings to be configured explicitly by the -// user so that it's clear and explicit in the configuration which protocol and -// IP address are being selected. To avoid generating errors immediately after -// upgrade, though, we'll make a best effort to populate something functionally -// equivalent to what the provider would've done automatically for any resource -// types we know about in this table. -// -// The leaf values in this data structure are raw expressions to be inserted, -// and so they must use valid expression syntax as understood by Terraform 0.12. -// They should generally be expressions using only constant values or expressions -// in terms of attributes accessed via the special "self" object. These should -// mimic as closely as possible the logic that the provider itself used to -// implement. -// -// NOTE: Because provider releases are independent from Terraform Core releases, -// there could potentially be new 0.11-compatible provider releases that -// introduce new uses of default connection info that this map doesn't know -// about. The upgrade tool will not handle these, and so we will advise -// provider developers that this mechanism is not to be used for any new -// resource types, even in 0.11 mode. -var resourceTypeAutomaticConnectionExprs = map[string]map[string]string{ - "aws_instance": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.public_ip, self.private_ip)`, - }, - "aws_spot_instance_request": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.public_ip, self.private_ip)`, - "user": `self.username != "" ? self.username : null`, - "password": `self.password != "" ? self.password : null`, - }, - "azure_instance": map[string]string{ - "type": `"ssh" # TF-UPGRADE-TODO: If this is a windows instance without an SSH server, change to "winrm"`, - "host": `coalesce(self.vip_address, self.ip_address)`, - }, - "azurerm_virtual_machine": map[string]string{ - "type": `"ssh" # TF-UPGRADE-TODO: If this is a windows instance without an SSH server, change to "winrm"`, - // The azurerm_virtual_machine resource type does not expose a remote - // access IP address directly, instead requring the user to separately - // fetch the network interface. - // (If we can add a "default_ssh_ip" or similar attribute to this - // resource type before its first 0.12-compatible release then we should - // update this to use that instead, for simplicity's sake.) - "host": `"" # TF-UPGRADE-TODO: Set this to the IP address of the machine's primary network interface`, - }, - "brightbox_server": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.public_hostname, self.ipv6_hostname, self.fqdn)`, - }, - "cloudscale_server": map[string]string{ - "type": `"ssh"`, - // The logic for selecting this is pretty complicated for this resource - // type, and the result is not exposed as an isolated attribute, so - // the conversion here is a little messy. We include newlines in this - // one so that the auto-formatter can indent it nicely for readability. - // NOTE: In v1.0.1 of this provider (the latest at the time of this - // writing) it has an possible bug where it selects _public_ IPv4 - // addresses but _private_ IPv6 addresses. That behavior is followed - // here to maximize compatibility with existing configurations. - "host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible. - concat( - flatten([ - for i in self.network_interface : [ - for a in i.addresses : a.address - if a.version == 4 - ] - if i.type == "public" - ]), - flatten([ - for i in self.network_interface : [ - for a in i.addresses : a.address - if a.version == 6 - ] - if i.type == "private" - ]), - )... - )`, - }, - "cloudstack_instance": map[string]string{ - "type": `"ssh"`, - "host": `self.ip_address`, - }, - "digitalocean_droplet": map[string]string{ - "type": `"ssh"`, - "host": `self.ipv4_address`, - }, - "flexibleengine_compute_bms_server_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "flexibleengine_compute_instance_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "google_compute_instance": map[string]string{ - "type": `"ssh"`, - // The logic for selecting this is pretty complicated for this resource - // type, and the result is not exposed as an isolated attribute, so - // the conversion here is a little messy. We include newlines in this - // one so that the auto-formatter can indent it nicely for readability. - // (If we can add a "default_ssh_ip" or similar attribute to this - // resource type before its first 0.12-compatible release then we should - // update this to use that instead, for simplicity's sake.) - "host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible. - concat( - # Prefer any available NAT IP address - flatten([ - for ni in self.network_interface : [ - for ac in ni.access_config : ac.nat_ip - ] - ]), - - # Otherwise, use the first available LAN IP address - [ - for ni in self.network_interface : ni.network_ip - ], - )... - )`, - }, - "hcloud_server": map[string]string{ - "type": `"ssh"`, - "host": `self.ipv4_address`, - }, - "huaweicloud_compute_instance_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "linode_instance": map[string]string{ - "type": `"ssh"`, - "host": `self.ipv4[0]`, - }, - "oneandone_baremetal_server": map[string]string{ - "type": `ssh`, - "host": `self.ips[0].ip`, - "password": `self.password != "" ? self.password : null`, - "private_key": `self.ssh_key_path != "" ? file(self.ssh_key_path) : null`, - }, - "oneandone_server": map[string]string{ - "type": `ssh`, - "host": `self.ips[0].ip`, - "password": `self.password != "" ? self.password : null`, - "private_key": `self.ssh_key_path != "" ? file(self.ssh_key_path) : null`, - }, - "openstack_compute_instance_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "opentelekomcloud_compute_bms_server_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "opentelekomcloud_compute_instance_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "packet_device": map[string]string{ - "type": `"ssh"`, - "host": `self.access_public_ipv4`, - }, - "profitbricks_server": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.primary_nic.ips...)`, - // The value for this isn't exported anywhere on the object, so we'll - // need to have the user fix it up manually. - "password": `"" # TF-UPGRADE-TODO: set this to a suitable value, such as the boot image password`, - }, - "scaleway_server": map[string]string{ - "type": `"ssh"`, - "host": `self.public_ip`, - }, - "telefonicaopencloud_compute_bms_server_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "telefonicaopencloud_compute_instance_v2": map[string]string{ - "type": `"ssh"`, - "host": `coalesce(self.access_ip_v4, self.access_ip_v6)`, - }, - "triton_machine": map[string]string{ - "type": `"ssh"`, - "host": `self.primaryip`, // convention would call for this to be named "primary_ip", but "primaryip" is the name this resource type uses - }, - "vsphere_virtual_machine": map[string]string{ - "type": `"ssh"`, - "host": `self.default_ip_address`, - }, - "yandex_compute_instance": map[string]string{ - "type": `"ssh"`, - // The logic for selecting this is pretty complicated for this resource - // type, and the result is not exposed as an isolated attribute, so - // the conversion here is a little messy. We include newlines in this - // one so that the auto-formatter can indent it nicely for readability. - "host": `coalesce( # TF-UPGRADE-TODO: Simplify this to reference a specific desired IP address, if possible. - concat( - # Prefer any available NAT IP address - for i in self.network_interface: [ - i.nat_ip_address - ], - - # Otherwise, use the first available internal IP address - for i in self.network_interface: [ - i.ip_address - ], - )... - )`, - }, -} - -// copied directly from internal/initwd/getter.go -var localSourcePrefixes = []string{ - "./", - "../", - ".\\", - "..\\", -} - -// isMaybeRelativeLocalPath tries to catch situations where a module source is -// an improperly-referenced relative path, such as "module" instead of -// "./module". This is a simple check that could return a false positive in the -// unlikely-yet-plausible case that a module source is for eg. a github -// repository that also looks exactly like an existing relative path. This -// should only be used to return a warning. -func isMaybeRelativeLocalPath(addr, dir string) bool { - for _, prefix := range localSourcePrefixes { - if strings.HasPrefix(addr, prefix) { - // it is _definitely_ a relative path - return false - } - } - - _, err := regsrc.ParseModuleSource(addr) - if err == nil { - // it is a registry source - return false - } - - possibleRelPath := filepath.Join(dir, addr) - _, err = os.Stat(possibleRelPath) - if err == nil { - // If there is no error, something exists at what would be the relative - // path, if the module source started with ./ - return true - } - - return false -} diff --git a/configs/configupgrade/upgrade_expr.go b/configs/configupgrade/upgrade_expr.go deleted file mode 100644 index a38ee284d..000000000 --- a/configs/configupgrade/upgrade_expr.go +++ /dev/null @@ -1,951 +0,0 @@ -package configupgrade - -import ( - "bytes" - "fmt" - "log" - "strconv" - "strings" - - hcl2 "github.com/hashicorp/hcl/v2" - hcl2syntax "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/zclconf/go-cty/cty" - - hcl1ast "github.com/hashicorp/hcl/hcl/ast" - hcl1printer "github.com/hashicorp/hcl/hcl/printer" - hcl1token "github.com/hashicorp/hcl/hcl/token" - - "github.com/hashicorp/hil" - hilast "github.com/hashicorp/hil/ast" - - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/tfdiags" -) - -func upgradeExpr(val interface{}, filename string, interp bool, an *analysis) ([]byte, tfdiags.Diagnostics) { - var buf bytes.Buffer - var diags tfdiags.Diagnostics - - // "val" here can be either a hcl1ast.Node or a hilast.Node, since both - // of these correspond to expressions in HCL2. Therefore we need to - // comprehensively handle every possible HCL1 *and* HIL AST node type - // and, at minimum, print it out as-is in HCL2 syntax. -Value: - switch tv := val.(type) { - - case *hcl1ast.LiteralType: - return upgradeExpr(tv.Token, filename, interp, an) - - case hcl1token.Token: - switch tv.Type { - case hcl1token.STRING: - litVal := tv.Value() - if !interp { - // Easy case, then. - printQuotedString(&buf, litVal.(string)) - break - } - - hilNode, err := hil.Parse(litVal.(string)) - if err != nil { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid interpolated string", - Detail: fmt.Sprintf("Interpolation parsing failed: %s", err), - Subject: hcl1PosRange(filename, tv.Pos).Ptr(), - }) - return nil, diags - } - - interpSrc, interpDiags := upgradeExpr(hilNode, filename, interp, an) - buf.Write(interpSrc) - diags = diags.Append(interpDiags) - - case hcl1token.HEREDOC: - // HCL1's "Value" method for tokens pulls out the body and removes - // any indents in the source for a flush heredoc, which throws away - // information we need to upgrade. Therefore we're going to - // re-implement a subset of that logic here where we want to retain - // the whitespace verbatim even in flush mode. - - firstNewlineIdx := strings.IndexByte(tv.Text, '\n') - if firstNewlineIdx < 0 { - // Should never happen, because tv.Value would already have - // panicked above in this case. - panic("heredoc doesn't contain newline") - } - introducer := tv.Text[:firstNewlineIdx+1] - marker := introducer[2:] // trim off << prefix - if marker[0] == '-' { - marker = marker[1:] // also trim of - prefix for flush heredoc - } - body := tv.Text[len(introducer) : len(tv.Text)-len(marker)] - flush := introducer[2] == '-' - if flush { - // HCL1 treats flush heredocs differently, trimming off any - // spare whitespace that might appear after the trailing - // newline, and so we must replicate that here to avoid - // introducing additional whitespace in the output. - body = strings.TrimRight(body, " \t") - } - - // Now we have: - // - introducer is the first line, like "<<-FOO\n" - // - marker is the end marker, like "FOO\n" - // - body is the raw data between the introducer and the marker, - // which we need to do recursive upgrading for. - - buf.WriteString(introducer) - if !interp { - // Easy case: escape all interpolation-looking sequences. - printHeredocLiteralFromHILOutput(&buf, body) - } else { - hilNode, err := hil.Parse(body) - if err != nil { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid interpolated string", - Detail: fmt.Sprintf("Interpolation parsing failed: %s", err), - Subject: hcl1PosRange(filename, tv.Pos).Ptr(), - }) - } - if hilNode != nil { - if _, ok := hilNode.(*hilast.Output); !ok { - // hil.Parse usually produces an output, but it can sometimes - // produce an isolated expression if the input is entirely - // a single interpolation. - if hilNode != nil { - hilNode = &hilast.Output{ - Exprs: []hilast.Node{hilNode}, - Posx: hilNode.Pos(), - } - } - } - interpDiags := upgradeHeredocBody(&buf, hilNode.(*hilast.Output), filename, an) - diags = diags.Append(interpDiags) - } - } - if !strings.HasSuffix(body, "\n") { - // The versions of HCL1 vendored into Terraform <=0.11 - // incorrectly allowed the end marker to appear at the end of - // the final line of the body, rather than on a line of its own. - // That is no longer valid in HCL2, so we need to fix it up. - buf.WriteByte('\n') - } - // NOTE: Marker intentionally contains an extra newline here because - // we need to ensure that any follow-on expression bits end up on - // a separate line, or else the HCL2 parser won't be able to - // recognize the heredoc marker. This causes an extra empty line - // in some cases, which we accept for simplicity's sake. - buf.WriteString(marker) - - case hcl1token.BOOL: - litVal := tv.Value() - if litVal.(bool) { - buf.WriteString("true") - } else { - buf.WriteString("false") - } - - case hcl1token.NUMBER: - num, err := strconv.ParseInt(tv.Text, 0, 64) - if err != nil { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid number value", - Detail: fmt.Sprintf("Parsing failed: %s", err), - Subject: hcl1PosRange(filename, tv.Pos).Ptr(), - }) - } - buf.WriteString(strconv.FormatInt(num, 10)) - - case hcl1token.FLOAT: - num, err := strconv.ParseFloat(tv.Text, 64) - if err != nil { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid float value", - Detail: fmt.Sprintf("Parsing failed: %s", err), - Subject: hcl1PosRange(filename, tv.Pos).Ptr(), - }) - } - buf.WriteString(strconv.FormatFloat(num, 'f', -1, 64)) - - default: - // For everything else we'll just pass through the given bytes verbatim, - // but we should't get here because the above is intended to be exhaustive. - buf.WriteString(tv.Text) - - } - - case *hcl1ast.ListType: - multiline := tv.Lbrack.Line != tv.Rbrack.Line - buf.WriteString("[") - if multiline { - buf.WriteString("\n") - } - for i, node := range tv.List { - src, moreDiags := upgradeExpr(node, filename, interp, an) - diags = diags.Append(moreDiags) - buf.Write(src) - if lit, ok := node.(*hcl1ast.LiteralType); ok && lit.LineComment != nil { - for _, comment := range lit.LineComment.List { - buf.WriteString(", " + comment.Text) - buf.WriteString("\n") - } - } else { - if multiline { - buf.WriteString(",\n") - } else if i < len(tv.List)-1 { - buf.WriteString(", ") - } - } - } - buf.WriteString("]") - - case *hcl1ast.ObjectType: - if len(tv.List.Items) == 0 { - buf.WriteString("{}") - break - } - buf.WriteString("{\n") - for _, item := range tv.List.Items { - if len(item.Keys) != 1 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid map element", - Detail: "A map element may not have any block-style labels.", - Subject: hcl1PosRange(filename, item.Pos()).Ptr(), - }) - continue - } - keySrc, moreDiags := upgradeExpr(item.Keys[0].Token, filename, interp, an) - diags = diags.Append(moreDiags) - valueSrc, moreDiags := upgradeExpr(item.Val, filename, interp, an) - diags = diags.Append(moreDiags) - if item.LeadComment != nil { - for _, c := range item.LeadComment.List { - buf.WriteString(c.Text) - buf.WriteByte('\n') - } - } - - buf.Write(keySrc) - buf.WriteString(" = ") - buf.Write(valueSrc) - if item.LineComment != nil { - for _, c := range item.LineComment.List { - buf.WriteByte(' ') - buf.WriteString(c.Text) - } - } - buf.WriteString("\n") - } - buf.WriteString("}") - - case hcl1ast.Node: - // If our more-specific cases above didn't match this then we'll - // ask the hcl1printer package to print the expression out - // itself, and assume it'll still be valid in HCL2. - // (We should rarely end up here, since our cases above should - // be comprehensive.) - log.Printf("[TRACE] configupgrade: Don't know how to upgrade %T as expression, so just passing it through as-is", tv) - hcl1printer.Fprint(&buf, tv) - - case *hilast.LiteralNode: - switch tl := tv.Value.(type) { - case string: - // This shouldn't generally happen because literal strings are - // always wrapped in hilast.Output in HIL, but we'll allow it anyway. - printQuotedString(&buf, tl) - case int: - buf.WriteString(strconv.Itoa(tl)) - case float64: - buf.WriteString(strconv.FormatFloat(tl, 'f', -1, 64)) - case bool: - if tl { - buf.WriteString("true") - } else { - buf.WriteString("false") - } - } - - case *hilast.VariableAccess: - // In HIL a variable access is just a single string which might contain - // a mixture of identifiers, dots, integer indices, and splat expressions. - // All of these concepts were formerly interpreted by Terraform itself, - // rather than by HIL. We're going to process this one chunk at a time - // here so we can normalize and introduce some newer syntax where it's - // safe to do so. - parts := strings.Split(tv.Name, ".") - - transformed := transformCountPseudoAttribute(&buf, parts, an) - if transformed { - break Value - } - - parts = upgradeTraversalParts(parts, an) // might add/remove/change parts - - vDiags := validateHilAddress(tv.Name, filename) - if len(vDiags) > 0 { - diags = diags.Append(vDiags) - break - } - - printHilTraversalPartsAsHcl2(&buf, parts) - - case *hilast.Arithmetic: - op, exists := hilArithmeticOpSyms[tv.Op] - if !exists { - panic(fmt.Errorf("arithmetic node with unsupported operator %#v", tv.Op)) - } - - lhsExpr := tv.Exprs[0] - rhsExpr := tv.Exprs[1] - lhsSrc, exprDiags := upgradeExpr(lhsExpr, filename, true, an) - diags = diags.Append(exprDiags) - rhsSrc, exprDiags := upgradeExpr(rhsExpr, filename, true, an) - diags = diags.Append(exprDiags) - - // HIL's AST represents -foo as (0 - foo), so we'll recognize - // that here and normalize it back. - if tv.Op == hilast.ArithmeticOpSub && len(lhsSrc) == 1 && lhsSrc[0] == '0' { - buf.WriteString("-") - buf.Write(rhsSrc) - break - } - - buf.Write(lhsSrc) - buf.WriteString(op) - buf.Write(rhsSrc) - - case *hilast.Call: - name := tv.Func - args := tv.Args - - // Some adaptations must happen prior to upgrading the arguments, - // because they depend on the original argument AST nodes. - switch name { - case "base64sha256", "base64sha512", "md5", "sha1", "sha256", "sha512": - // These functions were sometimes used in conjunction with the - // file() function to take the hash of the contents of a file. - // Prior to Terraform 0.11 there was a chance of silent corruption - // of strings containing non-UTF8 byte sequences, and so we have - // made it illegal to use file() with non-text files in 0.12 even - // though in this _particular_ situation (passing the function - // result directly to another function) there would not be any - // corruption; the general rule keeps things consistent. - // However, to still meet those use-cases we now have variants of - // the hashing functions that have a "file" prefix on their names - // and read the contents of a given file, rather than hashing - // directly the given string. - if len(args) > 0 { - if subCall, ok := args[0].(*hilast.Call); ok && subCall.Func == "file" { - // We're going to flatten this down into a single call, so - // we actually want the arguments of the sub-call here. - name = "file" + name - args = subCall.Args - - // For this one, we'll fall through to the normal upgrade - // handling now that we've fixed up the name and args... - } - } - - } - - argExprs := make([][]byte, len(args)) - multiline := false - totalLen := 0 - for i, arg := range args { - if i > 0 { - totalLen += 2 - } - exprSrc, exprDiags := upgradeExpr(arg, filename, true, an) - diags = diags.Append(exprDiags) - argExprs[i] = exprSrc - if bytes.Contains(exprSrc, []byte{'\n'}) { - // If any of our arguments are multi-line then we'll also be multiline - multiline = true - } - totalLen += len(exprSrc) - } - - if totalLen > 60 { // heuristic, since we don't know here how indented we are already - multiline = true - } - - // Some functions are now better expressed as native language constructs. - // These cases will return early if they emit anything, or otherwise - // fall through to the default emitter. - switch name { - case "list": - // Should now use tuple constructor syntax - buf.WriteByte('[') - if multiline { - buf.WriteByte('\n') - } - for i, exprSrc := range argExprs { - buf.Write(exprSrc) - if multiline { - buf.WriteString(",\n") - } else { - if i < len(args)-1 { - buf.WriteString(", ") - } - } - } - buf.WriteByte(']') - break Value - case "map": - // Should now use object constructor syntax, but we can only - // achieve that if the call is valid, which requires an even - // number of arguments. - if len(argExprs) == 0 { - buf.WriteString("{}") - break Value - } else if len(argExprs)%2 == 0 { - buf.WriteString("{\n") - for i := 0; i < len(argExprs); i += 2 { - k := argExprs[i] - v := argExprs[i+1] - - buf.Write(k) - buf.WriteString(" = ") - buf.Write(v) - buf.WriteByte('\n') - } - buf.WriteByte('}') - break Value - } - case "lookup": - // A lookup call with only two arguments is equivalent to native - // index syntax. (A third argument would specify a default value, - // so calls like that must be left alone.) - // (Note that we can't safely do this for element(...) because - // the user may be relying on its wraparound behavior.) - if len(argExprs) == 2 { - buf.Write(argExprs[0]) - buf.WriteByte('[') - buf.Write(argExprs[1]) - buf.WriteByte(']') - break Value - } - case "element": - // We cannot replace element with index syntax safely in general - // because users may be relying on its special modulo wraparound - // behavior that the index syntax doesn't do. However, if it seems - // like the user is trying to use element with a set, we'll insert - // an explicit conversion to list to mimic the implicit conversion - // that we used to do as an unintended side-effect of how functions - // work in HIL. - if len(argExprs) > 0 { - argTy := an.InferExpressionType(argExprs[0], nil) - if argTy.IsSetType() { - newExpr := []byte(`tolist(`) - newExpr = append(newExpr, argExprs[0]...) - newExpr = append(newExpr, ')') - argExprs[0] = newExpr - } - } - - // HIL used some undocumented special functions to implement certain - // operations, but since those were actually callable in real expressions - // some users inevitably depended on them, so we'll fix them up here. - // These each become two function calls to preserve the old behavior - // of implicitly converting to the source type first. Usage of these - // is relatively rare, so the result doesn't need to be too pretty. - case "__builtin_BoolToString": - buf.WriteString("tostring(tobool(") - buf.Write(argExprs[0]) - buf.WriteString("))") - break Value - case "__builtin_FloatToString": - buf.WriteString("tostring(tonumber(") - buf.Write(argExprs[0]) - buf.WriteString("))") - break Value - case "__builtin_IntToString": - buf.WriteString("tostring(floor(") - buf.Write(argExprs[0]) - buf.WriteString("))") - break Value - case "__builtin_StringToInt": - buf.WriteString("floor(tostring(") - buf.Write(argExprs[0]) - buf.WriteString("))") - break Value - case "__builtin_StringToFloat": - buf.WriteString("tonumber(tostring(") - buf.Write(argExprs[0]) - buf.WriteString("))") - break Value - case "__builtin_StringToBool": - buf.WriteString("tobool(tostring(") - buf.Write(argExprs[0]) - buf.WriteString("))") - break Value - case "__builtin_FloatToInt", "__builtin_IntToFloat": - // Since "floor" already has an implicit conversion of its argument - // to number, and the result is a whole number in either case, - // these ones are easier. (We no longer distinguish int and float - // as types in HCL2, even though HIL did.) - name = "floor" - } - - buf.WriteString(name) - buf.WriteByte('(') - if multiline { - buf.WriteByte('\n') - } - for i, exprSrc := range argExprs { - buf.Write(exprSrc) - if multiline { - buf.WriteString(",\n") - } else { - if i < len(args)-1 { - buf.WriteString(", ") - } - } - } - buf.WriteByte(')') - - case *hilast.Conditional: - condSrc, exprDiags := upgradeExpr(tv.CondExpr, filename, true, an) - diags = diags.Append(exprDiags) - trueSrc, exprDiags := upgradeExpr(tv.TrueExpr, filename, true, an) - diags = diags.Append(exprDiags) - falseSrc, exprDiags := upgradeExpr(tv.FalseExpr, filename, true, an) - diags = diags.Append(exprDiags) - - buf.Write(condSrc) - buf.WriteString(" ? ") - buf.Write(trueSrc) - buf.WriteString(" : ") - buf.Write(falseSrc) - - case *hilast.Index: - target, ok := tv.Target.(*hilast.VariableAccess) - if !ok { - panic(fmt.Sprintf("Index node with unsupported target type (%T)", tv.Target)) - } - parts := strings.Split(target.Name, ".") - - keySrc, exprDiags := upgradeExpr(tv.Key, filename, true, an) - diags = diags.Append(exprDiags) - - transformed := transformCountPseudoAttribute(&buf, parts, an) - if transformed { - break Value - } - - parts = upgradeTraversalParts(parts, an) // might add/remove/change parts - - vDiags := validateHilAddress(target.Name, filename) - if len(vDiags) > 0 { - diags = diags.Append(vDiags) - break - } - - first, remain := parts[0], parts[1:] - - var rAddr addrs.Resource - switch parts[0] { - case "data": - if len(parts) == 5 && parts[3] == "*" { - rAddr.Mode = addrs.DataResourceMode - rAddr.Type = parts[1] - rAddr.Name = parts[2] - } - default: - if len(parts) == 4 && parts[2] == "*" { - rAddr.Mode = addrs.ManagedResourceMode - rAddr.Type = parts[0] - rAddr.Name = parts[1] - } - } - - // We need to check if the thing being referenced has count - // to retain backward compatibility - hasCount := false - if v, exists := an.ResourceHasCount[rAddr]; exists { - hasCount = v - } - - hasSplat := false - - buf.WriteString(first) - for _, part := range remain { - // Attempt to convert old-style splat indexing to new one - // e.g. res.label.*.attr[idx] to res.label[idx].attr - if part == "*" && hasCount { - hasSplat = true - buf.WriteString(fmt.Sprintf("[%s]", keySrc)) - continue - } - - buf.WriteByte('.') - buf.WriteString(part) - } - - if !hasSplat { - buf.WriteString("[") - buf.Write(keySrc) - buf.WriteString("]") - } - - case *hilast.Output: - if len(tv.Exprs) == 1 { - item := tv.Exprs[0] - naked := true - if lit, ok := item.(*hilast.LiteralNode); ok { - if _, ok := lit.Value.(string); ok { - naked = false - } - } - if naked { - // If there's only one expression and it isn't a literal string - // then we'll just output it naked, since wrapping a single - // expression in interpolation is no longer idiomatic. - interped, interpDiags := upgradeExpr(item, filename, true, an) - diags = diags.Append(interpDiags) - buf.Write(interped) - break - } - } - - buf.WriteString(`"`) - for _, item := range tv.Exprs { - if lit, ok := item.(*hilast.LiteralNode); ok { - if litStr, ok := lit.Value.(string); ok { - printStringLiteralFromHILOutput(&buf, litStr) - continue - } - } - - interped, interpDiags := upgradeExpr(item, filename, true, an) - diags = diags.Append(interpDiags) - - buf.WriteString("${") - buf.Write(interped) - buf.WriteString("}") - } - buf.WriteString(`"`) - - case hilast.Node: - // Nothing reasonable we can do here, so we should've handled all of - // the possibilities above. - panic(fmt.Errorf("upgradeExpr doesn't handle HIL node type %T", tv)) - - default: - // If we end up in here then the caller gave us something completely invalid. - panic(fmt.Errorf("upgradeExpr on unsupported type %T", val)) - - } - - return buf.Bytes(), diags -} - -func validateHilAddress(address, filename string) tfdiags.Diagnostics { - parts := strings.Split(address, ".") - var diags tfdiags.Diagnostics - - label, ok := getResourceLabel(parts) - if ok && !hcl2syntax.ValidIdentifier(label) { - // We can't get any useful source location out of HIL unfortunately - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - fmt.Sprintf("Invalid address (%s) in ./%s", address, filename), - // The label could be invalid for another reason - // but this is the most likely, so we add it as hint - "Names of objects (resources, modules, etc) may no longer start with digits.")) - } - - return diags -} - -func getResourceLabel(parts []string) (string, bool) { - if len(parts) < 1 { - return "", false - } - - if parts[0] == "data" { - if len(parts) < 3 { - return "", false - } - return parts[2], true - } - - if len(parts) < 2 { - return "", false - } - - return parts[1], true -} - -// transformCountPseudoAttribute deals with the .count pseudo-attributes -// that 0.11 and prior allowed for resources. These no longer exist, -// because they don't do anything we can't do with the length(...) function. -func transformCountPseudoAttribute(buf *bytes.Buffer, parts []string, an *analysis) (transformed bool) { - if len(parts) > 0 { - var rAddr addrs.Resource - switch parts[0] { - case "data": - if len(parts) == 4 && parts[3] == "count" { - rAddr.Mode = addrs.DataResourceMode - rAddr.Type = parts[1] - rAddr.Name = parts[2] - } - default: - if len(parts) == 3 && parts[2] == "count" { - rAddr.Mode = addrs.ManagedResourceMode - rAddr.Type = parts[0] - rAddr.Name = parts[1] - } - } - // We need to check if the thing being referenced is actually an - // existing resource, because other three-part traversals might - // coincidentally end with "count". - if hasCount, exists := an.ResourceHasCount[rAddr]; exists { - if hasCount { - buf.WriteString("length(") - buf.WriteString(rAddr.String()) - buf.WriteString(")") - } else { - // If the resource does not have count, the .count - // attr would've always returned 1 before. - buf.WriteString("1") - } - transformed = true - return - } - } - return -} - -func printHilTraversalPartsAsHcl2(buf *bytes.Buffer, parts []string) { - first, remain := parts[0], parts[1:] - buf.WriteString(first) - seenSplat := false - for _, part := range remain { - if part == "*" { - seenSplat = true - buf.WriteString(".*") - continue - } - - // Other special cases apply only if we've not previously - // seen a splat expression marker, since attribute vs. index - // syntax have different interpretations after a simple splat. - if !seenSplat { - if v, err := strconv.Atoi(part); err == nil { - // Looks like it's old-style index traversal syntax foo.0.bar - // so we'll replace with canonical index syntax foo[0].bar. - fmt.Fprintf(buf, "[%d]", v) - continue - } - if !hcl2syntax.ValidIdentifier(part) { - // This should be rare since HIL's identifier syntax is _close_ - // to HCL2's, but we'll get here if one of the intervening - // parts is not a valid identifier in isolation, since HIL - // did not consider these to be separate identifiers. - // e.g. foo.1bar would be invalid in HCL2; must instead be foo["1bar"]. - buf.WriteByte('[') - printQuotedString(buf, part) - buf.WriteByte(']') - continue - } - } - - buf.WriteByte('.') - buf.WriteString(part) - } -} - -func upgradeHeredocBody(buf *bytes.Buffer, val *hilast.Output, filename string, an *analysis) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - for _, item := range val.Exprs { - if lit, ok := item.(*hilast.LiteralNode); ok { - if litStr, ok := lit.Value.(string); ok { - printHeredocLiteralFromHILOutput(buf, litStr) - continue - } - } - interped, interpDiags := upgradeExpr(item, filename, true, an) - diags = diags.Append(interpDiags) - - buf.WriteString("${") - buf.Write(interped) - buf.WriteString("}") - } - - return diags -} - -func upgradeTraversalExpr(val interface{}, filename string, an *analysis) ([]byte, tfdiags.Diagnostics) { - if lit, ok := val.(*hcl1ast.LiteralType); ok && lit.Token.Type == hcl1token.STRING { - trStr := lit.Token.Value().(string) - if strings.HasSuffix(trStr, ".%") || strings.HasSuffix(trStr, ".#") { - // Terraform 0.11 would often not validate traversals given in - // strings and so users would get away with this sort of - // flatmap-implementation-detail reference, particularly inside - // ignore_changes. We'll just trim these off to tolerate it, - // rather than failing below in ParseTraversalAbs. - trStr = trStr[:len(trStr)-2] - } - trSrc := []byte(trStr) - _, trDiags := hcl2syntax.ParseTraversalAbs(trSrc, "", hcl2.Pos{}) - if !trDiags.HasErrors() { - return trSrc, nil - } - } - return upgradeExpr(val, filename, false, an) -} - -var hilArithmeticOpSyms = map[hilast.ArithmeticOp]string{ - hilast.ArithmeticOpAdd: " + ", - hilast.ArithmeticOpSub: " - ", - hilast.ArithmeticOpMul: " * ", - hilast.ArithmeticOpDiv: " / ", - hilast.ArithmeticOpMod: " % ", - - hilast.ArithmeticOpLogicalAnd: " && ", - hilast.ArithmeticOpLogicalOr: " || ", - - hilast.ArithmeticOpEqual: " == ", - hilast.ArithmeticOpNotEqual: " != ", - hilast.ArithmeticOpLessThan: " < ", - hilast.ArithmeticOpLessThanOrEqual: " <= ", - hilast.ArithmeticOpGreaterThan: " > ", - hilast.ArithmeticOpGreaterThanOrEqual: " >= ", -} - -// upgradeTraversalParts might alter the given split parts from a HIL-style -// variable access to account for renamings made in Terraform v0.12. -func upgradeTraversalParts(parts []string, an *analysis) []string { - parts = upgradeCountTraversalParts(parts, an) - parts = upgradeTerraformRemoteStateTraversalParts(parts, an) - return parts -} - -func upgradeCountTraversalParts(parts []string, an *analysis) []string { - // test_instance.foo.id needs to become test_instance.foo[0].id if - // count is set for test_instance.foo. Likewise, if count _isn't_ set - // then test_instance.foo.0.id must become test_instance.foo.id. - if len(parts) < 3 { - return parts - } - var addr addrs.Resource - var idxIdx int - switch parts[0] { - case "data": - addr.Mode = addrs.DataResourceMode - addr.Type = parts[1] - addr.Name = parts[2] - idxIdx = 3 - default: - addr.Mode = addrs.ManagedResourceMode - addr.Type = parts[0] - addr.Name = parts[1] - idxIdx = 2 - } - - hasCount, exists := an.ResourceHasCount[addr] - if !exists { - // Probably not actually a resource instance at all, then. - return parts - } - - // Since at least one attribute is required after a resource reference - // prior to Terraform v0.12, we can assume there will be at least enough - // parts to contain the index even if no index is actually present. - if idxIdx >= len(parts) { - return parts - } - - maybeIdx := parts[idxIdx] - switch { - case hasCount: - if _, err := strconv.Atoi(maybeIdx); err == nil || maybeIdx == "*" { - // Has an index already, so no changes required. - return parts - } - // Need to insert index zero at idxIdx. - log.Printf("[TRACE] configupgrade: %s has count but reference does not have index, so adding one", addr) - newParts := make([]string, len(parts)+1) - copy(newParts, parts[:idxIdx]) - newParts[idxIdx] = "0" - copy(newParts[idxIdx+1:], parts[idxIdx:]) - return newParts - default: - // For removing indexes we'll be more conservative and only remove - // exactly index "0", because other indexes on a resource without - // count are invalid anyway and we're better off letting the normal - // configuration parser deal with that. - if maybeIdx != "0" { - return parts - } - - // Need to remove the index zero. - log.Printf("[TRACE] configupgrade: %s does not have count but reference has index, so removing it", addr) - newParts := make([]string, len(parts)-1) - copy(newParts, parts[:idxIdx]) - copy(newParts[idxIdx:], parts[idxIdx+1:]) - return newParts - } -} - -func upgradeTerraformRemoteStateTraversalParts(parts []string, an *analysis) []string { - // data.terraform_remote_state.x.foo needs to become - // data.terraform_remote_state.x.outputs.foo unless "foo" is a real - // attribute in the object type implied by the remote state schema. - if len(parts) < 4 { - return parts - } - if parts[0] != "data" || parts[1] != "terraform_remote_state" { - return parts - } - - attrIdx := 3 - if parts[attrIdx] == "*" { - attrIdx = 4 // data.terraform_remote_state.x.*.foo - } else if _, err := strconv.Atoi(parts[attrIdx]); err == nil { - attrIdx = 4 // data.terraform_remote_state.x.1.foo - } - if attrIdx >= len(parts) { - return parts - } - - attrName := parts[attrIdx] - - // Now we'll use the schema of data.terraform_remote_state to decide if - // the user intended this to be an output, or whether it's one of the real - // attributes of this data source. - var schema *configschema.Block - if providerSchema := an.ProviderSchemas["terraform"]; providerSchema != nil { - schema, _ = providerSchema.SchemaForResourceType(addrs.DataResourceMode, "terraform_remote_state") - } - // Schema should be available in all reasonable cases, but might be nil - // if input configuration contains a reference to a remote state data resource - // without actually defining that data resource. In that weird edge case, - // we'll just assume all attributes are outputs. - if schema != nil && schema.ImpliedType().HasAttribute(attrName) { - // User is accessing one of the real attributes, then, and we have - // no need to rewrite it. - return parts - } - - // If we get down here then our task is to produce a new parts slice - // that has the fixed additional attribute name "outputs" inserted at - // attrIdx, retaining all other parts. - newParts := make([]string, len(parts)+1) - copy(newParts, parts[:attrIdx]) - newParts[attrIdx] = "outputs" - copy(newParts[attrIdx+1:], parts[attrIdx:]) - return newParts -} - -func typeIsSettableFromTupleCons(ty cty.Type) bool { - return ty.IsListType() || ty.IsTupleType() || ty.IsSetType() -} diff --git a/configs/configupgrade/upgrade_native.go b/configs/configupgrade/upgrade_native.go deleted file mode 100644 index e2a71c72e..000000000 --- a/configs/configupgrade/upgrade_native.go +++ /dev/null @@ -1,796 +0,0 @@ -package configupgrade - -import ( - "bytes" - "fmt" - "io" - "log" - "regexp" - "sort" - "strings" - - version "github.com/hashicorp/go-version" - - hcl1ast "github.com/hashicorp/hcl/hcl/ast" - hcl1parser "github.com/hashicorp/hcl/hcl/parser" - hcl1printer "github.com/hashicorp/hcl/hcl/printer" - hcl1token "github.com/hashicorp/hcl/hcl/token" - - hcl2 "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/addrs" - backendinit "github.com/hashicorp/terraform/backend/init" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/tfdiags" -) - -type upgradeFileResult struct { - Content []byte - ProviderRequirements map[string]version.Constraints -} - -func (u *Upgrader) upgradeNativeSyntaxFile(filename string, src []byte, an *analysis) (upgradeFileResult, tfdiags.Diagnostics) { - var result upgradeFileResult - var diags tfdiags.Diagnostics - - log.Printf("[TRACE] configupgrade: Working on %q", filename) - - var buf bytes.Buffer - - f, err := hcl1parser.Parse(src) - if err != nil { - return result, diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Syntax error in configuration file", - Detail: fmt.Sprintf("Error while parsing: %s", err), - Subject: hcl1ErrSubjectRange(filename, err), - }) - } - - rootList := f.Node.(*hcl1ast.ObjectList) - rootItems := rootList.Items - adhocComments := collectAdhocComments(f) - - for _, item := range rootItems { - comments := adhocComments.TakeBefore(item) - for _, group := range comments { - printComments(&buf, group) - buf.WriteByte('\n') // Extra separator after each group - } - - blockType := item.Keys[0].Token.Value().(string) - labels := make([]string, len(item.Keys)-1) - for i, key := range item.Keys[1:] { - labels[i] = key.Token.Value().(string) - } - body, isObject := item.Val.(*hcl1ast.ObjectType) - if !isObject { - // Should never happen for valid input, since we don't expect - // any non-block items at our top level. - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagWarning, - Summary: "Unsupported top-level attribute", - Detail: fmt.Sprintf("Attribute %q is not expected here, so its expression was not upgraded.", blockType), - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - // Preserve the item as-is, using the hcl1printer package. - buf.WriteString("# TF-UPGRADE-TODO: Top-level attributes are not valid, so this was not automatically upgraded.\n") - hcl1printer.Fprint(&buf, item) - buf.WriteString("\n\n") - continue - } - declRange := hcl1PosRange(filename, item.Keys[0].Pos()) - - switch blockType { - - case "resource", "data": - if len(labels) != 2 { - // Should never happen for valid input. - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: fmt.Sprintf("Invalid %s block", blockType), - Detail: fmt.Sprintf("A %s block must have two labels: the type and the name.", blockType), - Subject: &declRange, - }) - continue - } - - rAddr := addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: labels[0], - Name: labels[1], - } - if blockType == "data" { - rAddr.Mode = addrs.DataResourceMode - } - - log.Printf("[TRACE] configupgrade: Upgrading %s at %s", rAddr, declRange) - moreDiags := u.upgradeNativeSyntaxResource(filename, &buf, rAddr, item, an, adhocComments) - diags = diags.Append(moreDiags) - - case "provider": - if len(labels) != 1 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: fmt.Sprintf("Invalid %s block", blockType), - Detail: fmt.Sprintf("A %s block must have one label: the provider type.", blockType), - Subject: &declRange, - }) - continue - } - - pType := labels[0] - log.Printf("[TRACE] configupgrade: Upgrading provider.%s at %s", pType, declRange) - moreDiags := u.upgradeNativeSyntaxProvider(filename, &buf, pType, item, an, adhocComments) - diags = diags.Append(moreDiags) - - case "terraform": - if len(labels) != 0 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: fmt.Sprintf("Invalid %s block", blockType), - Detail: fmt.Sprintf("A %s block must not have any labels.", blockType), - Subject: &declRange, - }) - continue - } - moreDiags := u.upgradeNativeSyntaxTerraformBlock(filename, &buf, item, an, adhocComments) - diags = diags.Append(moreDiags) - - case "variable": - if len(labels) != 1 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: fmt.Sprintf("Invalid %s block", blockType), - Detail: fmt.Sprintf("A %s block must have one label: the variable name.", blockType), - Subject: &declRange, - }) - continue - } - - printComments(&buf, item.LeadComment) - printBlockOpen(&buf, blockType, labels, item.LineComment) - rules := bodyContentRules{ - "description": noInterpAttributeRule(filename, cty.String, an), - "default": noInterpAttributeRule(filename, cty.DynamicPseudoType, an), - "type": maybeBareKeywordAttributeRule(filename, an, map[string]string{ - // "list" and "map" in older versions were documented to - // mean list and map of strings, so we'll migrate to that - // and let the user adjust it to some other type if desired. - "list": `list(string)`, - "map": `map(string)`, - }), - } - log.Printf("[TRACE] configupgrade: Upgrading var.%s at %s", labels[0], declRange) - bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("var.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n\n") - - case "output": - if len(labels) != 1 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: fmt.Sprintf("Invalid %s block", blockType), - Detail: fmt.Sprintf("A %s block must have one label: the output name.", blockType), - Subject: &declRange, - }) - continue - } - - printComments(&buf, item.LeadComment) - if invalidLabel(labels[0]) { - printLabelTodo(&buf, labels[0]) - } - printBlockOpen(&buf, blockType, labels, item.LineComment) - - rules := bodyContentRules{ - "description": noInterpAttributeRule(filename, cty.String, an), - "value": normalAttributeRule(filename, cty.DynamicPseudoType, an), - "sensitive": noInterpAttributeRule(filename, cty.Bool, an), - "depends_on": dependsOnAttributeRule(filename, an), - } - log.Printf("[TRACE] configupgrade: Upgrading output.%s at %s", labels[0], declRange) - bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("output.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n\n") - - case "module": - if len(labels) != 1 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: fmt.Sprintf("Invalid %s block", blockType), - Detail: fmt.Sprintf("A %s block must have one label: the module call name.", blockType), - Subject: &declRange, - }) - continue - } - - // Since upgrading is a single-module endeavor, we don't have access - // to the configuration of the child module here, but we know that - // in practice all arguments that aren't reserved meta-arguments - // in a module block are normal expression attributes so we'll - // start with the straightforward mapping of those and override - // the special lifecycle arguments below. - rules := justAttributesBodyRules(filename, body, an) - rules["source"] = moduleSourceRule(filename, an) - rules["version"] = noInterpAttributeRule(filename, cty.String, an) - rules["providers"] = func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - subBody, ok := item.Val.(*hcl1ast.ObjectType) - if !ok { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Invalid providers argument", - Detail: `The "providers" argument must be a map from provider addresses in the child module to corresponding provider addresses in this module.`, - Subject: &declRange, - }) - return diags - } - - // We're gonna cheat here and use justAttributesBodyRules to - // find all the attribute names but then just rewrite them all - // to be our specialized traversal-style mapping instead. - subRules := justAttributesBodyRules(filename, subBody, an) - for k := range subRules { - subRules[k] = maybeBareTraversalAttributeRule(filename, an) - } - buf.WriteString("providers = {\n") - bodyDiags := upgradeBlockBody(filename, blockAddr, buf, subBody.List.Items, body.Rbrace, subRules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n") - - return diags - } - - printComments(&buf, item.LeadComment) - printBlockOpen(&buf, blockType, labels, item.LineComment) - log.Printf("[TRACE] configupgrade: Upgrading module.%s at %s", labels[0], declRange) - bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("module.%s", labels[0]), &buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n\n") - - case "locals": - log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange) - printComments(&buf, item.LeadComment) - printBlockOpen(&buf, blockType, labels, item.LineComment) - - // The "locals" block contents are free-form declarations, so - // we'll just use the default attribute mapping rule for everything - // inside it. - rules := justAttributesBodyRules(filename, body, an) - log.Printf("[TRACE] configupgrade: Upgrading locals block at %s", declRange) - bodyDiags := upgradeBlockBody(filename, "locals", &buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n\n") - - default: - // Should never happen for valid input, because the above cases - // are exhaustive for valid blocks as of Terraform 0.11. - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagWarning, - Summary: "Unsupported root block type", - Detail: fmt.Sprintf("The block type %q is not expected here, so its content was not upgraded.", blockType), - Subject: hcl1PosRange(filename, item.Keys[0].Pos()).Ptr(), - }) - - // Preserve the block as-is, using the hcl1printer package. - buf.WriteString("# TF-UPGRADE-TODO: Block type was not recognized, so this block and its contents were not automatically upgraded.\n") - hcl1printer.Fprint(&buf, item) - buf.WriteString("\n\n") - continue - } - } - - // Print out any leftover comments - for _, group := range *adhocComments { - printComments(&buf, group) - } - - result.Content = buf.Bytes() - - return result, diags -} - -func (u *Upgrader) upgradeNativeSyntaxResource(filename string, buf *bytes.Buffer, addr addrs.Resource, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - body := item.Val.(*hcl1ast.ObjectType) - declRange := hcl1PosRange(filename, item.Keys[0].Pos()) - - // We should always have a schema for each provider in our analysis - // object. If not, it's a bug in the analyzer. - providerType, ok := an.ResourceProviderType[addr] - if !ok { - panic(fmt.Sprintf("unknown provider type for %s", addr.String())) - } - providerSchema, ok := an.ProviderSchemas[providerType] - if !ok { - panic(fmt.Sprintf("missing schema for provider type %q", providerType)) - } - schema, _ := providerSchema.SchemaForResourceAddr(addr) - if schema == nil { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Unknown resource type", - Detail: fmt.Sprintf("The resource type %q is not known to the currently-selected version of provider %q.", addr.Type, providerType), - Subject: &declRange, - }) - return diags - } - - var blockType string - switch addr.Mode { - case addrs.ManagedResourceMode: - blockType = "resource" - case addrs.DataResourceMode: - blockType = "data" - } - labels := []string{addr.Type, addr.Name} - - rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) - rules["count"] = normalAttributeRule(filename, cty.Number, an) - rules["depends_on"] = dependsOnAttributeRule(filename, an) - rules["provider"] = maybeBareTraversalAttributeRule(filename, an) - rules["lifecycle"] = nestedBlockRule(filename, lifecycleBlockBodyRules(filename, an), an, adhocComments) - if addr.Mode == addrs.ManagedResourceMode { - rules["connection"] = connectionBlockRule(filename, addr.Type, an, adhocComments) - rules["provisioner"] = provisionerBlockRule(filename, addr.Type, an, adhocComments) - } - - printComments(buf, item.LeadComment) - if invalidLabel(labels[1]) { - printLabelTodo(buf, labels[1]) - } - printBlockOpen(buf, blockType, labels, item.LineComment) - bodyDiags := upgradeBlockBody(filename, addr.String(), buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n\n") - - return diags -} - -func (u *Upgrader) upgradeNativeSyntaxProvider(filename string, buf *bytes.Buffer, typeName string, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - body := item.Val.(*hcl1ast.ObjectType) - - // We should always have a schema for each provider in our analysis - // object. If not, it's a bug in the analyzer. - providerSchema, ok := an.ProviderSchemas[typeName] - if !ok { - panic(fmt.Sprintf("missing schema for provider type %q", typeName)) - } - schema := providerSchema.Provider - rules := schemaDefaultBodyRules(filename, schema, an, adhocComments) - rules["alias"] = noInterpAttributeRule(filename, cty.String, an) - rules["version"] = noInterpAttributeRule(filename, cty.String, an) - - printComments(buf, item.LeadComment) - printBlockOpen(buf, "provider", []string{typeName}, item.LineComment) - bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("provider.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n\n") - - return diags -} - -func (u *Upgrader) upgradeNativeSyntaxTerraformBlock(filename string, buf *bytes.Buffer, item *hcl1ast.ObjectItem, an *analysis, adhocComments *commentQueue) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - body := item.Val.(*hcl1ast.ObjectType) - - rules := bodyContentRules{ - "required_version": noInterpAttributeRule(filename, cty.String, an), - "backend": func(buf *bytes.Buffer, blockAddr string, item *hcl1ast.ObjectItem) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - declRange := hcl1PosRange(filename, item.Keys[0].Pos()) - if len(item.Keys) != 2 { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: `Invalid backend block`, - Detail: `A backend block must have one label: the backend type name.`, - Subject: &declRange, - }) - return diags - } - - typeName := item.Keys[1].Token.Value().(string) - beFn := backendinit.Backend(typeName) - if beFn == nil { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Unsupported backend type", - Detail: fmt.Sprintf("Terraform does not support a backend type named %q.", typeName), - Subject: &declRange, - }) - return diags - } - be := beFn() - schema := be.ConfigSchema() - rules := schemaNoInterpBodyRules(filename, schema, an, adhocComments) - - body := item.Val.(*hcl1ast.ObjectType) - - printComments(buf, item.LeadComment) - printBlockOpen(buf, "backend", []string{typeName}, item.LineComment) - bodyDiags := upgradeBlockBody(filename, fmt.Sprintf("terraform.backend.%s", typeName), buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n") - - return diags - }, - } - - printComments(buf, item.LeadComment) - printBlockOpen(buf, "terraform", nil, item.LineComment) - bodyDiags := upgradeBlockBody(filename, "terraform", buf, body.List.Items, body.Rbrace, rules, adhocComments) - diags = diags.Append(bodyDiags) - buf.WriteString("}\n\n") - - return diags -} - -func upgradeBlockBody(filename string, blockAddr string, buf *bytes.Buffer, args []*hcl1ast.ObjectItem, end hcl1token.Pos, rules bodyContentRules, adhocComments *commentQueue) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - for i, arg := range args { - comments := adhocComments.TakeBefore(arg) - for _, group := range comments { - printComments(buf, group) - buf.WriteByte('\n') // Extra separator after each group - } - - printComments(buf, arg.LeadComment) - - name := arg.Keys[0].Token.Value().(string) - - rule, expected := rules[name] - if !expected { - if arg.Assign.IsValid() { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Unrecognized attribute name", - Detail: fmt.Sprintf("No attribute named %q is expected in %s.", name, blockAddr), - Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(), - }) - } else { - diags = diags.Append(&hcl2.Diagnostic{ - Severity: hcl2.DiagError, - Summary: "Unrecognized block type", - Detail: fmt.Sprintf("Blocks of type %q are not expected in %s.", name, blockAddr), - Subject: hcl1PosRange(filename, arg.Keys[0].Pos()).Ptr(), - }) - } - continue - } - - itemDiags := rule(buf, blockAddr, arg) - diags = diags.Append(itemDiags) - - // If we have another item and it's more than one line away - // from the current one then we'll print an extra blank line - // to retain that separation. - if (i + 1) < len(args) { - next := args[i+1] - thisPos := hcl1NodeEndPos(arg) - nextPos := next.Pos() - if nextPos.Line-thisPos.Line > 1 { - buf.WriteByte('\n') - } - } - } - - // Before we return, we must also print any remaining adhocComments that - // appear between our last item and the closing brace. - comments := adhocComments.TakeBeforePos(end) - for i, group := range comments { - printComments(buf, group) - if i < len(comments)-1 { - buf.WriteByte('\n') // Extra separator after each group - } - } - - return diags -} - -// printDynamicBody prints out a conservative, exhaustive dynamic block body -// for every attribute and nested block in the given schema, for situations -// when a dynamic expression was being assigned to a block type name in input -// configuration and so we can assume it's a list of maps but can't make -// any assumptions about what subset of the schema-specified keys might be -// present in the map values. -func printDynamicBlockBody(buf *bytes.Buffer, iterName string, schema *configschema.Block) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - attrNames := make([]string, 0, len(schema.Attributes)) - for name := range schema.Attributes { - attrNames = append(attrNames, name) - } - sort.Strings(attrNames) - for _, name := range attrNames { - attrS := schema.Attributes[name] - if !(attrS.Required || attrS.Optional) { // no Computed-only attributes - continue - } - if attrS.Required { - // For required attributes we can generate a simpler expression - // that just assumes the presence of the key representing the - // attribute value. - printAttribute(buf, name, []byte(fmt.Sprintf(`%s.value.%s`, iterName, name)), nil) - } else { - // Otherwise we must be conservative and generate a conditional - // lookup that will just populate nothing at all if the expected - // key is not present. - printAttribute(buf, name, []byte(fmt.Sprintf(`lookup(%s.value, %q, null)`, iterName, name)), nil) - } - } - - blockTypeNames := make([]string, 0, len(schema.BlockTypes)) - for name := range schema.BlockTypes { - blockTypeNames = append(blockTypeNames, name) - } - sort.Strings(blockTypeNames) - for i, name := range blockTypeNames { - blockS := schema.BlockTypes[name] - - // We'll disregard any block type that consists only of computed - // attributes, since otherwise we'll just create weird empty blocks - // that do nothing except create confusion. - if !schemaHasSettableArguments(&blockS.Block) { - continue - } - - if i > 0 || len(attrNames) > 0 { - buf.WriteByte('\n') - } - printBlockOpen(buf, "dynamic", []string{name}, nil) - switch blockS.Nesting { - case configschema.NestingMap: - printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, {})`, iterName, name)), nil) - printAttribute(buf, "labels", []byte(fmt.Sprintf(`[%s.key]`, name)), nil) - case configschema.NestingSingle, configschema.NestingGroup: - printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, null) != null ? [%s.value.%s] : []`, iterName, name, iterName, name)), nil) - default: - printAttribute(buf, "for_each", []byte(fmt.Sprintf(`lookup(%s.value, %q, [])`, iterName, name)), nil) - } - printBlockOpen(buf, "content", nil, nil) - moreDiags := printDynamicBlockBody(buf, name, &blockS.Block) - diags = diags.Append(moreDiags) - buf.WriteString("}\n") - buf.WriteString("}\n") - } - - return diags -} - -func printComments(buf *bytes.Buffer, group *hcl1ast.CommentGroup) { - if group == nil { - return - } - for _, comment := range group.List { - buf.WriteString(comment.Text) - buf.WriteByte('\n') - } -} - -func printBlockOpen(buf *bytes.Buffer, blockType string, labels []string, commentGroup *hcl1ast.CommentGroup) { - buf.WriteString(blockType) - for _, label := range labels { - buf.WriteByte(' ') - printQuotedString(buf, label) - } - buf.WriteString(" {") - if commentGroup != nil { - for _, c := range commentGroup.List { - buf.WriteByte(' ') - buf.WriteString(c.Text) - } - } - buf.WriteByte('\n') -} - -func printAttribute(buf *bytes.Buffer, name string, valSrc []byte, commentGroup *hcl1ast.CommentGroup) { - buf.WriteString(name) - buf.WriteString(" = ") - buf.Write(valSrc) - if commentGroup != nil { - for _, c := range commentGroup.List { - buf.WriteByte(' ') - buf.WriteString(c.Text) - } - } - buf.WriteByte('\n') -} - -func printQuotedString(buf *bytes.Buffer, val string) { - buf.WriteByte('"') - printStringLiteralFromHILOutput(buf, val) - buf.WriteByte('"') -} - -func printStringLiteralFromHILOutput(buf *bytes.Buffer, val string) { - val = strings.Replace(val, `\`, `\\`, -1) - val = strings.Replace(val, `"`, `\"`, -1) - val = strings.Replace(val, "\n", `\n`, -1) - val = strings.Replace(val, "\r", `\r`, -1) - val = strings.Replace(val, `${`, `$${`, -1) - val = strings.Replace(val, `%{`, `%%{`, -1) - buf.WriteString(val) -} - -func printHeredocLiteralFromHILOutput(buf *bytes.Buffer, val string) { - val = strings.Replace(val, `${`, `$${`, -1) - val = strings.Replace(val, `%{`, `%%{`, -1) - buf.WriteString(val) -} - -func collectAdhocComments(f *hcl1ast.File) *commentQueue { - comments := make(map[hcl1token.Pos]*hcl1ast.CommentGroup) - for _, c := range f.Comments { - comments[c.Pos()] = c - } - - // We'll remove from our map any comments that are attached to specific - // nodes as lead or line comments, since we'll find those during our - // walk anyway. - hcl1ast.Walk(f, func(nn hcl1ast.Node) (hcl1ast.Node, bool) { - switch t := nn.(type) { - case *hcl1ast.LiteralType: - if t.LeadComment != nil { - for _, comment := range t.LeadComment.List { - delete(comments, comment.Pos()) - } - } - - if t.LineComment != nil { - for _, comment := range t.LineComment.List { - delete(comments, comment.Pos()) - } - } - case *hcl1ast.ObjectItem: - if t.LeadComment != nil { - for _, comment := range t.LeadComment.List { - delete(comments, comment.Pos()) - } - } - - if t.LineComment != nil { - for _, comment := range t.LineComment.List { - delete(comments, comment.Pos()) - } - } - } - - return nn, true - }) - - if len(comments) == 0 { - var ret commentQueue - return &ret - } - - ret := make([]*hcl1ast.CommentGroup, 0, len(comments)) - for _, c := range comments { - ret = append(ret, c) - } - sort.Slice(ret, func(i, j int) bool { - return ret[i].Pos().Before(ret[j].Pos()) - }) - queue := commentQueue(ret) - return &queue -} - -type commentQueue []*hcl1ast.CommentGroup - -func (q *commentQueue) TakeBeforeToken(token hcl1token.Token) []*hcl1ast.CommentGroup { - return q.TakeBeforePos(token.Pos) -} - -func (q *commentQueue) TakeBefore(node hcl1ast.Node) []*hcl1ast.CommentGroup { - return q.TakeBeforePos(node.Pos()) -} - -func (q *commentQueue) TakeBeforePos(pos hcl1token.Pos) []*hcl1ast.CommentGroup { - toPos := pos - var i int - for i = 0; i < len(*q); i++ { - if (*q)[i].Pos().After(toPos) { - break - } - } - if i == 0 { - return nil - } - - ret := (*q)[:i] - *q = (*q)[i:] - - return ret -} - -// hcl1NodeEndPos tries to find the latest possible position in the given -// node. This is primarily to try to find the last line number of a multi-line -// construct and is a best-effort sort of thing because HCL1 only tracks -// start positions for tokens and has no generalized way to find the full -// range for a single node. -func hcl1NodeEndPos(node hcl1ast.Node) hcl1token.Pos { - switch tn := node.(type) { - case *hcl1ast.ObjectItem: - if tn.LineComment != nil && len(tn.LineComment.List) > 0 { - return tn.LineComment.List[len(tn.LineComment.List)-1].Start - } - return hcl1NodeEndPos(tn.Val) - case *hcl1ast.ListType: - return tn.Rbrack - case *hcl1ast.ObjectType: - return tn.Rbrace - default: - // If all else fails, we'll just return the position of what we were given. - return tn.Pos() - } -} - -func hcl1ErrSubjectRange(filename string, err error) *hcl2.Range { - if pe, isPos := err.(*hcl1parser.PosError); isPos { - return hcl1PosRange(filename, pe.Pos).Ptr() - } - return nil -} - -func hcl1PosRange(filename string, pos hcl1token.Pos) hcl2.Range { - return hcl2.Range{ - Filename: filename, - Start: hcl2.Pos{ - Line: pos.Line, - Column: pos.Column, - Byte: pos.Offset, - }, - End: hcl2.Pos{ - Line: pos.Line, - Column: pos.Column, - Byte: pos.Offset, - }, - } -} - -func passthruBlockTodo(w io.Writer, node hcl1ast.Node, msg string) { - fmt.Fprintf(w, "\n# TF-UPGRADE-TODO: %s\n", msg) - hcl1printer.Fprint(w, node) - w.Write([]byte{'\n', '\n'}) -} - -func schemaHasSettableArguments(schema *configschema.Block) bool { - for _, attrS := range schema.Attributes { - if attrS.Optional || attrS.Required { - return true - } - } - for _, blockS := range schema.BlockTypes { - if schemaHasSettableArguments(&blockS.Block) { - return true - } - } - return false -} - -func invalidLabel(name string) bool { - matched, err := regexp.Match(`[0-9]`, []byte{name[0]}) - if err == nil { - return matched - } - // This isn't likely, but if there's an error here we'll just ignore it and - // move on. - return false -} - -func printLabelTodo(buf *bytes.Buffer, label string) { - buf.WriteString("# TF-UPGRADE-TODO: In Terraform v0.11 and earlier, it was possible to begin a\n" + - "# resource name with a number, but it is no longer possible in Terraform v0.12.\n" + - "#\n" + - "# Rename the resource and run `terraform state mv` to apply the rename in the\n" + - "# state. Detailed information on the `state move` command can be found in the\n" + - "# documentation online: https://www.terraform.io/docs/commands/state/mv.html\n", - ) -} diff --git a/configs/configupgrade/upgrade_test.go b/configs/configupgrade/upgrade_test.go deleted file mode 100644 index db15d850f..000000000 --- a/configs/configupgrade/upgrade_test.go +++ /dev/null @@ -1,308 +0,0 @@ -package configupgrade - -import ( - "bytes" - "flag" - "io" - "io/ioutil" - "log" - "os" - "os/exec" - "path/filepath" - "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/addrs" - backendinit "github.com/hashicorp/terraform/backend/init" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/helper/logging" - "github.com/hashicorp/terraform/providers" - "github.com/hashicorp/terraform/provisioners" - "github.com/hashicorp/terraform/terraform" -) - -func TestUpgradeValid(t *testing.T) { - // This test uses the contents of the testdata/valid directory as - // a table of tests. Every directory there must have both "input" and - // "want" subdirectories, where "input" is the configuration to be - // upgraded and "want" is the expected result. - fixtureDir := "testdata/valid" - testDirs, err := ioutil.ReadDir(fixtureDir) - if err != nil { - t.Fatal(err) - } - - for _, entry := range testDirs { - if !entry.IsDir() { - continue - } - t.Run(entry.Name(), func(t *testing.T) { - inputDir := filepath.Join(fixtureDir, entry.Name(), "input") - wantDir := filepath.Join(fixtureDir, entry.Name(), "want") - u := &Upgrader{ - Providers: providers.ResolverFixed(testProviders), - Provisioners: testProvisioners, - } - - inputSrc, err := LoadModule(inputDir) - if err != nil { - t.Fatal(err) - } - wantSrc, err := LoadModule(wantDir) - if err != nil { - t.Fatal(err) - } - - gotSrc, diags := u.Upgrade(inputSrc, inputDir) - if diags.HasErrors() { - t.Error(diags.Err()) - } - - // Upgrade uses a nil entry as a signal to delete a file, which - // we can't test here because we aren't modifying an existing - // dir in place, so we'll just ignore those and leave that mechanism - // to be tested elsewhere. - - for name, got := range gotSrc { - if gotSrc[name] == nil { - delete(gotSrc, name) - continue - } - want, wanted := wantSrc[name] - if !wanted { - t.Errorf("unexpected extra output file %q\n=== GOT ===\n%s", name, got) - continue - } - - got = bytes.TrimSpace(got) - want = bytes.TrimSpace(want) - if !bytes.Equal(got, want) { - diff := diffSourceFiles(got, want) - t.Errorf("wrong content in %q\n%s", name, diff) - } - } - - for name, want := range wantSrc { - if _, present := gotSrc[name]; !present { - t.Errorf("missing output file %q\n=== WANT ===\n%s", name, want) - } - } - }) - } -} - -func TestUpgradeRenameJSON(t *testing.T) { - inputDir := filepath.Join("testdata/valid/rename-json/input") - inputSrc, err := LoadModule(inputDir) - if err != nil { - t.Fatal(err) - } - - u := &Upgrader{ - Providers: providers.ResolverFixed(testProviders), - } - gotSrc, diags := u.Upgrade(inputSrc, inputDir) - if diags.HasErrors() { - t.Error(diags.Err()) - } - - // This test fixture is also fully covered by TestUpgradeValid, so - // we're just testing that the file was renamed here. - src, exists := gotSrc["misnamed-json.tf"] - if src != nil { - t.Errorf("misnamed-json.tf still has content") - } else if !exists { - t.Errorf("misnamed-json.tf not marked for deletion") - } - - src, exists = gotSrc["misnamed-json.tf.json"] - if src == nil || !exists { - t.Errorf("misnamed-json.tf.json was not created") - } -} - -func diffSourceFiles(got, want []byte) []byte { - // We'll try to run "diff -u" here to get nice output, but if that fails - // (e.g. because we're running on a machine without diff installed) then - // we'll fall back on just printing out the before and after in full. - gotR, gotW, err := os.Pipe() - if err != nil { - return diffSourceFilesFallback(got, want) - } - defer gotR.Close() - defer gotW.Close() - wantR, wantW, err := os.Pipe() - if err != nil { - return diffSourceFilesFallback(got, want) - } - defer wantR.Close() - defer wantW.Close() - - cmd := exec.Command("diff", "-u", "--label=GOT", "--label=WANT", "/dev/fd/3", "/dev/fd/4") - cmd.ExtraFiles = []*os.File{gotR, wantR} - stdout, err := cmd.StdoutPipe() - stderr, err := cmd.StderrPipe() - if err != nil { - return diffSourceFilesFallback(got, want) - } - - go func() { - wantW.Write(want) - wantW.Close() - }() - go func() { - gotW.Write(got) - gotW.Close() - }() - - err = cmd.Start() - if err != nil { - return diffSourceFilesFallback(got, want) - } - - outR := io.MultiReader(stdout, stderr) - out, err := ioutil.ReadAll(outR) - if err != nil { - return diffSourceFilesFallback(got, want) - } - - cmd.Wait() // not checking errors here because on failure we'll have stderr captured to return - - const noNewline = "\\ No newline at end of file\n" - if bytes.HasSuffix(out, []byte(noNewline)) { - out = out[:len(out)-len(noNewline)] - } - return out -} - -func diffSourceFilesFallback(got, want []byte) []byte { - var buf bytes.Buffer - buf.WriteString("=== GOT ===\n") - buf.Write(got) - buf.WriteString("\n=== WANT ===\n") - buf.Write(want) - buf.WriteString("\n") - return buf.Bytes() -} - -var testProviders = map[addrs.Provider]providers.Factory{ - addrs.NewLegacyProvider("test"): providers.Factory(func() (providers.Interface, error) { - p := &terraform.MockProvider{} - p.GetSchemaReturn = &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ - "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "type": {Type: cty.String, Optional: true}, - "image": {Type: cty.String, Optional: true}, - "tags": {Type: cty.Map(cty.String), Optional: true}, - "security_groups": {Type: cty.List(cty.String), Optional: true}, - "subnet_ids": {Type: cty.Set(cty.String), Optional: true}, - "list_of_obj": {Type: cty.List(cty.EmptyObject), Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "network": { - Nesting: configschema.NestingSet, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "cidr_block": {Type: cty.String, Optional: true}, - "subnet_cidrs": {Type: cty.Map(cty.String), Computed: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "subnet": { - Nesting: configschema.NestingSet, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "number": {Type: cty.Number, Required: true}, - }, - }, - }, - }, - }, - }, - "addresses": { - Nesting: configschema.NestingSingle, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "ipv4": {Type: cty.String, Computed: true}, - "ipv6": {Type: cty.String, Computed: true}, - }, - }, - }, - }, - }, - }, - } - return p, nil - }), - addrs.NewLegacyProvider("terraform"): providers.Factory(func() (providers.Interface, error) { - p := &terraform.MockProvider{} - p.GetSchemaReturn = &terraform.ProviderSchema{ - DataSources: map[string]*configschema.Block{ - "terraform_remote_state": { - // This is just enough an approximation of the remote state - // schema to check out reference upgrade logic. It is - // intentionally not fully-comprehensive. - Attributes: map[string]*configschema.Attribute{ - "backend": {Type: cty.String, Optional: true}, - }, - }, - }, - } - return p, nil - }), - addrs.NewLegacyProvider("aws"): providers.Factory(func() (providers.Interface, error) { - // This is here only so we can test the provisioner connection info - // migration behavior, which is resource-type specific. Do not use - // it in any other tests. - p := &terraform.MockProvider{} - p.GetSchemaReturn = &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ - "aws_instance": {}, - }, - } - return p, nil - }), -} - -var testProvisioners = map[string]provisioners.Factory{ - "test": provisioners.Factory(func() (provisioners.Interface, error) { - p := &terraform.MockProvisioner{} - p.GetSchemaResponse = provisioners.GetSchemaResponse{ - Provisioner: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "commands": {Type: cty.List(cty.String), Optional: true}, - "interpreter": {Type: cty.String, Optional: true}, - }, - }, - } - return p, nil - }), -} - -func init() { - // Initialize the backends - backendinit.Init(nil) -} - -func TestMain(m *testing.M) { - flag.Parse() - if testing.Verbose() { - // if we're verbose, use the logging requested by TF_LOG - logging.SetOutput() - } else { - // otherwise silence all logs - log.SetOutput(ioutil.Discard) - } - - // We have fmt.Stringer implementations on lots of objects that hide - // details that we very often want to see in tests, so we just disable - // spew's use of String methods globally on the assumption that spew - // usage implies an intent to see the raw values and ignore any - // abstractions. - spew.Config.DisableMethods = true - - os.Exit(m.Run()) -} diff --git a/configs/configupgrade/upgrader.go b/configs/configupgrade/upgrader.go deleted file mode 100644 index 67ca6af15..000000000 --- a/configs/configupgrade/upgrader.go +++ /dev/null @@ -1,13 +0,0 @@ -package configupgrade - -import ( - "github.com/hashicorp/terraform/providers" - "github.com/hashicorp/terraform/terraform" -) - -// Upgrader is the main type in this package, containing all of the -// dependencies that are needed to perform upgrades. -type Upgrader struct { - Providers providers.Resolver - Provisioners map[string]terraform.ProvisionerFactory -}