590 lines
18 KiB
Go
590 lines
18 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"
|
|
"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",
|
|
),
|
|
))
|
|
}
|
|
|
|
// 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"
|
|
}
|