From ffe5f7c4e60b8d759b1251ce81a2cb44c7190b8f Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 20 Jun 2018 19:27:14 -0700 Subject: [PATCH] command: 0.12upgrade command This is the frontend to the work-in-progress codepath for upgrading the source code for a module written for Terraform v0.11 or earlier to use the new syntax and idiom of v0.12. The underlying upgrade code is not yet complete as of this commit, and so the command is not yet very useful. We will continue to iterate on the upgrade code in subsequent commits. --- command/012_config_upgrade.go | 245 +++++++++++++++++++++++++++++++ commands.go | 7 + configs/configupgrade/upgrade.go | 3 +- configs/parser.go | 15 ++ 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 command/012_config_upgrade.go diff --git a/command/012_config_upgrade.go b/command/012_config_upgrade.go new file mode 100644 index 000000000..106e5fd37 --- /dev/null +++ b/command/012_config_upgrade.go @@ -0,0 +1,245 @@ +package command + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/hcl2/hcl" + "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.flagSet("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 + } + + 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(&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(`-----------------------------------------------------------------------------`) + } + + newSources, upgradeDiags := configupgrade.Upgrade(sources) + diags = diags.Append(upgradeDiags) + if upgradeDiags.HasErrors() { + c.showDiagnostics(diags) + return 2 + } + + // Now we'll write the contents of newSources into the filesystem. + for name, src := range newSources { + fn := filepath.Join(dir, name) + if src == nil { + // indicates a file to be deleted + err := os.Remove(fn) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to remove file", + fmt.Sprintf("The file %s must be renamed as part of the upgrade process, but the old file could not be deleted: %s.", fn, err), + )) + } + continue + } + + err := ioutil.WriteFile(fn, src, 0644) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to write file", + fmt.Sprintf("The file %s must be updated or created as part of the upgrade process, but there was an error while writing: %s.", fn, err), + )) + } + } + + c.showDiagnostics(diags) + if diags.HasErrors() { + return 2 + } + + if !skipConfirm { + if len(diags) != 0 { + c.Ui.Output(`-----------------------------------------------------------------------------`) + } + c.Ui.Output(c.Colorize().Color(` +[bold][green]Upgrade complete![reset] + +The configuration files were upgraded successfully. Use your version control +system to review the proposed changes, make any necessary adjustments, and +then commit. +`)) + if len(diags) != 0 { + // We checked for errors above, so these must be warnings. + c.Ui.Output(`Some warnings were generated during the upgrade, as shown above. These +indicate situations where Terraform could not decide on an appropriate course +of action without further human input. + +Where possible, these have also been marked with TF-UPGRADE-TODO comments to +mark the locations where a decision must be made. After reviewing and adjusting +these, manually remove the TF-UPGRADE-TODO comment before continuing. +`) + } + + } + return 0 +} + +func (c *ZeroTwelveUpgradeCommand) Help() string { + helpText := ` +Usage: terraform 0.12upgrade [module-dir] + + Rewrites the .tf files for a single module that was written for a Terraform + version prior to v0.12 so that it uses new syntax features from v0.12 + and later. + + Also rewrites constructs that behave differently after v0.12, and flags any + suspicious constructs that require human review, + + By default, 0.12upgrade rewrites the files in the current working directory. + However, a path to a different directory can be provided. The command will + prompt for confirmation interactively unless the -yes option is given. + +Options: + + -yes Skip the initial introduction messages and interactive + confirmation. This can be used to run this command in + batch from a script. + + -force Override the heuristic that attempts to detect if a + configuration is already written for v0.12 or later. + Some of the transformations made by this command are + not idempotent, so re-running against the same module + may change the meanings expressions in the module. +` + return strings.TrimSpace(helpText) +} + +func (c *ZeroTwelveUpgradeCommand) Synopsis() string { + return "Rewrites pre-0.12 module source code for v0.12" +} diff --git a/commands.go b/commands.go index 584763387..ac4ca02b0 100644 --- a/commands.go +++ b/commands.go @@ -74,6 +74,7 @@ func initCommands(config *Config, services *disco.Disco) { "debug": struct{}{}, // includes all subcommands "force-unlock": struct{}{}, "push": struct{}{}, + "0.12upgrade": struct{}{}, } Commands = map[string]cli.CommandFactory{ @@ -271,6 +272,12 @@ func initCommands(config *Config, services *disco.Disco) { // Plumbing //----------------------------------------------------------- + "0.12upgrade": func() (cli.Command, error) { + return &command.ZeroTwelveUpgradeCommand{ + Meta: meta, + }, nil + }, + "debug": func() (cli.Command, error) { return &command.DebugCommand{ Meta: meta, diff --git a/configs/configupgrade/upgrade.go b/configs/configupgrade/upgrade.go index 0707a8d4e..09c771b8a 100644 --- a/configs/configupgrade/upgrade.go +++ b/configs/configupgrade/upgrade.go @@ -61,6 +61,7 @@ func Upgrade(input ModuleSources) (ModuleSources, tfdiags.Diagnostics) { ret[name] = nil // mark for deletion oldName := name name = input.UnusedFilename(name + ".json") + ret[name] = src diags = diags.Append(&hcl2.Diagnostic{ Severity: hcl2.DiagWarning, @@ -70,6 +71,7 @@ func Upgrade(input ModuleSources) (ModuleSources, tfdiags.Diagnostics) { oldName, name, ), }) + continue } } @@ -77,7 +79,6 @@ func Upgrade(input ModuleSources) (ModuleSources, tfdiags.Diagnostics) { // 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. - ret[name] = src diags = diags.Append(&hcl2.Diagnostic{ Severity: hcl2.DiagWarning, Summary: "JSON configuration file was not rewritten", diff --git a/configs/parser.go b/configs/parser.go index 3d3f07ff1..8176fa1b7 100644 --- a/configs/parser.go +++ b/configs/parser.go @@ -83,3 +83,18 @@ func (p *Parser) LoadHCLFile(path string) (hcl.Body, hcl.Diagnostics) { func (p *Parser) Sources() map[string][]byte { return p.p.Sources() } + +// ForceFileSource artificially adds source code to the cache of file sources, +// as if it had been loaded from the given filename. +// +// This should be used only in special situations where configuration is loaded +// some other way. Most callers should load configuration via methods of +// Parser, which will update the sources cache automatically. +func (p *Parser) ForceFileSource(filename string, src []byte) { + // We'll make a synthetic hcl.File here just so we can reuse the + // existing cache. + p.p.AddFile(filename, &hcl.File{ + Body: hcl.EmptyBody(), + Bytes: src, + }) +}