terraform/command/013_config_upgrade.go

217 lines
5.7 KiB
Go

package command
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/moduledeps"
"github.com/hashicorp/terraform/tfdiags"
)
// ZeroThirteenUpgradeCommand upgrades configuration files for a module
// to include explicit provider source settings
type ZeroThirteenUpgradeCommand struct {
Meta
}
func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
args, err := c.Meta.process(args, true)
if err != nil {
return 1
}
flags := c.Meta.defaultFlagSet("0.13upgrade")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
var diags tfdiags.Diagnostics
var dir string
args = flags.Args()
switch len(args) {
case 0:
dir = "."
case 1:
dir = args[0]
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Too many arguments",
"The command 0.13upgrade expects only a single argument, giving the directory containing the module to upgrade.",
))
c.showDiagnostics(diags)
return 1
}
// Check for user-supplied plugin path
if c.pluginPath, err = c.loadPluginPath(); err != nil {
c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
return 1
}
dir = c.normalizePath(dir)
// Upgrade only if some configuration is present
empty, err := configs.IsEmptyDir(dir)
if err != nil {
diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err))
return 1
}
if empty {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Not a module directory",
fmt.Sprintf("The given directory %s does not contain any Terraform configuration files.", dir),
))
c.showDiagnostics(diags)
return 1
}
// Early-load the config so that we can check provider dependencies
earlyConfig, earlyConfDiags := c.loadConfigEarly(dir)
if earlyConfDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Failed to load configuration"))
diags = diags.Append(earlyConfDiags)
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
}
}
// Find the provider dependencies
configDeps, depsDiags := earlyConfig.ProviderDependencies()
if depsDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Could not detect provider dependencies"))
diags = diags.Append(depsDiags)
c.showDiagnostics(diags)
return 1
}
// Detect source for each provider
providerSources, detectDiags := detectProviderSources(configDeps.Providers)
if detectDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Unable to detect sources for providers"))
diags = diags.Append(detectDiags)
c.showDiagnostics(diags)
return 1
}
if len(providerSources) == 0 {
c.Ui.Output("No non-default providers found. Your configuration is ready to use!")
return 0
}
// Generate the required providers configuration
genDiags := generateRequiredProviders(providerSources, dir)
diags = diags.Append(genDiags)
c.showDiagnostics(diags)
if diags.HasErrors() {
return 2
}
if len(diags) != 0 {
c.Ui.Output(`-----------------------------------------------------------------------------`)
}
c.Ui.Output(c.Colorize().Color(`
[bold][green]Upgrade complete![reset]
Use your version control system to review the proposed changes, make any
necessary adjustments, and then commit.
`))
return 0
}
// For providers which need a source attribute, detect and return source
// FIXME: currently does not filter or detect sources
func detectProviderSources(providers moduledeps.Providers) (map[string]string, tfdiags.Diagnostics) {
sources := make(map[string]string)
for provider := range providers {
sources[provider.Type] = provider.String()
}
return sources, nil
}
var providersTemplate = template.Must(template.New("providers.tf").Parse(`terraform {
required_providers {
{{- range $type, $source := .}}
{{$type}} = {
source = "{{$source}}"
}
{{- end}}
}
}
`))
// Generate a file with terraform.required_providers blocks for each provider
func generateRequiredProviders(providerSources map[string]string, dir string) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// Find unused file named "providers.tf", or fall back to e.g. "providers-1.tf"
path := filepath.Join(dir, "providers.tf")
if _, err := os.Stat(path); !os.IsNotExist(err) {
for i := 1; ; i++ {
path = filepath.Join(dir, fmt.Sprintf("providers-%d.tf", i))
if _, err := os.Stat(path); os.IsNotExist(err) {
break
}
}
}
f, err := os.Create(path)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to create providers file",
fmt.Sprintf("Error when generating providers configuration at '%s': %s", path, err),
))
return diags
}
defer f.Close()
err = providersTemplate.Execute(f, providerSources)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to create providers file",
fmt.Sprintf("Error when generating providers configuration at '%s': %s", path, err),
))
return diags
}
return nil
}
func (c *ZeroThirteenUpgradeCommand) Help() string {
helpText := `
Usage: terraform 0.13upgrade [module-dir]
Generates a "providers.tf" configuration file which includes source
configuration for every non-default provider.
`
return strings.TrimSpace(helpText)
}
func (c *ZeroThirteenUpgradeCommand) Synopsis() string {
return "Rewrites pre-0.13 module source code for v0.13"
}