command: Add scaffold for 0.13upgrade command

This commit is contained in:
Alisdair McDiarmid 2020-03-12 16:47:31 -04:00
parent 42f7beff31
commit 3b0b29ef52
13 changed files with 496 additions and 0 deletions

View File

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

View File

@ -0,0 +1,200 @@
package command
import (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/hashicorp/terraform/helper/copy"
"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",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath(tc.path), 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(tc.args); 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)
}
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))
}
})
}
}
func TestZeroThirteenUpgrade_invalidFlags(t *testing.T) {
td := tempDir(t)
os.MkdirAll(td, 0755)
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([]string{"--whoops"}); code == 0 {
t.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Usage: terraform 0.13upgrade") {
t.Fatal("unexpected error:", errMsg)
}
}
func TestZeroThirteenUpgrade_tooManyArguments(t *testing.T) {
td := tempDir(t)
os.MkdirAll(td, 0755)
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([]string{".", "./modules/test"}); code == 0 {
t.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Error: Too many arguments") {
t.Fatal("unexpected error:", errMsg)
}
}
func TestZeroThirteenUpgrade_empty(t *testing.T) {
td := tempDir(t)
os.MkdirAll(td, 0755)
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.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Not a module directory") {
t.Fatal("unexpected error:", errMsg)
}
}
func TestZeroThirteenUpgrade_invalidProviderVersion(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("013upgrade-invalid"), 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.Fatal("expected error, got:", ui.OutputWriter)
}
errMsg := ui.ErrorWriter.String()
if !strings.Contains(errMsg, "Invalid provider version constraint") {
t.Fatal("unexpected error:", errMsg)
}
}
func TestZeroThirteenUpgrade_noProviders(t *testing.T) {
td := tempDir(t)
copy.CopyDir(testFixturePath("013upgrade-no-providers"), 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, "No non-default providers found") {
t.Fatal("unexpected output:", output)
}
if _, err := os.Stat("providers.tf"); !os.IsNotExist(err) {
t.Fatal("unexpected providers.tf created")
}
}

View File

@ -0,0 +1,13 @@
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,10 @@
provider foo {}
terraform {
required_providers {
bar = "1.0.0"
baz = {
version = "~> 2.0.0"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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,10 @@
terraform {
required_providers {
alpha = {
source = "registry.terraform.io/-/alpha"
}
beta = {
source = "registry.terraform.io/-/beta"
}
}
}

View File

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

View File

@ -91,6 +91,7 @@ func initCommands(config *cliconfig.Config, services *disco.Disco, providerSrc g
"force-unlock": struct{}{},
"push": struct{}{},
"0.12upgrade": struct{}{},
"0.13upgrade": struct{}{},
}
Commands = map[string]cli.CommandFactory{
@ -312,6 +313,12 @@ func initCommands(config *cliconfig.Config, services *disco.Disco, providerSrc g
}, nil
},
"0.13upgrade": func() (cli.Command, error) {
return &command.ZeroThirteenUpgradeCommand{
Meta: meta,
}, nil
},
"debug": func() (cli.Command, error) {
return &command.DebugCommand{
Meta: meta,