command: Add scaffold for 0.13upgrade command
This commit is contained in:
parent
42f7beff31
commit
3b0b29ef52
|
@ -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"
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
provider foo {}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
bar = "1.0.0"
|
||||||
|
baz = {
|
||||||
|
version = "~> 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
alpha = {
|
||||||
|
source = "registry.terraform.io/-/alpha"
|
||||||
|
}
|
||||||
|
beta = {
|
||||||
|
source = "registry.terraform.io/-/beta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
provider alpha {}
|
||||||
|
provider beta {}
|
|
@ -0,0 +1,10 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
cloud = {
|
||||||
|
source = "registry.terraform.io/-/cloud"
|
||||||
|
}
|
||||||
|
some = {
|
||||||
|
source = "registry.terraform.io/-/some"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
resource some_resource a {}
|
||||||
|
resource cloud_horse x {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
provider "invalid" {
|
||||||
|
version = "invalid"
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
variable "x" {
|
||||||
|
default = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "y" {
|
||||||
|
default = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
output "product" {
|
||||||
|
value = var.x * var.y
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
alpha = {
|
||||||
|
source = "registry.terraform.io/-/alpha"
|
||||||
|
}
|
||||||
|
beta = {
|
||||||
|
source = "registry.terraform.io/-/beta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
resource beta_resource b {}
|
||||||
|
resource alpha_resource a {}
|
|
@ -91,6 +91,7 @@ func initCommands(config *cliconfig.Config, services *disco.Disco, providerSrc g
|
||||||
"force-unlock": struct{}{},
|
"force-unlock": struct{}{},
|
||||||
"push": struct{}{},
|
"push": struct{}{},
|
||||||
"0.12upgrade": struct{}{},
|
"0.12upgrade": struct{}{},
|
||||||
|
"0.13upgrade": struct{}{},
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands = map[string]cli.CommandFactory{
|
Commands = map[string]cli.CommandFactory{
|
||||||
|
@ -312,6 +313,12 @@ func initCommands(config *cliconfig.Config, services *disco.Disco, providerSrc g
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"0.13upgrade": func() (cli.Command, error) {
|
||||||
|
return &command.ZeroThirteenUpgradeCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
"debug": func() (cli.Command, error) {
|
"debug": func() (cli.Command, error) {
|
||||||
return &command.DebugCommand{
|
return &command.DebugCommand{
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
|
|
Loading…
Reference in New Issue