github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/012_config_upgrade.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/terraform/configs/configupgrade"
    13  	"github.com/hashicorp/terraform/terraform"
    14  	"github.com/hashicorp/terraform/tfdiags"
    15  )
    16  
    17  // ZeroTwelveUpgradeCommand is a Command implementation that can upgrade
    18  // the configuration files for a module from pre-0.11 syntax to new 0.12
    19  // idiom, while also flagging any suspicious constructs that will require
    20  // human review.
    21  type ZeroTwelveUpgradeCommand struct {
    22  	Meta
    23  }
    24  
    25  func (c *ZeroTwelveUpgradeCommand) Run(args []string) int {
    26  	args, err := c.Meta.process(args, true)
    27  	if err != nil {
    28  		return 1
    29  	}
    30  
    31  	var skipConfirm, force bool
    32  
    33  	flags := c.Meta.extendedFlagSet("0.12upgrade")
    34  	flags.BoolVar(&skipConfirm, "yes", false, "skip confirmation prompt")
    35  	flags.BoolVar(&force, "force", false, "override duplicate upgrade heuristic")
    36  	if err := flags.Parse(args); err != nil {
    37  		return 1
    38  	}
    39  
    40  	var diags tfdiags.Diagnostics
    41  
    42  	var dir string
    43  	args = flags.Args()
    44  	switch len(args) {
    45  	case 0:
    46  		dir = "."
    47  	case 1:
    48  		dir = args[0]
    49  	default:
    50  		diags = diags.Append(tfdiags.Sourceless(
    51  			tfdiags.Error,
    52  			"Too many arguments",
    53  			"The command 0.12upgrade expects only a single argument, giving the directory containing the module to upgrade.",
    54  		))
    55  		c.showDiagnostics(diags)
    56  		return 1
    57  	}
    58  
    59  	// Check for user-supplied plugin path
    60  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    61  		c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
    62  		return 1
    63  	}
    64  
    65  	dir = c.normalizePath(dir)
    66  
    67  	sources, err := configupgrade.LoadModule(dir)
    68  	if err != nil {
    69  		if os.IsNotExist(err) {
    70  			diags = diags.Append(tfdiags.Sourceless(
    71  				tfdiags.Error,
    72  				"Module directory not found",
    73  				fmt.Sprintf("The given directory %s does not exist.", dir),
    74  			))
    75  		} else {
    76  			diags = diags.Append(err)
    77  		}
    78  		c.showDiagnostics(diags)
    79  		return 1
    80  	}
    81  
    82  	if len(sources) == 0 {
    83  		diags = diags.Append(tfdiags.Sourceless(
    84  			tfdiags.Error,
    85  			"Not a module directory",
    86  			fmt.Sprintf("The given directory %s does not contain any Terraform configuration files.", dir),
    87  		))
    88  		c.showDiagnostics(diags)
    89  		return 1
    90  	}
    91  
    92  	// The config loader doesn't naturally populate our sources
    93  	// map, so we'll do it manually so our diagnostics can have
    94  	// source code snippets inside them.
    95  	// This is weird, but this whole upgrade codepath is pretty
    96  	// weird and temporary, so we'll accept it.
    97  	if loader, err := c.initConfigLoader(); err == nil {
    98  		parser := loader.Parser()
    99  		for name, src := range sources {
   100  			parser.ForceFileSource(filepath.Join(dir, name), src)
   101  		}
   102  	}
   103  
   104  	if !force {
   105  		// We'll check first if this directory already looks upgraded, so we
   106  		// don't waste the user's time dealing with an interactive prompt
   107  		// immediately followed by an error.
   108  		if already, rng := sources.MaybeAlreadyUpgraded(); already {
   109  			diags = diags.Append(&hcl.Diagnostic{
   110  				Severity: hcl.DiagError,
   111  				Summary:  "Module already upgraded",
   112  				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),
   113  				Subject:  rng.ToHCL().Ptr(),
   114  			})
   115  			c.showDiagnostics(diags)
   116  			return 1
   117  		}
   118  	}
   119  
   120  	if !skipConfirm {
   121  		c.Ui.Output(fmt.Sprintf(`
   122  This command will rewrite the configuration files in the given directory so
   123  that they use the new syntax features from Terraform v0.12, and will identify
   124  any constructs that may need to be adjusted for correct operation with
   125  Terraform v0.12.
   126  
   127  We recommend using this command in a clean version control work tree, so that
   128  you can easily see the proposed changes as a diff against the latest commit.
   129  If you have uncommited changes already present, we recommend aborting this
   130  command and dealing with them before running this command again.
   131  `))
   132  
   133  		query := "Would you like to upgrade the module in the current directory?"
   134  		if dir != "." {
   135  			query = fmt.Sprintf("Would you like to upgrade the module in %s?", dir)
   136  		}
   137  		v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
   138  			Id:          "approve",
   139  			Query:       query,
   140  			Description: `Only 'yes' will be accepted to confirm.`,
   141  		})
   142  		if err != nil {
   143  			diags = diags.Append(err)
   144  			c.showDiagnostics(diags)
   145  			return 1
   146  		}
   147  		if v != "yes" {
   148  			c.Ui.Info("Upgrade cancelled.")
   149  			return 0
   150  		}
   151  
   152  		c.Ui.Output(`-----------------------------------------------------------------------------`)
   153  	}
   154  
   155  	upgrader := &configupgrade.Upgrader{
   156  		Providers:    c.providerResolver(),
   157  		Provisioners: c.provisionerFactories(),
   158  	}
   159  	newSources, upgradeDiags := upgrader.Upgrade(sources, dir)
   160  	diags = diags.Append(upgradeDiags)
   161  	if upgradeDiags.HasErrors() {
   162  		c.showDiagnostics(diags)
   163  		return 2
   164  	}
   165  
   166  	// Now we'll write the contents of newSources into the filesystem.
   167  	for name, src := range newSources {
   168  		fn := filepath.Join(dir, name)
   169  		if src == nil {
   170  			// indicates a file to be deleted
   171  			err := os.Remove(fn)
   172  			if err != nil {
   173  				diags = diags.Append(tfdiags.Sourceless(
   174  					tfdiags.Error,
   175  					"Failed to remove file",
   176  					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),
   177  				))
   178  			}
   179  			continue
   180  		}
   181  
   182  		err := ioutil.WriteFile(fn, src, 0644)
   183  		if err != nil {
   184  			diags = diags.Append(tfdiags.Sourceless(
   185  				tfdiags.Error,
   186  				"Failed to write file",
   187  				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),
   188  			))
   189  		}
   190  	}
   191  
   192  	c.showDiagnostics(diags)
   193  	if diags.HasErrors() {
   194  		return 2
   195  	}
   196  
   197  	if !skipConfirm {
   198  		if len(diags) != 0 {
   199  			c.Ui.Output(`-----------------------------------------------------------------------------`)
   200  		}
   201  		c.Ui.Output(c.Colorize().Color(`
   202  [bold][green]Upgrade complete![reset]
   203  
   204  The configuration files were upgraded successfully. Use your version control
   205  system to review the proposed changes, make any necessary adjustments, and
   206  then commit.
   207  `))
   208  		if len(diags) != 0 {
   209  			// We checked for errors above, so these must be warnings.
   210  			c.Ui.Output(`Some warnings were generated during the upgrade, as shown above. These
   211  indicate situations where Terraform could not decide on an appropriate course
   212  of action without further human input.
   213  
   214  Where possible, these have also been marked with TF-UPGRADE-TODO comments to
   215  mark the locations where a decision must be made. After reviewing and adjusting
   216  these, manually remove the TF-UPGRADE-TODO comment before continuing.
   217  `)
   218  		}
   219  
   220  	}
   221  	return 0
   222  }
   223  
   224  func (c *ZeroTwelveUpgradeCommand) Help() string {
   225  	helpText := `
   226  Usage: terraform 0.12upgrade [module-dir]
   227  
   228    Rewrites the .tf files for a single module that was written for a Terraform
   229    version prior to v0.12 so that it uses new syntax features from v0.12
   230    and later.
   231  
   232    Also rewrites constructs that behave differently after v0.12, and flags any
   233    suspicious constructs that require human review,
   234  
   235    By default, 0.12upgrade rewrites the files in the current working directory.
   236    However, a path to a different directory can be provided. The command will
   237    prompt for confirmation interactively unless the -yes option is given.
   238  
   239  Options:
   240  
   241    -yes        Skip the initial introduction messages and interactive
   242                confirmation. This can be used to run this command in
   243                batch from a script.
   244  
   245    -force      Override the heuristic that attempts to detect if a
   246                configuration is already written for v0.12 or later.
   247                Some of the transformations made by this command are
   248                not idempotent, so re-running against the same module
   249                may change the meanings expressions in the module.
   250  `
   251  	return strings.TrimSpace(helpText)
   252  }
   253  
   254  func (c *ZeroTwelveUpgradeCommand) Synopsis() string {
   255  	return "Rewrites pre-0.12 module source code for v0.12"
   256  }