Merge pull request #24879 from hashicorp/alisdair/013upgrade-rework

command: Rework 0.13upgrade sub-command
This commit is contained in:
Alisdair McDiarmid 2020-05-07 12:00:42 -04:00 committed by GitHub
commit de541c4d74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1093 additions and 217 deletions

View File

@ -2,15 +2,20 @@ package command
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"path"
"sort"
"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/internal/initwd"
"github.com/hashicorp/terraform/moduledeps"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// ZeroThirteenUpgradeCommand upgrades configuration files for a module
@ -19,6 +24,9 @@ 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")
@ -71,58 +79,412 @@ func (c *ZeroThirteenUpgradeCommand) Run(args []string) int {
return 1
}
// Early-load the config so that we can check provider dependencies
earlyConfig, earlyConfDiags := c.loadConfigEarly(dir)
if earlyConfDiags.HasErrors() {
// 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"))
diags = diags.Append(earlyConfDiags)
c.showDiagnostics(diags)
return 1
}
{
// Before we go further, we'll check to make sure none of the modules
// in the configuration declare that they don't support this Terraform
// version, so we can produce a version-related error message rather
// than potentially-confusing downstream errors.
versionDiags := initwd.CheckCoreVersionRequirements(earlyConfig)
diags = diags.Append(versionDiags)
if versionDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
// 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,
}
}
}
}
}
// Find the provider dependencies
configDeps, depsDiags := earlyConfig.ProviderDependencies()
if depsDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Could not detect provider dependencies"))
diags = diags.Append(depsDiags)
c.showDiagnostics(diags)
return 1
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,
}
}
}
}
}
// Detect source for each provider
providerSources, detectDiags := detectProviderSources(configDeps.Providers)
if detectDiags.HasErrors() {
c.Ui.Error(strings.TrimSpace("Unable to detect sources for providers"))
// 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)
c.showDiagnostics(diags)
return 1
}
if diags.HasErrors() {
c.Ui.Error("Unable to detect sources for providers")
c.showDiagnostics(diags)
return 1
}
if len(providerSources) == 0 {
c.Ui.Output("No non-default providers found. Your configuration is ready to use!")
return 0
}
// Default output filename is "providers.tf"
filename := path.Join(dir, "providers.tf")
// Generate the required providers configuration
genDiags := generateRequiredProviders(providerSources, dir)
diags = diags.Append(genDiags)
// 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 2
return 1
}
if len(diags) != 0 {
@ -138,64 +500,78 @@ necessary adjustments, and then commit.
return 0
}
// For providers which need a source attribute, detect and return source
// FIXME: currently does not filter or detect sources
func detectProviderSources(providers moduledeps.Providers) (map[string]string, tfdiags.Diagnostics) {
sources := make(map[string]string)
for provider := range providers {
sources[provider.Type] = provider.String()
}
return sources, nil
}
var providersTemplate = template.Must(template.New("providers.tf").Parse(`terraform {
required_providers {
{{- range $type, $source := .}}
{{$type}} = {
source = "{{$source}}"
}
{{- end}}
}
}
`))
// Generate a file with terraform.required_providers blocks for each provider
func generateRequiredProviders(providerSources map[string]string, dir string) tfdiags.Diagnostics {
// 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
// Find unused file named "providers.tf", or fall back to e.g. "providers-1.tf"
path := filepath.Join(dir, "providers.tf")
if _, err := os.Stat(path); !os.IsNotExist(err) {
for i := 1; ; i++ {
path = filepath.Join(dir, fmt.Sprintf("providers-%d.tf", i))
if _, err := os.Stat(path); os.IsNotExist(err) {
break
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),
))
}
}
f, err := os.Create(path)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to create providers file",
fmt.Sprintf("Error when generating providers configuration at '%s': %s", path, err),
))
return diags
}
defer f.Close()
return diags
}
err = providersTemplate.Execute(f, providerSources)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unable to create providers file",
fmt.Sprintf("Error when generating providers configuration at '%s': %s", path, err),
))
return diags
// 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 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 {

View File

