terraform/command/013_config_upgrade.go

612 lines
19 KiB
Go

package command
import (
"fmt"
"io/ioutil"
"os"
"path"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty"
)
// ZeroThirteenUpgradeCommand upgrades configuration files for a module
// to include explicit provider source settings
type ZeroThirteenUpgradeCommand struct {
Meta
}
// Warning diagnostic detail message used for JSON and override config files
const skippedConfigurationFileWarning = "The %s configuration file %q was skipped, because %s files are assumed to be generated. The program that generated this file may need to be updated for changes to the configuration language."
func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
args = c.Meta.process(args)
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
var err error
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
}
// Set up the config loader and find all the config files
loader, err := c.initConfigLoader()
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
parser := loader.Parser()
primary, overrides, hclDiags := parser.ConfigDirFiles(dir)
diags = diags.Append(hclDiags)
if diags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Failed to load configuration"))
c.showDiagnostics(diags)
return 1
}
// Load and parse all primary files
files := make(map[string]*configs.File)
for _, path := range primary {
// Skip JSON configuration files, because we can't rewrite them and
// they're probably generated anyway.
if strings.HasSuffix(strings.ToLower(path), ".json") {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"JSON configuration file was not rewritten",
fmt.Sprintf(
skippedConfigurationFileWarning,
"JSON",
path,
"JSON",
),
))
continue
}
file, fileDiags := parser.LoadConfigFile(path)
diags = diags.Append(fileDiags)
if file != nil {
files[path] = file
}
}
if diags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Failed to load configuration"))
c.showDiagnostics(diags)
return 1
}
// It's not clear what the correct behaviour is for upgrading override
// files. For now, just log that we're ignoring the file.
for _, path := range overrides {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Override configuration file was not rewritten",
fmt.Sprintf(
skippedConfigurationFileWarning,
"override",
path,
"override",
),
))
}
// Check Terraform required_version constraints
for _, file := range files {
for _, constraint := range file.CoreVersionConstraints {
if !constraint.Required.Check(tfversion.SemVer) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported Terraform Core version",
Detail: fmt.Sprintf(
"This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.",
tfversion.String(),
),
Subject: &constraint.DeclRange,
})
}
}
}
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
// Build up a list of required providers, uniquely by local name
requiredProviders := make(map[string]*configs.RequiredProvider)
var rewritePaths []string
// Step 1: copy all explicit provider requirements across
for path, file := range files {
for _, rps := range file.RequiredProviders {
rewritePaths = append(rewritePaths, path)
for _, rp := range rps.RequiredProviders {
if previous, exist := requiredProviders[rp.Name]; exist {
diags = diags.Append(&hcl.Diagnostic{
Summary: "Duplicate required provider configuration",
Detail: fmt.Sprintf("Found duplicate required provider configuration for %q.Previously configured at %s", rp.Name, previous.DeclRange),
Severity: hcl.DiagWarning,
Context: rps.DeclRange.Ptr(),
Subject: rp.DeclRange.Ptr(),
})
} else {
// We're copying the struct here to ensure that any
// mutation does not affect the original, if we rewrite
// this file
requiredProviders[rp.Name] = &configs.RequiredProvider{
Name: rp.Name,
Source: rp.Source,
Type: rp.Type,
Requirement: rp.Requirement,
DeclRange: rp.DeclRange,
}
}
}
}
}
for _, file := range files {
// Step 2: add missing provider requirements from provider blocks
for _, p := range file.ProviderConfigs {
// If no explicit provider configuration exists for the
// provider configuration's local name, add one with a legacy
// provider address.
if _, exist := requiredProviders[p.Name]; !exist {
requiredProviders[p.Name] = &configs.RequiredProvider{
Name: p.Name,
}
}
}
// Step 3: add missing provider requirements from resources
resources := [][]*configs.Resource{file.ManagedResources, file.DataResources}
for _, rs := range resources {
for _, r := range rs {
// Find the appropriate provider local name for this resource
var localName string
// If there's a provider config, use that to determine the
// local name. Otherwise use the implied provider local name
// based on the resource's address.
if r.ProviderConfigRef != nil {
localName = r.ProviderConfigRef.Name
} else {
localName = r.Addr().ImpliedProvider()
}
// If no explicit provider configuration exists for this local
// name, add one with a legacy provider address.
if _, exist := requiredProviders[localName]; !exist {
requiredProviders[localName] = &configs.RequiredProvider{
Name: localName,
}
}
}
}
}
// We should now have a complete understanding of the provider requirements
// stated in the config. If there are any providers, attempt to detect
// their sources, and rewrite the config.
if len(requiredProviders) > 0 {
detectDiags := c.detectProviderSources(requiredProviders)
diags = diags.Append(detectDiags)
if diags.HasErrors() {
c.Ui.Error("Unable to detect sources for providers")
c.showDiagnostics(diags)
return 1
}
// Default output filename is "providers.tf"
filename := path.Join(dir, "providers.tf")
// Special case: if we only have one file with a required providers
// block, output to that file instead.
if len(rewritePaths) == 1 {
filename = rewritePaths[0]
}
// Remove the output file from the list of paths we want to rewrite
// later. Otherwise we'd delete the required providers block after
// writing it.
for i, path := range rewritePaths {
if path == filename {
rewritePaths = append(rewritePaths[:i], rewritePaths[i+1:]...)
break
}
}
var out *hclwrite.File
// If the output file doesn't exist, just create a new empty file
if _, err := os.Stat(filename); os.IsNotExist(err) {
out = hclwrite.NewEmptyFile()
} else if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to read configuration file",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
} else {
// Configuration file already exists, so load and parse it
config, err := ioutil.ReadFile(filename)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to read configuration file",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
var parseDiags hcl.Diagnostics
out, parseDiags = hclwrite.ParseConfig(config, filename, hcl.InitialPos)
diags = diags.Append(parseDiags)
}
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
// Find all required_providers blocks, and store them alongside a map
// back to the parent terraform block.
var requiredProviderBlocks []*hclwrite.Block
parentBlocks := make(map[*hclwrite.Block]*hclwrite.Block)
root := out.Body()
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() != "terraform" {
continue
}
for _, childBlock := range rootBlock.Body().Blocks() {
if childBlock.Type() == "required_providers" {
requiredProviderBlocks = append(requiredProviderBlocks, childBlock)
parentBlocks[childBlock] = rootBlock
}
}
}
// First required provider block, and the rest found in this file.
var first *hclwrite.Block
var rest []*hclwrite.Block
if len(requiredProviderBlocks) > 0 {
// If we already have one or more required provider blocks, we'll rewrite
// the first one, and remove the rest.
first, rest = requiredProviderBlocks[0], requiredProviderBlocks[1:]
} else {
// Otherwise, find or a create a terraform block, and add a new
// empty required providers block to it.
var tfBlock *hclwrite.Block
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() == "terraform" {
tfBlock = rootBlock
break
}
}
if tfBlock == nil {
tfBlock = root.AppendNewBlock("terraform", nil)
}
first = tfBlock.Body().AppendNewBlock("required_providers", nil)
}
// Find the body of the first block to prepare for rewriting it
body := first.Body()
// Build a sorted list of provider local names, for consistent ordering
var localNames []string
for localName := range requiredProviders {
localNames = append(localNames, localName)
}
sort.Strings(localNames)
// Populate the required providers block
for _, localName := range localNames {
requiredProvider := requiredProviders[localName]
var attributes = make(map[string]cty.Value)
if !requiredProvider.Type.IsZero() {
attributes["source"] = cty.StringVal(requiredProvider.Type.ForDisplay())
}
if version := requiredProvider.Requirement.Required.String(); version != "" {
attributes["version"] = cty.StringVal(version)
}
var attributesObject cty.Value
if len(attributes) > 0 {
attributesObject = cty.ObjectVal(attributes)
} else {
attributesObject = cty.EmptyObjectVal
}
body.SetAttributeValue(localName, attributesObject)
// If we don't have a source attribute, manually construct a commented
// block explaining what to do
if _, hasSource := attributes["source"]; !hasSource {
// Generate the token stream for the required provider
rp := body.GetAttribute(localName)
expr := rp.Expr().BuildTokens(nil)
// Partition the tokens into before and after the opening brace
before, after := partitionTokensAfter(expr, hclsyntax.TokenOBrace)
// If the value is an empty object, add a newline between the
// braces so that the comment is not on the same line as either
// brace.
if len(before) == 1 && len(after) == 1 {
newline := &hclwrite.Token{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
}
after = append(hclwrite.Tokens{newline}, after...)
}
// Generate the comment and insert it at the start of the object
comment := noSourceDetectedComment(localName)
commentedBlock := append(before, comment...)
commentedBlock = append(commentedBlock, after...)
// Set the required provider object to this raw token stream
body.SetAttributeRaw(localName, commentedBlock)
}
}
// Remove the rest of the blocks (and the parent block, if it's empty)
for _, rpBlock := range rest {
tfBlock := parentBlocks[rpBlock]
tfBody := tfBlock.Body()
tfBody.RemoveBlock(rpBlock)
// If the terraform block has no blocks and no attributes, it's
// basically empty (aside from comments and whitespace), so it's
// more useful to remove it than leave it in.
if len(tfBody.Blocks()) == 0 && len(tfBody.Attributes()) == 0 {
root.RemoveBlock(tfBlock)
}
}
// Write the config back to the file
f, err := os.OpenFile(filename, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to open configuration file for writing",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
_, err = out.WriteTo(f)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to rewrite configuration file",
fmt.Sprintf("Error when rewriting configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
// After successfully writing the new configuration, remove all other
// required provider blocks from remaining configuration files.
for _, path := range rewritePaths {
// Read and parse the existing file
config, err := ioutil.ReadFile(path)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to read configuration file",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
file, parseDiags := hclwrite.ParseConfig(config, filename, hcl.InitialPos)
diags = diags.Append(parseDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
// Find and remove all terraform.required_providers blocks
root := file.Body()
for _, rootBlock := range root.Blocks() {
if rootBlock.Type() != "terraform" {
continue
}
tfBody := rootBlock.Body()
for _, childBlock := range tfBody.Blocks() {
if childBlock.Type() == "required_providers" {
rootBlock.Body().RemoveBlock(childBlock)
// If the terraform block is now empty, remove it
if len(tfBody.Blocks()) == 0 && len(tfBody.Attributes()) == 0 {
root.RemoveBlock(rootBlock)
}
}
}
}
// Write the config back to the file
f, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to open configuration file for writing",
fmt.Sprintf("Error when reading configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
_, err = file.WriteTo(f)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to rewrite configuration file",
fmt.Sprintf("Error when rewriting configuration file %q: %s", filename, err),
))
c.showDiagnostics(diags)
return 1
}
}
}
c.showDiagnostics(diags)
if diags.HasErrors() {
return 1
}
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 the source
func (c *ZeroThirteenUpgradeCommand) detectProviderSources(requiredProviders map[string]*configs.RequiredProvider) tfdiags.Diagnostics {
source := c.providerInstallSource()
var diags tfdiags.Diagnostics
for name, rp := range requiredProviders {
// If there's already an explicit source, skip it
if rp.Source != "" {
continue
}
// Construct a legacy provider FQN using the required provider local
// name. This ignores any auto-generated provider FQN from the load &
// parse process, because we know that without an explicit source it is
// not explicitly specified.
addr := addrs.NewLegacyProvider(name)
p, err := getproviders.LookupLegacyProvider(addr, source)
if err == nil {
rp.Type = p
} else {
if _, ok := err.(getproviders.ErrProviderNotKnown); ok {
// Setting the provider address to a zero value struct
// indicates that there is no known FQN for this provider,
// which will cause us to write an explanatory comment in the
// HCL output advising the user what to do about this.
rp.Type = addrs.Provider{}
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Could not detect provider source",
fmt.Sprintf("Error looking up provider source for %q: %s", name, err),
))
}
}
return diags
}
// Take a list of tokens and a separator token, and return two lists: one up to
// and including the first instance of the separator, and the rest of the
// tokens. If the separator is not present, return the entire list in the first
// return value.
func partitionTokensAfter(tokens hclwrite.Tokens, separator hclsyntax.TokenType) (hclwrite.Tokens, hclwrite.Tokens) {
for i := 0; i < len(tokens); i++ {
if tokens[i].Type == separator {
return tokens[0 : i+1], tokens[i+1:]
}
}
return tokens, nil
}
// Generate a list of tokens for a comment explaining that a provider source
// could not be detected.
func noSourceDetectedComment(name string) hclwrite.Tokens {
comment := fmt.Sprintf(`# TF-UPGRADE-TODO
#
# No source detected for this provider. You must add a source address
# in the following format:
#
# source = "your-registry.example.com/organization/%s"
#
# For more information, see the provider source documentation:
#
# https://www.terraform.io/docs/configuration/providers.html#provider-source`, name)
var tokens hclwrite.Tokens
for _, line := range strings.Split(comment, "\n") {
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}})
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenComment, Bytes: []byte(line)})
}
return tokens
}
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"
}