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{}{},
|
||||
"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,
|
||||
|
|
Loading…
Reference in New Issue