@ -1,44 +1,115 @@
package command
import (
"bytes"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"strings"
"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/internal/getproviders"
"github.com/mitchellh/cli"
)
func TestZeroThirteenUpgrade_success(t *testing.T) {
testCases := map[string]struct {
path string
args []string
out string
}{
"implicit": {
path: "013upgrade-implicit-providers",
out: "providers.tf",
},
"explicit": {
path: "013upgrade-explicit-providers",
out: "providers.tf",
},
"subdir": {
path: "013upgrade-subdir",
args: []string{"subdir"},
out: "subdir/providers.tf",
},
"fileExists": {
path: "013upgrade-file-exists",
out: "providers-1.tf",
},
// This map from provider type name to namespace is used by the fake registry
// when called via LookupLegacyProvider. Providers not in this map will return
// a 404 Not Found error.
var legacyProviderNamespaces = map[string]string{
"foo": "hashicorp",
"bar": "hashicorp",
"baz": "terraform-providers",
}
func verifyExpectedFiles(t *testing.T, expectedPath string) {
// Compare output and expected file trees
var outputFiles, expectedFiles []string
// Gather list of output files in the current working directory
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
outputFiles = append(outputFiles, path)
}
return nil
})
if err != nil {
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) {
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)
copy.CopyDir(testFixturePath(tc.path), td)
copy.CopyDir(inputPath, td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
@ -46,11 +117,12 @@ func TestZeroThirteenUpgrade_success(t *testing.T) {
c := &ZeroThirteenUpgradeCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
ProviderSource: registrySource,
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())
}
@ -59,22 +131,95 @@ func TestZeroThirteenUpgrade_success(t *testing.T) {
t.Fatal("unexpected output:", output)
}
actual, err := ioutil.ReadFile(tc.out)
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))
}
verifyExpectedFiles(t, expectedPath)
})
}
}
// 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) {
td := tempDir(t)
os.MkdirAll(td, 0755)
@ -147,54 +292,66 @@ func TestZeroThirteenUpgrade_empty(t *testing.T) {
}
}
func TestZeroThirteenUpgrade_invalidProviderVersion(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("013upgrade-invalid"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// testServices starts up a local HTTP server running a fake provider registry
// service which responds only to discovery requests and legacy provider lookup
// API calls.
//
// 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)
c := &ZeroThirteenUpgradeCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
services = disco.New()
services.ForceHostServices(svchost.Hostname("registry.terraform.io"), map[string]interface{}{
"providers.v1": server.URL + "/providers/v1/",
})
if code := c.Run(nil); code == 0 {
t.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Invalid provider version constraint") {
t.Fatal("unexpected error:", errMsg)
return services, func() {
server.Close()
}
}
func TestZeroThirteenUpgrade_noProviders(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("013upgrade-no-providers"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// testRegistrySource is a wrapper around testServices that uses the created
// discovery object to produce a Source instance that is ready to use with the
// fake registry services.
//
// 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)
c := &ZeroThirteenUpgradeCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
path := req.URL.EscapedPath()
if !strings.HasPrefix(path, "/providers/v1/") {
resp.WriteHeader(404)
resp.Write([]byte(`not a provider registry endpoint`))
return
}
if code := c.Run(nil); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
pathParts := strings.Split(path, "/")[3:]
if len(pathParts) != 2 {
resp.WriteHeader(404)
resp.Write([]byte(`unrecognized path scheme`))
return
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "No non-default providers found") {
t.Fatal("unexpected output:", output)
if pathParts[0] != "-" {
resp.WriteHeader(404)
resp.Write([]byte(`this registry only supports legacy namespace lookup requests`))
}
if _, err := os.Stat("providers.tf"); !os.IsNotExist(err) {
t.Fatal("unexpected providers.tf created")
if namespace, ok := legacyProviderNamespaces[pathParts[1]]; ok {
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`))
}
}

View File

@ -0,0 +1,5 @@
resource foo_resource b {}
resource bar_resource c {}
resource bar_resource ab {
provider = baz
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,5 @@
resource foo_resource b {}
resource bar_resource c {}
resource bar_resource ab {
provider = baz
}

View File

@ -0,0 +1,3 @@
# This is a file called providers.tf which does not originally have a
# required_providers block.
resource foo_resource a {}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,12 @@
provider foo {
version = "1.2.3"
}
terraform {
required_providers {
bar = "1.0.0"
baz = {
version = "~> 2.0.0"
}
}
}

View File

@ -1,10 +1,12 @@
provider foo {}
provider bar {}
terraform {
required_providers {
alpha = {
source = "registry.terraform.io/-/alpha"
bar = {
source = "hashicorp/bar"
}
beta = {
source = "registry.terraform.io/-/beta"
foo = {
source = "hashicorp/foo"
}
}
}

View File

@ -0,0 +1,2 @@
provider foo {}
provider bar {}

View File

@ -1,2 +0,0 @@
provider alpha {}
provider beta {}

View File

@ -0,0 +1 @@
resource something_resource a {}

View File

@ -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
}
}
}

View File

@ -0,0 +1 @@
resource something_resource a {}

View File

@ -0,0 +1,5 @@
resource foo_resource b {}
resource bar_resource c {}
resource bar_resource ab {
provider = baz
}

View File

@ -1,10 +1,13 @@
terraform {
required_providers {
cloud = {
source = "registry.terraform.io/-/cloud"
bar = {
source = "hashicorp/bar"
}
some = {
source = "registry.terraform.io/-/some"
baz = {
source = "terraform-providers/baz"
}
foo = {
source = "hashicorp/foo"
}
}
}

View File

@ -0,0 +1,5 @@
resource foo_resource b {}
resource bar_resource c {}
resource bar_resource ab {
provider = baz
}

View File

@ -1,2 +0,0 @@
resource some_resource a {}
resource cloud_horse x {}

View File

@ -1,3 +0,0 @@
provider "invalid" {
version = "invalid"
}

View File

@ -0,0 +1,5 @@
resource bar_instance a {}
resource baz_instance b {}
terraform {
required_version = "> 0.12.0"
}

View File

@ -0,0 +1 @@
resource foo_instance c {}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,11 @@
terraform {
required_providers {
baz = "~> 2.0.0"
}
}
terraform {
required_providers {
foo = "0.5"
}
}
resource foo_instance c {}

View File

@ -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 {}

View File

@ -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"
}
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,11 @@
variable "x" {
default = 3
}
variable "y" {
default = 5
}
output "product" {
value = var.x * var.y
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}
}

View File

@ -3,7 +3,7 @@ provider foo {}
terraform {
required_providers {
bar = "1.0.0"
baz = {
unknown = {
version = "~> 2.0.0"
}
}

View File

@ -0,0 +1,7 @@
terraform {
required_providers {
aws = {
source = "registry.acme.corp/acme/aws"
}
}
}

View File

@ -0,0 +1,7 @@
terraform {
required_providers {
aws = {
source = "registry.acme.corp/acme/aws"
}
}
}

View File

@ -0,0 +1,3 @@
provider bar {
version = "1.0.0"
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,12 @@
{
"variable": {
"example": {
"default": "hello"
}
}
"terraform": {
"required_providers": {
"aws": "2.50.0"
}
}
}

View File

@ -0,0 +1,3 @@
provider bar {
version = "1.0.0"
}

View File

@ -0,0 +1,12 @@
provider foo {
version = "1.2.3"
}
terraform {
required_providers {
bar = "1.0.0"
baz = {
version = "~> 2.0.0"
}
}
}

View File

@ -0,0 +1,12 @@
{
"variable": {
"example": {
"default": "hello"
}
}
"terraform": {
"required_providers": {
"aws": "2.50.0"
}
}
}

View File

@ -1,10 +0,0 @@
terraform {
required_providers {
alpha = {
source = "registry.terraform.io/-/alpha"
}
beta = {
source = "registry.terraform.io/-/beta"
}
}
}

View File

@ -1,2 +0,0 @@
resource beta_resource b {}
resource alpha_resource a {}

View File

@ -0,0 +1,8 @@
terraform {
required_providers {
bar = {
version = "~> 2.0.0"
}
}
}

View File

@ -0,0 +1 @@
resource foo_resource b {}

View File

@ -0,0 +1,7 @@
terraform {
required_providers {
foo = {
source = "hashicorp/foo"
}
}
}

View File

@ -0,0 +1,9 @@
terraform {
required_providers {
bar = {
source = "hashicorp/bar"
version = "~> 2.0.0"
}
}
}

View File

@ -0,0 +1 @@
resource foo_resource b {}

View File

@ -0,0 +1,8 @@
terraform {
required_providers {
bar = {
version = "~> 2.0.0"
}
}
}

View File

@ -0,0 +1 @@
resource foo_resource b {}

View File

@ -12,6 +12,7 @@ import (
// parent.
type RequiredProvider struct {
Name string
Source string
Type addrs.Provider
Requirement VersionConstraint
DeclRange hcl.Range
@ -67,7 +68,8 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (*RequiredProviders, hcl.Dia
}
}
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() {
hclDiags := sourceDiags.ToHCL()

View File

@ -21,6 +21,9 @@ var (
if x.Type != y.Type {
return false
}
if x.Source != y.Source {
return false
}
if x.Requirement.Required.String() != y.Requirement.Required.String() {
return false
}
@ -90,6 +93,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
RequiredProviders: map[string]*RequiredProvider{
"my_test": {
Name: "my_test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
Requirement: testVC("2.0.0"),
DeclRange: mockRange,
@ -128,6 +132,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
},
"my_test": {
Name: "my_test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
Requirement: testVC("2.0.0"),
DeclRange: mockRange,
@ -183,6 +188,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
RequiredProviders: map[string]*RequiredProvider{
"my_test": {
Name: "my_test",
Source: "some/invalid/provider/source/test",
Type: addrs.Provider{},
Requirement: testVC("~>2.0.0"),
DeclRange: mockRange,
@ -240,6 +246,7 @@ func TestDecodeRequiredProvidersBlock(t *testing.T) {
RequiredProviders: map[string]*RequiredProvider{
"my_test": {
Name: "my_test",
Source: "mycloud/test",
Type: addrs.NewProvider(addrs.DefaultRegistryHost, "mycloud", "test"),
DeclRange: mockRange,
},

2
go.mod
View File

@ -68,7 +68,7 @@ require (
github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-version v1.2.0
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
github.com/hashicorp/hcl/v2 v2.4.0
github.com/hashicorp/hcl/v2 v2.5.0
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
github.com/hashicorp/memberlist v0.1.0 // indirect
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb // indirect

4
go.sum
View File

@ -246,8 +246,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hashicorp/hcl/v2 v2.4.0 h1:xwVa1aj4nCSoAjUnFPBAIfqlzPgSZEVMdkJv/mgj4jY=
github.com/hashicorp/hcl/v2 v2.4.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/hashicorp/hcl/v2 v2.5.0 h1:tnNRfUho4o/6qLTqd54gj9Gs5AWmdc0tG8YdElu6MEw=
github.com/hashicorp/hcl/v2 v2.5.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 h1:2yzhWGdgQUWZUCNK+AoO35V+HTsgEmcM4J9IkArh7PI=
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE=
github.com/hashicorp/memberlist v0.1.0 h1:qSsCiC0WYD39lbSitKNt40e30uorm2Ss/d4JGU1hzH8=

View File

@ -80,6 +80,11 @@ func findLegacyProviderLookupSource(host svchost.Hostname, source Source) *Regis
// just use it.
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:
// Trickier case: if it's a multisource then we need to scan over
// its selectors until we find one that is a *RegistrySource _and_

View File

@ -1,5 +1,11 @@
# HCL Changelog
## v2.5.0 (May 6, 2020)
### Enhancements
* hclwrite: Generate multi-line objects and maps. ([#372](https://github.com/hashicorp/hcl/pull/372))
## v2.4.0 (Apr 13, 2020)
### Enhancements

View File

@ -119,15 +119,15 @@ func appendTokensForValue(val cty.Value, toks Tokens) Tokens {
Type: hclsyntax.TokenOBrace,
Bytes: []byte{'{'},
})
if val.LengthInt() > 0 {
toks = append(toks, &Token{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
})
}
i := 0
for it := val.ElementIterator(); it.Next(); {
if i > 0 {
toks = append(toks, &Token{
Type: hclsyntax.TokenComma,
Bytes: []byte{','},
})
}
eKey, eVal := it.Element()
if hclsyntax.ValidIdentifier(eKey.AsString()) {
toks = append(toks, &Token{
@ -142,6 +142,10 @@ func appendTokensForValue(val cty.Value, toks Tokens) Tokens {
Bytes: []byte{'='},
})
toks = appendTokensForValue(eVal, toks)
toks = append(toks, &Token{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
})
i++
}

2
vendor/modules.txt vendored
View File

@ -370,7 +370,7 @@ github.com/hashicorp/hcl/hcl/token
github.com/hashicorp/hcl/json/parser
github.com/hashicorp/hcl/json/scanner
github.com/hashicorp/hcl/json/token
# github.com/hashicorp/hcl/v2 v2.4.0
# github.com/hashicorp/hcl/v2 v2.5.0
## explicit
github.com/hashicorp/hcl/v2
github.com/hashicorp/hcl/v2/ext/customdecode