command: Rework 0.13upgrade sub-command
This commit implements most of the intended functionality of the upgrade command for rewriting configurations. For a given module, it makes a list of all providers in use. Then it attempts to detect the source address for providers without an explicit source. Once this step is complete, the tool rewrites the relevant configuration files. This results in a single "required_providers" block for the module, with a source for each provider. Any providers for which the source cannot be detected (for example, unofficial providers) will need a source to be defined by the user. The tool writes an explanatory comment to the configuration to help with this.
This commit is contained in:
parent
5b307a07dc
commit
ae98bd12a7
|
@ -2,15 +2,20 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
|
|
||||||
|
"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/configs"
|
||||||
"github.com/hashicorp/terraform/internal/initwd"
|
"github.com/hashicorp/terraform/internal/getproviders"
|
||||||
"github.com/hashicorp/terraform/moduledeps"
|
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ZeroThirteenUpgradeCommand upgrades configuration files for a module
|
// ZeroThirteenUpgradeCommand upgrades configuration files for a module
|
||||||
|
@ -19,6 +24,9 @@ type ZeroThirteenUpgradeCommand struct {
|
||||||
Meta
|
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 {
|
func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
|
||||||
args = c.Meta.process(args)
|
args = c.Meta.process(args)
|
||||||
flags := c.Meta.defaultFlagSet("0.13upgrade")
|
flags := c.Meta.defaultFlagSet("0.13upgrade")
|
||||||
|
@ -71,58 +79,412 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early-load the config so that we can check provider dependencies
|
// Set up the config loader and find all the config files
|
||||||
earlyConfig, earlyConfDiags := c.loadConfigEarly(dir)
|
loader, err := c.initConfigLoader()
|
||||||
if earlyConfDiags.HasErrors() {
|
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.Ui.Error(strings.TrimSpace("Failed to load configuration"))
|
||||||
diags = diags.Append(earlyConfDiags)
|
|
||||||
c.showDiagnostics(diags)
|
c.showDiagnostics(diags)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// Load and parse all primary files
|
||||||
// Before we go further, we'll check to make sure none of the modules
|
files := make(map[string]*configs.File)
|
||||||
// in the configuration declare that they don't support this Terraform
|
for _, path := range primary {
|
||||||
// version, so we can produce a version-related error message rather
|
// Skip JSON configuration files, because we can't rewrite them and
|
||||||
// than potentially-confusing downstream errors.
|
// they're probably generated anyway.
|
||||||
versionDiags := initwd.CheckCoreVersionRequirements(earlyConfig)
|
if strings.HasSuffix(strings.ToLower(path), ".json") {
|
||||||
diags = diags.Append(versionDiags)
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
if versionDiags.HasErrors() {
|
tfdiags.Warning,
|
||||||
c.showDiagnostics(diags)
|
"JSON configuration file was not rewritten",
|
||||||
return 1
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the provider dependencies
|
for _, file := range files {
|
||||||
configDeps, depsDiags := earlyConfig.ProviderDependencies()
|
// Step 2: add missing provider requirements from provider blocks
|
||||||
if depsDiags.HasErrors() {
|
for _, p := range file.ProviderConfigs {
|
||||||
c.Ui.Error(strings.TrimSpace("Could not detect provider dependencies"))
|
// If no explicit provider configuration exists for the
|
||||||
diags = diags.Append(depsDiags)
|
// provider configuration's local name, add one with a legacy
|
||||||
c.showDiagnostics(diags)
|
// provider address.
|
||||||
return 1
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect source for each provider
|
// We should now have a complete understanding of the provider requirements
|
||||||
providerSources, detectDiags := detectProviderSources(configDeps.Providers)
|
// stated in the config. If there are any providers, attempt to detect
|
||||||
if detectDiags.HasErrors() {
|
// their sources, and rewrite the config.
|
||||||
c.Ui.Error(strings.TrimSpace("Unable to detect sources for providers"))
|
if len(requiredProviders) > 0 {
|
||||||
|
detectDiags := c.detectProviderSources(requiredProviders)
|
||||||
diags = diags.Append(detectDiags)
|
diags = diags.Append(detectDiags)
|
||||||
c.showDiagnostics(diags)
|
if diags.HasErrors() {
|
||||||
return 1
|
c.Ui.Error("Unable to detect sources for providers")
|
||||||
}
|
c.showDiagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
if len(providerSources) == 0 {
|
// Default output filename is "providers.tf"
|
||||||
c.Ui.Output("No non-default providers found. Your configuration is ready to use!")
|
filename := path.Join(dir, "providers.tf")
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the required providers configuration
|
// Special case: if we only have one file with a required providers
|
||||||
genDiags := generateRequiredProviders(providerSources, dir)
|
// block, output to that file instead.
|
||||||
diags = diags.Append(genDiags)
|
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)
|
c.showDiagnostics(diags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return 2
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(diags) != 0 {
|
if len(diags) != 0 {
|
||||||
|
@ -138,64 +500,78 @@ necessary adjustments, and then commit.
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// For providers which need a source attribute, detect and return source
|
// For providers which need a source attribute, detect the source
|
||||||
// FIXME: currently does not filter or detect sources
|
func (c *ZeroThirteenUpgradeCommand) detectProviderSources(requiredProviders map[string]*configs.RequiredProvider) tfdiags.Diagnostics {
|
||||||
func detectProviderSources(providers moduledeps.Providers) (map[string]string, tfdiags.Diagnostics) {
|
source := c.providerInstallSource()
|
||||||
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
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
// Find unused file named "providers.tf", or fall back to e.g. "providers-1.tf"
|
for name, rp := range requiredProviders {
|
||||||
path := filepath.Join(dir, "providers.tf")
|
// If there's already an explicit source, skip it
|
||||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
if rp.Source != "" {
|
||||||
for i := 1; ; i++ {
|
continue
|
||||||
path = filepath.Join(dir, fmt.Sprintf("providers-%d.tf", i))
|
}
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
||||||
break
|
// 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),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Create(path)
|
return diags
|
||||||
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)
|
// Take a list of tokens and a separator token, and return two lists: one up to
|
||||||
if err != nil {
|
// and including the first instance of the separator, and the rest of the
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
// tokens. If the separator is not present, return the entire list in the first
|
||||||
tfdiags.Error,
|
// return value.
|
||||||
"Unable to create providers file",
|
func partitionTokensAfter(tokens hclwrite.Tokens, separator hclsyntax.TokenType) (hclwrite.Tokens, hclwrite.Tokens) {
|
||||||
fmt.Sprintf("Error when generating providers configuration at '%s': %s", path, err),
|
for i := 0; i < len(tokens); i++ {
|
||||||
))
|
if tokens[i].Type == separator {
|
||||||
return diags
|
return tokens[0 : i+1], tokens[i+1:]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
func (c *ZeroThirteenUpgradeCommand) Help() string {
|
||||||
|
|
|
@ -1,44 +1,115 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
|
"github.com/hashicorp/terraform-svchost/disco"
|
||||||
"github.com/hashicorp/terraform/helper/copy"
|
"github.com/hashicorp/terraform/helper/copy"
|
||||||
|
"github.com/hashicorp/terraform/internal/getproviders"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestZeroThirteenUpgrade_success(t *testing.T) {
|
// This map from provider type name to namespace is used by the fake registry
|
||||||
testCases := map[string]struct {
|
// when called via LookupLegacyProvider. Providers not in this map will return
|
||||||
path string
|
// a 404 Not Found error.
|
||||||
args []string
|
var legacyProviderNamespaces = map[string]string{
|
||||||
out string
|
"foo": "hashicorp",
|
||||||
}{
|
"bar": "hashicorp",
|
||||||
"implicit": {
|
"baz": "terraform-providers",
|
||||||
path: "013upgrade-implicit-providers",
|
}
|
||||||
out: "providers.tf",
|
|
||||||
},
|
func verifyExpectedFiles(t *testing.T, expectedPath string) {
|
||||||
"explicit": {
|
// Compare output and expected file trees
|
||||||
path: "013upgrade-explicit-providers",
|
var outputFiles, expectedFiles []string
|
||||||
out: "providers.tf",
|
|
||||||
},
|
// Gather list of output files in the current working directory
|
||||||
"subdir": {
|
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
||||||
path: "013upgrade-subdir",
|
if !info.IsDir() {
|
||||||
args: []string{"subdir"},
|
outputFiles = append(outputFiles, path)
|
||||||
out: "subdir/providers.tf",
|
}
|
||||||
},
|
return nil
|
||||||
"fileExists": {
|
})
|
||||||
path: "013upgrade-file-exists",
|
if err != nil {
|
||||||
out: "providers-1.tf",
|
t.Fatal("error listing output files:", err)
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for name, tc := range testCases {
|
|
||||||
|
// Gather list of expected files
|
||||||
|
revertChdir := testChdir(t, expectedPath)
|
||||||
|
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
||||||
|
if !info.IsDir() {
|
||||||
|
expectedFiles = append(expectedFiles, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("error listing expected files:", err)
|
||||||
|
}
|
||||||
|
revertChdir()
|
||||||
|
|
||||||
|
// If the file trees don't match, give up early
|
||||||
|
if diff := cmp.Diff(expectedFiles, outputFiles); diff != "" {
|
||||||
|
t.Fatalf("expected and output file trees do not match\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the contents of each file is correct
|
||||||
|
for _, filePath := range outputFiles {
|
||||||
|
output, err := ioutil.ReadFile(path.Join(".", filePath))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output %s: %s", filePath, err)
|
||||||
|
}
|
||||||
|
expected, err := ioutil.ReadFile(path.Join(expectedPath, filePath))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read expected %s: %s", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(expected, output); diff != "" {
|
||||||
|
t.Fatalf("expected and output file for %s do not match\n%s", filePath, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZeroThirteenUpgrade_success(t *testing.T) {
|
||||||
|
registrySource, close := testRegistrySource(t)
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
testCases := map[string]string{
|
||||||
|
"implicit": "013upgrade-implicit-providers",
|
||||||
|
"explicit": "013upgrade-explicit-providers",
|
||||||
|
"provider not found": "013upgrade-provider-not-found",
|
||||||
|
"implicit not found": "013upgrade-implicit-not-found",
|
||||||
|
"file exists": "013upgrade-file-exists",
|
||||||
|
"no providers": "013upgrade-no-providers",
|
||||||
|
"submodule": "013upgrade-submodule",
|
||||||
|
"providers with source": "013upgrade-providers-with-source",
|
||||||
|
"preserves comments": "013upgrade-preserves-comments",
|
||||||
|
"multiple blocks": "013upgrade-multiple-blocks",
|
||||||
|
"multiple files": "013upgrade-multiple-files",
|
||||||
|
"existing providers.tf": "013upgrade-existing-providers-tf",
|
||||||
|
"skipped files": "013upgrade-skipped-files",
|
||||||
|
}
|
||||||
|
for name, testPath := range testCases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
inputPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "input")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to find input path %s: %s", testPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "expected")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to find expected path %s: %s", testPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
td := tempDir(t)
|
td := tempDir(t)
|
||||||
copy.CopyDir(testFixturePath(tc.path), td)
|
copy.CopyDir(inputPath, td)
|
||||||
defer os.RemoveAll(td)
|
defer os.RemoveAll(td)
|
||||||
defer testChdir(t, td)()
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
@ -46,11 +117,12 @@ func TestZeroThirteenUpgrade_success(t *testing.T) {
|
||||||
c := &ZeroThirteenUpgradeCommand{
|
c := &ZeroThirteenUpgradeCommand{
|
||||||
Meta: Meta{
|
Meta: Meta{
|
||||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||||
|
ProviderSource: registrySource,
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if code := c.Run(tc.args); code != 0 {
|
if code := c.Run(nil); code != 0 {
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,22 +131,95 @@ func TestZeroThirteenUpgrade_success(t *testing.T) {
|
||||||
t.Fatal("unexpected output:", output)
|
t.Fatal("unexpected output:", output)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual, err := ioutil.ReadFile(tc.out)
|
verifyExpectedFiles(t, expectedPath)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read output %s: %s", tc.out, err)
|
|
||||||
}
|
|
||||||
expected, err := ioutil.ReadFile("expected/providers.tf")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("failed to read expected/providers.tf", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(actual, expected) {
|
|
||||||
t.Fatalf("actual output: \n%s\nexpected output: \n%s", string(actual), string(expected))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that non-default upgrade paths are supported, and that the output is
|
||||||
|
// in the correct place. This test is very similar to the table tests above,
|
||||||
|
// but with a different expected output path, and with an argument passed to
|
||||||
|
// the Run call.
|
||||||
|
func TestZeroThirteenUpgrade_submodule(t *testing.T) {
|
||||||
|
registrySource, close := testRegistrySource(t)
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
testPath := "013upgrade-submodule"
|
||||||
|
|
||||||
|
inputPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "input")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to find input path %s: %s", testPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The expected output for processing a submodule is different
|
||||||
|
expectedPath, err := filepath.Abs(testFixturePath(path.Join(testPath, "expected-module")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to find expected path %s: %s", testPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(inputPath, td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ZeroThirteenUpgradeCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||||
|
ProviderSource: registrySource,
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we pass a target module directory to process
|
||||||
|
if code := c.Run([]string{"module"}); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if !strings.Contains(output, "Upgrade complete") {
|
||||||
|
t.Fatal("unexpected output:", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyExpectedFiles(t, expectedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that JSON and override files are skipped with a warning. Generated
|
||||||
|
// output for this config is verified in the table driven tests above.
|
||||||
|
func TestZeroThirteenUpgrade_skippedFiles(t *testing.T) {
|
||||||
|
inputPath := testFixturePath(path.Join("013upgrade-skipped-files", "input"))
|
||||||
|
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(inputPath, td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &ZeroThirteenUpgradeCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if code := c.Run(nil); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if !strings.Contains(output, "Upgrade complete") {
|
||||||
|
t.Fatal("unexpected output:", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
errMsg := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(errMsg, `The JSON configuration file "variables.tf.json" was skipped`) {
|
||||||
|
t.Fatal("missing JSON skipped file warning:", errMsg)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errMsg, `The override configuration file "bar_override.tf" was skipped`) {
|
||||||
|
t.Fatal("missing override skipped file warning:", errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestZeroThirteenUpgrade_invalidFlags(t *testing.T) {
|
func TestZeroThirteenUpgrade_invalidFlags(t *testing.T) {
|
||||||
td := tempDir(t)
|
td := tempDir(t)
|
||||||
os.MkdirAll(td, 0755)
|
os.MkdirAll(td, 0755)
|
||||||
|
@ -147,54 +292,66 @@ func TestZeroThirteenUpgrade_empty(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestZeroThirteenUpgrade_invalidProviderVersion(t *testing.T) {
|
// testServices starts up a local HTTP server running a fake provider registry
|
||||||
td := tempDir(t)
|
// service which responds only to discovery requests and legacy provider lookup
|
||||||
copy.CopyDir(testFixturePath("013upgrade-invalid"), td)
|
// API calls.
|
||||||
defer os.RemoveAll(td)
|
//
|
||||||
defer testChdir(t, td)()
|
// The final return value is a function to call at the end of a test function
|
||||||
|
// to shut down the test server. After you call that function, the discovery
|
||||||
|
// object becomes useless.
|
||||||
|
func testServices(t *testing.T) (services *disco.Disco, cleanup func()) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler))
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
services = disco.New()
|
||||||
c := &ZeroThirteenUpgradeCommand{
|
services.ForceHostServices(svchost.Hostname("registry.terraform.io"), map[string]interface{}{
|
||||||
Meta: Meta{
|
"providers.v1": server.URL + "/providers/v1/",
|
||||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
})
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if code := c.Run(nil); code == 0 {
|
return services, func() {
|
||||||
t.Fatal("expected error, got:", ui.OutputWriter)
|
server.Close()
|
||||||
}
|
|
||||||
|
|
||||||
errMsg := ui.ErrorWriter.String()
|
|
||||||
if !strings.Contains(errMsg, "Invalid provider version constraint") {
|
|
||||||
t.Fatal("unexpected error:", errMsg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestZeroThirteenUpgrade_noProviders(t *testing.T) {
|
// testRegistrySource is a wrapper around testServices that uses the created
|
||||||
td := tempDir(t)
|
// discovery object to produce a Source instance that is ready to use with the
|
||||||
copy.CopyDir(testFixturePath("013upgrade-no-providers"), td)
|
// fake registry services.
|
||||||
defer os.RemoveAll(td)
|
//
|
||||||
defer testChdir(t, td)()
|
// As with testServices, the final return value is a function to call at the end
|
||||||
|
// of your test in order to shut down the test server.
|
||||||
|
func testRegistrySource(t *testing.T) (source *getproviders.RegistrySource, cleanup func()) {
|
||||||
|
services, close := testServices(t)
|
||||||
|
source = getproviders.NewRegistrySource(services)
|
||||||
|
return source, close
|
||||||
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
|
||||||
c := &ZeroThirteenUpgradeCommand{
|
path := req.URL.EscapedPath()
|
||||||
Meta: Meta{
|
|
||||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
if !strings.HasPrefix(path, "/providers/v1/") {
|
||||||
Ui: ui,
|
resp.WriteHeader(404)
|
||||||
},
|
resp.Write([]byte(`not a provider registry endpoint`))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if code := c.Run(nil); code != 0 {
|
pathParts := strings.Split(path, "/")[3:]
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
|
if len(pathParts) != 2 {
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
resp.Write([]byte(`unrecognized path scheme`))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output := ui.OutputWriter.String()
|
if pathParts[0] != "-" {
|
||||||
if !strings.Contains(output, "No non-default providers found") {
|
resp.WriteHeader(404)
|
||||||
t.Fatal("unexpected output:", output)
|
resp.Write([]byte(`this registry only supports legacy namespace lookup requests`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat("providers.tf"); !os.IsNotExist(err) {
|
if namespace, ok := legacyProviderNamespaces[pathParts[1]]; ok {
|
||||||
t.Fatal("unexpected providers.tf created")
|
resp.Header().Set("Content-Type", "application/json")
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
resp.Write([]byte(`{"namespace":"` + namespace + `"}`))
|
||||||
|
} else {
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
resp.Write([]byte(`provider not found`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
resource foo_resource b {}
|
||||||
|
resource bar_resource c {}
|
||||||
|
resource bar_resource ab {
|
||||||
|
provider = baz
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
# This is a file called providers.tf which does not originally have a
|
||||||
|
# required_providers block.
|
||||||
|
resource foo_resource a {}
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "hashicorp/bar"
|
||||||
|
}
|
||||||
|
baz = {
|
||||||
|
source = "terraform-providers/baz"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
resource foo_resource b {}
|
||||||
|
resource bar_resource c {}
|
||||||
|
resource bar_resource ab {
|
||||||
|
provider = baz
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# This is a file called providers.tf which does not originally have a
|
||||||
|
# required_providers block.
|
||||||
|
resource foo_resource a {}
|
|
@ -0,0 +1,19 @@
|
||||||
|
provider foo {
|
||||||
|
version = "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "hashicorp/bar"
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
||||||
|
baz = {
|
||||||
|
source = "terraform-providers/baz"
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
bar = {
|
|
||||||
source = "registry.terraform.io/-/bar"
|
|
||||||
}
|
|
||||||
baz = {
|
|
||||||
source = "registry.terraform.io/-/baz"
|
|
||||||
}
|
|
||||||
foo = {
|
|
||||||
source = "registry.terraform.io/-/foo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
provider foo {
|
||||||
|
version = "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = "1.0.0"
|
||||||
|
baz = {
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
|
provider foo {}
|
||||||
|
provider bar {}
|
||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
alpha = {
|
bar = {
|
||||||
source = "registry.terraform.io/-/alpha"
|
source = "hashicorp/bar"
|
||||||
}
|
}
|
||||||
beta = {
|
foo = {
|
||||||
source = "registry.terraform.io/-/beta"
|
source = "hashicorp/foo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
provider foo {}
|
||||||
|
provider bar {}
|
|
@ -1,2 +0,0 @@
|
||||||
provider alpha {}
|
|
||||||
provider beta {}
|
|
|
@ -0,0 +1 @@
|
||||||
|
resource something_resource a {}
|
|
@ -0,0 +1,16 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
something = {
|
||||||
|
# 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/something"
|
||||||
|
#
|
||||||
|
# For more information, see the provider source documentation:
|
||||||
|
#
|
||||||
|
# https://www.terraform.io/docs/configuration/providers.html#provider-source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
resource something_resource a {}
|
|
@ -0,0 +1,5 @@
|
||||||
|
resource foo_resource b {}
|
||||||
|
resource bar_resource c {}
|
||||||
|
resource bar_resource ab {
|
||||||
|
provider = baz
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
cloud = {
|
bar = {
|
||||||
source = "registry.terraform.io/-/cloud"
|
source = "hashicorp/bar"
|
||||||
}
|
}
|
||||||
some = {
|
baz = {
|
||||||
source = "registry.terraform.io/-/some"
|
source = "terraform-providers/baz"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
resource foo_resource b {}
|
||||||
|
resource bar_resource c {}
|
||||||
|
resource bar_resource ab {
|
||||||
|
provider = baz
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
resource some_resource a {}
|
|
||||||
resource cloud_horse x {}
|
|
|
@ -1,3 +0,0 @@
|
||||||
provider "invalid" {
|
|
||||||
version = "invalid"
|
|
||||||
}
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
resource bar_instance a {}
|
||||||
|
resource baz_instance b {}
|
||||||
|
terraform {
|
||||||
|
required_version = "> 0.12.0"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
resource foo_instance c {}
|
|
@ -0,0 +1,15 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "registry.acme.corp/acme/bar"
|
||||||
|
}
|
||||||
|
baz = {
|
||||||
|
source = "terraform-providers/baz"
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
version = "0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
resource bar_instance a {}
|
||||||
|
resource baz_instance b {}
|
||||||
|
terraform {
|
||||||
|
required_version = "> 0.12.0"
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "registry.acme.corp/acme/bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
baz = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
foo = "0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource foo_instance c {}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# This file starts with a resource and a required providers block, and should
|
||||||
|
# end up with just the resource.
|
||||||
|
resource foo_instance a {}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# This file starts with a resource and a required providers block, and should
|
||||||
|
# end up with the full required providers configuration. This file is chosen
|
||||||
|
# to keep the required providers block because its file name is "providers.tf".
|
||||||
|
resource bar_instance b {}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "registry.acme.corp/acme/bar"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
# This file starts with a resource and a required providers block, and should
|
||||||
|
# end up with just the resource.
|
||||||
|
resource foo_instance a {}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
foo = "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
# This file starts with a resource and a required providers block, and should
|
||||||
|
# end up with the full required providers configuration. This file is chosen
|
||||||
|
# to keep the required providers block because its file name is "providers.tf".
|
||||||
|
resource bar_instance b {}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "registry.acme.corp/acme/bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
variable "x" {
|
||||||
|
default = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "y" {
|
||||||
|
default = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
output "product" {
|
||||||
|
value = var.x * var.y
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
resource foo_instance a {}
|
||||||
|
resource bar_instance b {}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
# Provider requirements go here
|
||||||
|
required_providers {
|
||||||
|
# Pin bar to this version
|
||||||
|
bar = {
|
||||||
|
source = "hashicorp/bar"
|
||||||
|
version = "0.5.0"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
resource foo_instance a {}
|
||||||
|
resource bar_instance b {}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
# Provider requirements go here
|
||||||
|
required_providers {
|
||||||
|
# Pin bar to this version
|
||||||
|
bar = "0.5.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
provider foo {}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "hashicorp/bar"
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
||||||
|
unknown = {
|
||||||
|
# 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/unknown"
|
||||||
|
#
|
||||||
|
# For more information, see the provider source documentation:
|
||||||
|
#
|
||||||
|
# https://www.terraform.io/docs/configuration/providers.html#provider-source
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ provider foo {}
|
||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
bar = "1.0.0"
|
bar = "1.0.0"
|
||||||
baz = {
|
unknown = {
|
||||||
version = "~> 2.0.0"
|
version = "~> 2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "registry.acme.corp/acme/aws"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "registry.acme.corp/acme/aws"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
provider bar {
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
provider foo {
|
||||||
|
version = "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "hashicorp/bar"
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
||||||
|
baz = {
|
||||||
|
source = "terraform-providers/baz"
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"variable": {
|
||||||
|
"example": {
|
||||||
|
"default": "hello"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"terraform": {
|
||||||
|
"required_providers": {
|
||||||
|
"aws": "2.50.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
provider bar {
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
provider foo {
|
||||||
|
version = "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = "1.0.0"
|
||||||
|
baz = {
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"variable": {
|
||||||
|
"example": {
|
||||||
|
"default": "hello"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"terraform": {
|
||||||
|
"required_providers": {
|
||||||
|
"aws": "2.50.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
alpha = {
|
|
||||||
source = "registry.terraform.io/-/alpha"
|
|
||||||
}
|
|
||||||
beta = {
|
|
||||||
source = "registry.terraform.io/-/beta"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
resource beta_resource b {}
|
|
||||||
resource alpha_resource a {}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
resource foo_resource b {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
foo = {
|
||||||
|
source = "hashicorp/foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
source = "hashicorp/bar"
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
resource foo_resource b {}
|
|
@ -0,0 +1,8 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = {
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
resource foo_resource b {}
|
|
@ -12,6 +12,7 @@ import (
|
||||||
// parent.
|
// parent.
|
||||||
type RequiredProvider struct {
|
type RequiredProvider struct {
|
||||||
Name string
|
Name string
|
||||||
|
Source string
|
||||||
Type addrs.Provider
|
Type addrs.Provider
|
||||||
Requirement VersionConstraint
|
Requirement VersionConstraint
|
||||||
DeclRange hcl.Range
|
DeclRange hcl.Range
|
||||||
|
@ -67,7 +68,8 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if expr.Type().HasAttribute("source") {
|
if expr.Type().HasAttribute("source") {
|
||||||
fqn, sourceDiags := addrs.ParseProviderSourceString(expr.GetAttr("source").AsString())
|
rp.Source = expr.GetAttr("source").AsString()
|
||||||
|
fqn, sourceDiags := addrs.ParseProviderSourceString(rp.Source)
|
||||||
|
|
||||||
if sourceDiags.HasErrors() {
|
if sourceDiags.HasErrors() {
|
||||||
hclDiags := sourceDiags.ToHCL()
|
hclDiags := sourceDiags.ToHCL()
|
||||||
|
|
|
@ -21,6 +21,9 @@ var (
|
||||||
if x.Type != y.Type {
|
if x.Type != y.Type {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if x.Source != y.Source {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if x.Requirement.Required.String() != y.Requirement.Required.String() {
|
if x.Requirement.Required.String() != y.Requirement.Required.String() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -90,6 +93,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
|
||||||
RequiredProviders: map[string]*RequiredProvider{
|
RequiredProviders: map[string]*RequiredProvider{
|
||||||
"my_test": {
|
"my_test": {
|
||||||
Name: "my_test",
|
Name: "my_test",
|
||||||
|
Source: "mycloud/test",
|
||||||
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
||||||
Requirement: testVC("2.0.0"),
|
Requirement: testVC("2.0.0"),
|
||||||
DeclRange: mockRange,
|
DeclRange: mockRange,
|
||||||
|
@ -128,6 +132,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
|
||||||
},
|
},
|
||||||
"my_test": {
|
"my_test": {
|
||||||
Name: "my_test",
|
Name: "my_test",
|
||||||
|
Source: "mycloud/test",
|
||||||
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
||||||
Requirement: testVC("2.0.0"),
|
Requirement: testVC("2.0.0"),
|
||||||
DeclRange: mockRange,
|
DeclRange: mockRange,
|
||||||
|
@ -183,6 +188,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
|
||||||
RequiredProviders: map[string]*RequiredProvider{
|
RequiredProviders: map[string]*RequiredProvider{
|
||||||
"my_test": {
|
"my_test": {
|
||||||
Name: "my_test",
|
Name: "my_test",
|
||||||
|
Source: "some/invalid/provider/source/test",
|
||||||
Type: addrs.Provider{},
|
Type: addrs.Provider{},
|
||||||
Requirement: testVC("~>2.0.0"),
|
Requirement: testVC("~>2.0.0"),
|
||||||
DeclRange: mockRange,
|
DeclRange: mockRange,
|
||||||
|
@ -240,6 +246,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
|
||||||
RequiredProviders: map[string]*RequiredProvider{
|
RequiredProviders: map[string]*RequiredProvider{
|
||||||
"my_test": {
|
"my_test": {
|
||||||
Name: "my_test",
|
Name: "my_test",
|
||||||
|
Source: "mycloud/test",
|
||||||
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
|
||||||
DeclRange: mockRange,
|
DeclRange: mockRange,
|
||||||
},
|
},
|
||||||
|
|
|
@ -80,6 +80,11 @@ func findLegacyProviderLookupSource(host svchost.Hostname, source Source) *Regis
|
||||||
// just use it.
|
// just use it.
|
||||||
return source
|
return source
|
||||||
|
|
||||||
|
case *MemoizeSource:
|
||||||
|
// Also easy: the source is a memoize wrapper, so defer to its
|
||||||
|
// underlying source.
|
||||||
|
return findLegacyProviderLookupSource(host, source.underlying)
|
||||||
|
|
||||||
case MultiSource:
|
case MultiSource:
|
||||||
// Trickier case: if it's a multisource then we need to scan over
|
// Trickier case: if it's a multisource then we need to scan over
|
||||||
// its selectors until we find one that is a *RegistrySource _and_
|
// its selectors until we find one that is a *RegistrySource _and_
|
||||||
|
|
Loading…
Reference in New Issue