command: convert to use backends
This commit is contained in:
parent
9654387771
commit
ad7b063262
233
command/apply.go
233
command/apply.go
|
@ -2,15 +2,16 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-getter"
|
"github.com/hashicorp/go-getter"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/helper/experiment"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
|
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
|
||||||
cmdFlags.IntVar(
|
cmdFlags.IntVar(
|
||||||
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
|
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
||||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
|
@ -51,32 +52,25 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the args. The "maybeInit" flag tracks whether we may need to
|
||||||
|
// initialize the configuration from a remote path. This is true as long
|
||||||
|
// as we have an argument.
|
||||||
|
args = cmdFlags.Args()
|
||||||
|
maybeInit := len(args) == 1
|
||||||
|
configPath, err := ModulePath(args)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.Destroy && maybeInit {
|
||||||
|
// We need the pwd for the getter operation below
|
||||||
pwd, err := os.Getwd()
|
pwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPath string
|
|
||||||
maybeInit := true
|
|
||||||
args = cmdFlags.Args()
|
|
||||||
if len(args) > 1 {
|
|
||||||
c.Ui.Error("The apply command expects at most one argument.")
|
|
||||||
cmdFlags.Usage()
|
|
||||||
return 1
|
|
||||||
} else if len(args) == 1 {
|
|
||||||
configPath = args[0]
|
|
||||||
} else {
|
|
||||||
configPath = pwd
|
|
||||||
maybeInit = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the extra hooks to count resources
|
|
||||||
countHook := new(CountHook)
|
|
||||||
stateHook := new(StateHook)
|
|
||||||
c.Meta.extraHooks = []terraform.Hook{countHook, stateHook}
|
|
||||||
|
|
||||||
if !c.Destroy && maybeInit {
|
|
||||||
// Do a detect to determine if we need to do an init + apply.
|
// Do a detect to determine if we need to do an init + apply.
|
||||||
if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
|
if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
@ -96,6 +90,33 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the path is a plan
|
||||||
|
plan, err := c.Plan(configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if c.Destroy && plan != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"Destroy can't be called with a plan file."))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if plan != nil {
|
||||||
|
// Reset the config path for backend loading
|
||||||
|
configPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the module if we don't have one yet (not running from plan)
|
||||||
|
var mod *module.Tree
|
||||||
|
if plan == nil {
|
||||||
|
mod, err = c.Module(configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
terraform.SetDebugInfo(DefaultDataDir)
|
terraform.SetDebugInfo(DefaultDataDir)
|
||||||
|
|
||||||
// Check for the legacy graph
|
// Check for the legacy graph
|
||||||
|
@ -107,26 +128,20 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
"please report any issues causing you to use this to the Terraform\n" +
|
"please report any issues causing you to use this to the Terraform\n" +
|
||||||
"project.\n\n"))
|
"project.\n\n"))
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// This is going to keep track of shadow errors
|
// Load the backend
|
||||||
var shadowErr error
|
b, err := c.Backend(&BackendOpts{
|
||||||
|
ConfigPath: configPath,
|
||||||
// Build the context based on the arguments given
|
Plan: plan,
|
||||||
ctx, planned, err := c.Context(contextOpts{
|
|
||||||
Destroy: c.Destroy,
|
|
||||||
Path: configPath,
|
|
||||||
StatePath: c.Meta.statePath,
|
|
||||||
Parallelism: c.Meta.parallelism,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if c.Destroy && planned {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Destroy can't be called with a plan file."))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're not forcing and we're destroying, verify with the
|
||||||
|
// user at this point.
|
||||||
if !destroyForce && c.Destroy {
|
if !destroyForce && c.Destroy {
|
||||||
// Default destroy message
|
// Default destroy message
|
||||||
desc := "Terraform will delete all your managed infrastructure.\n" +
|
desc := "Terraform will delete all your managed infrastructure.\n" +
|
||||||
|
@ -159,80 +174,32 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !planned {
|
|
||||||
if err := ctx.Input(c.InputMode()); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record any shadow errors for later
|
// Build the operation
|
||||||
if err := ctx.ShadowError(); err != nil {
|
opReq := c.Operation()
|
||||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
opReq.Destroy = c.Destroy
|
||||||
err, "input operation:"))
|
opReq.Module = mod
|
||||||
}
|
opReq.Plan = plan
|
||||||
}
|
opReq.PlanRefresh = refresh
|
||||||
if !validateContext(ctx, c.Ui) {
|
opReq.Type = backend.OperationTypeApply
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plan if we haven't already
|
// Perform the operation
|
||||||
if !planned {
|
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||||
if refresh {
|
defer ctxCancel()
|
||||||
if _, err := ctx.Refresh(); err != nil {
|
op, err := b.Operation(ctx, opReq)
|
||||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := ctx.Plan(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Error creating plan: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record any shadow errors for later
|
|
||||||
if err := ctx.ShadowError(); err != nil {
|
|
||||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
|
||||||
err, "plan operation:"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the state hook for continuous state updates
|
|
||||||
{
|
|
||||||
state, err := c.State()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
|
||||||
"Error reading state: %s", err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
stateHook.State = state
|
// Wait for the operation to complete or an interrupt to occur
|
||||||
}
|
|
||||||
|
|
||||||
// Start the apply in a goroutine so that we can be interrupted.
|
|
||||||
var state *terraform.State
|
|
||||||
var applyErr error
|
|
||||||
doneCh := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(doneCh)
|
|
||||||
state, applyErr = ctx.Apply()
|
|
||||||
|
|
||||||
// Record any shadow errors for later
|
|
||||||
if err := ctx.ShadowError(); err != nil {
|
|
||||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
|
||||||
err, "apply operation:"))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for the apply to finish or for us to be interrupted so
|
|
||||||
// we can handle it properly.
|
|
||||||
err = nil
|
|
||||||
select {
|
select {
|
||||||
case <-c.ShutdownCh:
|
case <-c.ShutdownCh:
|
||||||
c.Ui.Output("Interrupt received. Gracefully shutting down...")
|
// Cancel our context so we can start gracefully exiting
|
||||||
|
ctxCancel()
|
||||||
|
|
||||||
// Stop execution
|
// Notify the user
|
||||||
go ctx.Stop()
|
c.Ui.Output("Interrupt received. Gracefully shutting down...")
|
||||||
|
|
||||||
// Still get the result, since there is still one
|
// Still get the result, since there is still one
|
||||||
select {
|
select {
|
||||||
|
@ -241,65 +208,27 @@ func (c *ApplyCommand) Run(args []string) int {
|
||||||
"Two interrupts received. Exiting immediately. Note that data\n" +
|
"Two interrupts received. Exiting immediately. Note that data\n" +
|
||||||
"loss may have occurred.")
|
"loss may have occurred.")
|
||||||
return 1
|
return 1
|
||||||
case <-doneCh:
|
case <-op.Done():
|
||||||
}
|
}
|
||||||
case <-doneCh:
|
case <-op.Done():
|
||||||
}
|
if err := op.Err; err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
// Persist the state
|
|
||||||
if state != nil {
|
|
||||||
if err := c.Meta.PersistState(state); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to save state: %s", err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if applyErr != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Error applying plan:\n\n"+
|
|
||||||
"%s\n\n"+
|
|
||||||
"Terraform does not automatically rollback in the face of errors.\n"+
|
|
||||||
"Instead, your Terraform state file has been partially updated with\n"+
|
|
||||||
"any resources that successfully completed. Please address the error\n"+
|
|
||||||
"above and apply again to incrementally change your infrastructure.",
|
|
||||||
multierror.Flatten(applyErr)))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Destroy {
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset][bold][green]\n"+
|
|
||||||
"Destroy complete! Resources: %d destroyed.",
|
|
||||||
countHook.Removed)))
|
|
||||||
} else {
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset][bold][green]\n"+
|
|
||||||
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
|
|
||||||
countHook.Added,
|
|
||||||
countHook.Changed,
|
|
||||||
countHook.Removed)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if countHook.Added > 0 || countHook.Changed > 0 {
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset]\n"+
|
|
||||||
"The state of your infrastructure has been saved to the path\n"+
|
|
||||||
"below. This state is required to modify and destroy your\n"+
|
|
||||||
"infrastructure, so keep it safe. To inspect the complete state\n"+
|
|
||||||
"use the `terraform show` command.\n\n"+
|
|
||||||
"State path: %s",
|
|
||||||
c.Meta.StateOutPath())))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.Destroy {
|
if !c.Destroy {
|
||||||
if outputs := outputsAsString(state, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" {
|
// Get the right module that we used. If we ran a plan, then use
|
||||||
|
// that module.
|
||||||
|
if plan != nil {
|
||||||
|
mod = plan.Module
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" {
|
||||||
c.Ui.Output(c.Colorize().Color(outputs))
|
c.Ui.Output(c.Colorize().Color(outputs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have an error in the shadow graph, let the user know.
|
|
||||||
c.outputShadowError(shadowErr, applyErr == nil)
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -519,7 +519,7 @@ func TestApply_plan(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state", statePath,
|
"-state-out", statePath,
|
||||||
planPath,
|
planPath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
|
@ -564,7 +564,7 @@ func TestApply_plan_backup(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state", statePath,
|
"-state-out", statePath,
|
||||||
"-backup", backupPath,
|
"-backup", backupPath,
|
||||||
planPath,
|
planPath,
|
||||||
}
|
}
|
||||||
|
@ -601,7 +601,7 @@ func TestApply_plan_noBackup(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state", statePath,
|
"-state-out", statePath,
|
||||||
"-backup", "-",
|
"-backup", "-",
|
||||||
planPath,
|
planPath,
|
||||||
}
|
}
|
||||||
|
@ -670,12 +670,13 @@ func TestApply_plan_remoteState(t *testing.T) {
|
||||||
|
|
||||||
// State file should be not be installed
|
// State file should be not be installed
|
||||||
if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil {
|
if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil {
|
||||||
t.Fatalf("State path should not exist")
|
data, _ := ioutil.ReadFile(DefaultStateFilename)
|
||||||
|
t.Fatalf("State path should not exist: %s", string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for remote state
|
// Check that there is no remote state config
|
||||||
if _, err := os.Stat(remoteStatePath); err != nil {
|
if _, err := os.Stat(remoteStatePath); err == nil {
|
||||||
t.Fatalf("missing remote state: %s", err)
|
t.Fatalf("has remote state config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -710,7 +711,7 @@ func TestApply_planWithVarFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-state", statePath,
|
"-state-out", statePath,
|
||||||
planPath,
|
planPath,
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
|
@ -1489,59 +1490,6 @@ func TestApply_disableBackup(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -state-out wasn't taking effect when a plan is supplied. GH-7264
|
|
||||||
func TestApply_stateOutWithPlan(t *testing.T) {
|
|
||||||
p := testProvider()
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
|
|
||||||
tmpDir := testTempDir(t)
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
statePath := filepath.Join(tmpDir, "state.tfstate")
|
|
||||||
planPath := filepath.Join(tmpDir, "terraform.tfplan")
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"-state", statePath,
|
|
||||||
"-out", planPath,
|
|
||||||
testFixturePath("plan"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run plan first to get a current plan file
|
|
||||||
pc := &PlanCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(p),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if code := pc.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// now run apply with the generated plan
|
|
||||||
stateOutPath := filepath.Join(tmpDir, "state-new.tfstate")
|
|
||||||
|
|
||||||
args = []string{
|
|
||||||
"-state", statePath,
|
|
||||||
"-state-out", stateOutPath,
|
|
||||||
planPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
ac := &ApplyCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(p),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if code := ac.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// now make sure we wrote out our new state
|
|
||||||
if _, err := os.Stat(stateOutPath); err != nil {
|
|
||||||
t.Fatalf("missing new state file: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testHttpServer(t *testing.T) net.Listener {
|
func testHttpServer(t *testing.T) net.Listener {
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package command
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
@ -27,6 +28,47 @@ const DefaultBackupExtension = ".backup"
|
||||||
// operations as it walks the dependency graph.
|
// operations as it walks the dependency graph.
|
||||||
const DefaultParallelism = 10
|
const DefaultParallelism = 10
|
||||||
|
|
||||||
|
// ErrUnsupportedLocalOp is the common error message shown for operations
|
||||||
|
// that require a backend.Local.
|
||||||
|
const ErrUnsupportedLocalOp = `The configured backend doesn't support this operation.
|
||||||
|
|
||||||
|
The "backend" in Terraform defines how Terraform operates. The default
|
||||||
|
backend performs all operations locally on your machine. Your configuration
|
||||||
|
is configured to use a non-local backend. This backend doesn't support this
|
||||||
|
operation.
|
||||||
|
|
||||||
|
If you want to use the state from the backend but force all other data
|
||||||
|
(configuration, variables, etc.) to come locally, you can force local
|
||||||
|
behavior with the "-local" flag.
|
||||||
|
`
|
||||||
|
|
||||||
|
// ModulePath returns the path to the root module from the CLI args.
|
||||||
|
//
|
||||||
|
// This centralizes the logic for any commands that expect a module path
|
||||||
|
// on their CLI args. This will verify that only one argument is given
|
||||||
|
// and that it is a path to configuration.
|
||||||
|
//
|
||||||
|
// If your command accepts more than one arg, then change the slice bounds
|
||||||
|
// to pass validation.
|
||||||
|
func ModulePath(args []string) (string, error) {
|
||||||
|
// TODO: test
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
return "", fmt.Errorf("Too many command line arguments. Configuration path expected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
path, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error getting pwd: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return args[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
|
func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
|
||||||
log.Println("[INFO] Validating the context...")
|
log.Println("[INFO] Validating the context...")
|
||||||
ws, es := ctx.Validate()
|
ws, es := ctx.Validate()
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -164,7 +170,20 @@ func testState() *terraform.State {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
state.Init()
|
state.Init()
|
||||||
return state
|
|
||||||
|
// Write and read the state so that it is properly initialized. We
|
||||||
|
// do this since we didn't call the normal NewState constructor.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := terraform.WriteState(state, &buf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := terraform.ReadState(&buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func testStateFile(t *testing.T, s *terraform.State) string {
|
func testStateFile(t *testing.T, s *terraform.State) string {
|
||||||
|
@ -220,9 +239,8 @@ func testStateFileRemote(t *testing.T, s *terraform.State) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
// testStateOutput tests that the state at the given path contains
|
// testStateRead reads the state from a file
|
||||||
// the expected state string.
|
func testStateRead(t *testing.T, path string) *terraform.State {
|
||||||
func testStateOutput(t *testing.T, path string, expected string) {
|
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
|
@ -234,6 +252,13 @@ func testStateOutput(t *testing.T, path string, expected string) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
// testStateOutput tests that the state at the given path contains
|
||||||
|
// the expected state string.
|
||||||
|
func testStateOutput(t *testing.T, path string, expected string) {
|
||||||
|
newState := testStateRead(t, path)
|
||||||
actual := strings.TrimSpace(newState.String())
|
actual := strings.TrimSpace(newState.String())
|
||||||
expected = strings.TrimSpace(expected)
|
expected = strings.TrimSpace(expected)
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
|
@ -401,3 +426,106 @@ func testStdoutCapture(t *testing.T, dst io.Writer) func() {
|
||||||
<-doneCh
|
<-doneCh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testInteractiveInput configures tests so that the answers given are sent
|
||||||
|
// in order to interactive prompts. The returned function must be called
|
||||||
|
// in a defer to clean up.
|
||||||
|
func testInteractiveInput(t *testing.T, answers []string) func() {
|
||||||
|
// Disable test mode so input is called
|
||||||
|
test = false
|
||||||
|
|
||||||
|
// Setup reader/writers
|
||||||
|
testInputResponse = answers
|
||||||
|
defaultInputReader = bytes.NewBufferString("")
|
||||||
|
defaultInputWriter = new(bytes.Buffer)
|
||||||
|
|
||||||
|
// Return the cleanup
|
||||||
|
return func() {
|
||||||
|
test = true
|
||||||
|
testInputResponse = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testBackendState is used to make a test HTTP server to test a configured
|
||||||
|
// backend. This returns the complete state that can be saved. Use
|
||||||
|
// `testStateFileRemote` to write the returned state.
|
||||||
|
func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State, *httptest.Server) {
|
||||||
|
var b64md5 string
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
cb := func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method == "PUT" {
|
||||||
|
resp.WriteHeader(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Header().Set("Content-MD5", b64md5)
|
||||||
|
resp.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a state was given, make sure we calculate the proper b64md5
|
||||||
|
if s != nil {
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
if err := enc.Encode(s); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
md5 := md5.Sum(buf.Bytes())
|
||||||
|
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(cb))
|
||||||
|
|
||||||
|
state := terraform.NewState()
|
||||||
|
state.Backend = &terraform.BackendState{
|
||||||
|
Type: "http",
|
||||||
|
Config: map[string]interface{}{"address": srv.URL},
|
||||||
|
Hash: 2529831861221416334,
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, srv
|
||||||
|
}
|
||||||
|
|
||||||
|
// testRemoteState is used to make a test HTTP server to return a given
|
||||||
|
// state file that can be used for testing legacy remote state.
|
||||||
|
func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) {
|
||||||
|
var b64md5 string
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
cb := func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method == "PUT" {
|
||||||
|
resp.WriteHeader(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Header().Set("Content-MD5", b64md5)
|
||||||
|
resp.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(cb))
|
||||||
|
remote := &terraform.RemoteState{
|
||||||
|
Type: "http",
|
||||||
|
Config: map[string]string{"address": srv.URL},
|
||||||
|
}
|
||||||
|
|
||||||
|
if s != nil {
|
||||||
|
// Set the remote data
|
||||||
|
s.Remote = remote
|
||||||
|
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
if err := enc.Encode(s); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
md5 := md5.Sum(buf.Bytes())
|
||||||
|
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
|
||||||
|
}
|
||||||
|
|
||||||
|
return remote, srv
|
||||||
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ package command
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
||||||
"github.com/hashicorp/terraform/repl"
|
"github.com/hashicorp/terraform/repl"
|
||||||
|
|
||||||
|
@ -30,30 +30,39 @@ func (c *ConsoleCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pwd, err := os.Getwd()
|
configPath, err := ModulePath(cmdFlags.Args())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPath string
|
// Load the module
|
||||||
args = cmdFlags.Args()
|
mod, err := c.Module(configPath)
|
||||||
if len(args) > 1 {
|
if err != nil {
|
||||||
c.Ui.Error("The console command expects at most one argument.")
|
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||||
cmdFlags.Usage()
|
|
||||||
return 1
|
return 1
|
||||||
} else if len(args) == 1 {
|
|
||||||
configPath = args[0]
|
|
||||||
} else {
|
|
||||||
configPath = pwd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
// Load the backend
|
||||||
ctx, _, err := c.Context(contextOpts{
|
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
|
||||||
Path: configPath,
|
if err != nil {
|
||||||
PathEmptyOk: true,
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
StatePath: c.Meta.statePath,
|
return 1
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// We require a local backend
|
||||||
|
local, ok := b.(backend.Local)
|
||||||
|
if !ok {
|
||||||
|
c.Ui.Error(ErrUnsupportedLocalOp)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the operation
|
||||||
|
opReq := c.Operation()
|
||||||
|
opReq.Module = mod
|
||||||
|
|
||||||
|
// Get the context
|
||||||
|
ctx, _, err := local.Context(opReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -1,231 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/colorstring"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FormatPlanOpts are the options for formatting a plan.
|
|
||||||
type FormatPlanOpts struct {
|
|
||||||
// Plan is the plan to format. This is required.
|
|
||||||
Plan *terraform.Plan
|
|
||||||
|
|
||||||
// Color is the colorizer. This is optional.
|
|
||||||
Color *colorstring.Colorize
|
|
||||||
|
|
||||||
// ModuleDepth is the depth of the modules to expand. By default this
|
|
||||||
// is zero which will not expand modules at all.
|
|
||||||
ModuleDepth int
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatPlan takes a plan and returns a
|
|
||||||
func FormatPlan(opts *FormatPlanOpts) string {
|
|
||||||
p := opts.Plan
|
|
||||||
if p.Diff == nil || p.Diff.Empty() {
|
|
||||||
return "This plan does nothing."
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Color == nil {
|
|
||||||
opts.Color = &colorstring.Colorize{
|
|
||||||
Colors: colorstring.DefaultColors,
|
|
||||||
Reset: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
for _, m := range p.Diff.Modules {
|
|
||||||
if len(m.Path)-1 <= opts.ModuleDepth || opts.ModuleDepth == -1 {
|
|
||||||
formatPlanModuleExpand(buf, m, opts)
|
|
||||||
} else {
|
|
||||||
formatPlanModuleSingle(buf, m, opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatPlanModuleExpand will output the given module and all of its
|
|
||||||
// resources.
|
|
||||||
func formatPlanModuleExpand(
|
|
||||||
buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) {
|
|
||||||
// Ignore empty diffs
|
|
||||||
if m.Empty() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var moduleName string
|
|
||||||
if !m.IsRoot() {
|
|
||||||
moduleName = fmt.Sprintf("module.%s", strings.Join(m.Path[1:], "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to output the resources in sorted order to make things
|
|
||||||
// easier to scan through, so get all the resource names and sort them.
|
|
||||||
names := make([]string, 0, len(m.Resources))
|
|
||||||
for name, _ := range m.Resources {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
// Go through each sorted name and start building the output
|
|
||||||
for _, name := range names {
|
|
||||||
rdiff := m.Resources[name]
|
|
||||||
if rdiff.Empty() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSource := strings.HasPrefix(name, "data.")
|
|
||||||
|
|
||||||
if moduleName != "" {
|
|
||||||
name = moduleName + "." + name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the color for the text (green for adding, yellow
|
|
||||||
// for change, red for delete), and symbol, and output the
|
|
||||||
// resource header.
|
|
||||||
color := "yellow"
|
|
||||||
symbol := "~"
|
|
||||||
oldValues := true
|
|
||||||
switch rdiff.ChangeType() {
|
|
||||||
case terraform.DiffDestroyCreate:
|
|
||||||
color = "green"
|
|
||||||
symbol = "-/+"
|
|
||||||
case terraform.DiffCreate:
|
|
||||||
color = "green"
|
|
||||||
symbol = "+"
|
|
||||||
oldValues = false
|
|
||||||
|
|
||||||
// If we're "creating" a data resource then we'll present it
|
|
||||||
// to the user as a "read" operation, so it's clear that this
|
|
||||||
// operation won't change anything outside of the Terraform state.
|
|
||||||
// Unfortunately by the time we get here we only have the name
|
|
||||||
// to work with, so we need to cheat and exploit knowledge of the
|
|
||||||
// naming scheme for data resources.
|
|
||||||
if dataSource {
|
|
||||||
symbol = "<="
|
|
||||||
color = "cyan"
|
|
||||||
}
|
|
||||||
case terraform.DiffDestroy:
|
|
||||||
color = "red"
|
|
||||||
symbol = "-"
|
|
||||||
}
|
|
||||||
|
|
||||||
var extraAttr []string
|
|
||||||
if rdiff.DestroyTainted {
|
|
||||||
extraAttr = append(extraAttr, "tainted")
|
|
||||||
}
|
|
||||||
if rdiff.DestroyDeposed {
|
|
||||||
extraAttr = append(extraAttr, "deposed")
|
|
||||||
}
|
|
||||||
var extraStr string
|
|
||||||
if len(extraAttr) > 0 {
|
|
||||||
extraStr = fmt.Sprintf(" (%s)", strings.Join(extraAttr, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(opts.Color.Color(fmt.Sprintf(
|
|
||||||
"[%s]%s %s%s\n",
|
|
||||||
color, symbol, name, extraStr)))
|
|
||||||
|
|
||||||
// Get all the attributes that are changing, and sort them. Also
|
|
||||||
// determine the longest key so that we can align them all.
|
|
||||||
keyLen := 0
|
|
||||||
keys := make([]string, 0, len(rdiff.Attributes))
|
|
||||||
for key, _ := range rdiff.Attributes {
|
|
||||||
// Skip the ID since we do that specially
|
|
||||||
if key == "id" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
keys = append(keys, key)
|
|
||||||
if len(key) > keyLen {
|
|
||||||
keyLen = len(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
// Go through and output each attribute
|
|
||||||
for _, attrK := range keys {
|
|
||||||
attrDiff := rdiff.Attributes[attrK]
|
|
||||||
|
|
||||||
v := attrDiff.New
|
|
||||||
if v == "" && attrDiff.NewComputed {
|
|
||||||
v = "<computed>"
|
|
||||||
}
|
|
||||||
|
|
||||||
if attrDiff.Sensitive {
|
|
||||||
v = "<sensitive>"
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMsg := ""
|
|
||||||
if attrDiff.RequiresNew && rdiff.Destroy {
|
|
||||||
updateMsg = opts.Color.Color(" [red](forces new resource)")
|
|
||||||
} else if attrDiff.Sensitive && oldValues {
|
|
||||||
updateMsg = opts.Color.Color(" [yellow](attribute changed)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldValues {
|
|
||||||
var u string
|
|
||||||
if attrDiff.Sensitive {
|
|
||||||
u = "<sensitive>"
|
|
||||||
} else {
|
|
||||||
u = attrDiff.Old
|
|
||||||
}
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
|
||||||
" %s:%s %#v => %#v%s\n",
|
|
||||||
attrK,
|
|
||||||
strings.Repeat(" ", keyLen-len(attrK)),
|
|
||||||
u,
|
|
||||||
v,
|
|
||||||
updateMsg))
|
|
||||||
} else {
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
|
||||||
" %s:%s %#v%s\n",
|
|
||||||
attrK,
|
|
||||||
strings.Repeat(" ", keyLen-len(attrK)),
|
|
||||||
v,
|
|
||||||
updateMsg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the reset color so we don't overload the user's terminal
|
|
||||||
buf.WriteString(opts.Color.Color("[reset]\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatPlanModuleSingle will output the given module and all of its
|
|
||||||
// resources.
|
|
||||||
func formatPlanModuleSingle(
|
|
||||||
buf *bytes.Buffer, m *terraform.ModuleDiff, opts *FormatPlanOpts) {
|
|
||||||
// Ignore empty diffs
|
|
||||||
if m.Empty() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
moduleName := fmt.Sprintf("module.%s", strings.Join(m.Path[1:], "."))
|
|
||||||
|
|
||||||
// Determine the color for the text (green for adding, yellow
|
|
||||||
// for change, red for delete), and symbol, and output the
|
|
||||||
// resource header.
|
|
||||||
color := "yellow"
|
|
||||||
symbol := "~"
|
|
||||||
switch m.ChangeType() {
|
|
||||||
case terraform.DiffCreate:
|
|
||||||
color = "green"
|
|
||||||
symbol = "+"
|
|
||||||
case terraform.DiffDestroy:
|
|
||||||
color = "red"
|
|
||||||
symbol = "-"
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(opts.Color.Color(fmt.Sprintf(
|
|
||||||
"[%s]%s %s\n",
|
|
||||||
color, symbol, moduleName)))
|
|
||||||
buf.WriteString(fmt.Sprintf(
|
|
||||||
" %d resource(s)",
|
|
||||||
len(m.Resources)))
|
|
||||||
buf.WriteString(opts.Color.Color("[reset]\n"))
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/colorstring"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test that a root level data source gets a special plan output on create
|
|
||||||
func TestFormatPlan_destroyDeposed(t *testing.T) {
|
|
||||||
plan := &terraform.Plan{
|
|
||||||
Diff: &terraform.Diff{
|
|
||||||
Modules: []*terraform.ModuleDiff{
|
|
||||||
&terraform.ModuleDiff{
|
|
||||||
Path: []string{"root"},
|
|
||||||
Resources: map[string]*terraform.InstanceDiff{
|
|
||||||
"aws_instance.foo": &terraform.InstanceDiff{
|
|
||||||
DestroyDeposed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
opts := &FormatPlanOpts{
|
|
||||||
Plan: plan,
|
|
||||||
Color: &colorstring.Colorize{
|
|
||||||
Colors: colorstring.DefaultColors,
|
|
||||||
Disable: true,
|
|
||||||
},
|
|
||||||
ModuleDepth: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := FormatPlan(opts)
|
|
||||||
|
|
||||||
expected := strings.TrimSpace(`
|
|
||||||
- aws_instance.foo (deposed)
|
|
||||||
`)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that computed fields with an interpolation string get displayed
|
|
||||||
func TestFormatPlan_displayInterpolations(t *testing.T) {
|
|
||||||
plan := &terraform.Plan{
|
|
||||||
Diff: &terraform.Diff{
|
|
||||||
Modules: []*terraform.ModuleDiff{
|
|
||||||
&terraform.ModuleDiff{
|
|
||||||
Path: []string{"root"},
|
|
||||||
Resources: map[string]*terraform.InstanceDiff{
|
|
||||||
"aws_instance.foo": &terraform.InstanceDiff{
|
|
||||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
||||||
"computed_field": &terraform.ResourceAttrDiff{
|
|
||||||
New: "${aws_instance.other.id}",
|
|
||||||
NewComputed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
opts := &FormatPlanOpts{
|
|
||||||
Plan: plan,
|
|
||||||
Color: &colorstring.Colorize{
|
|
||||||
Colors: colorstring.DefaultColors,
|
|
||||||
Disable: true,
|
|
||||||
},
|
|
||||||
ModuleDepth: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
out := FormatPlan(opts)
|
|
||||||
lines := strings.Split(out, "\n")
|
|
||||||
if len(lines) != 2 {
|
|
||||||
t.Fatal("expected 2 lines of output, got:\n", out)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := strings.TrimSpace(lines[1])
|
|
||||||
expected := `computed_field: "" => "${aws_instance.other.id}"`
|
|
||||||
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that a root level data source gets a special plan output on create
|
|
||||||
func TestFormatPlan_rootDataSource(t *testing.T) {
|
|
||||||
plan := &terraform.Plan{
|
|
||||||
Diff: &terraform.Diff{
|
|
||||||
Modules: []*terraform.ModuleDiff{
|
|
||||||
&terraform.ModuleDiff{
|
|
||||||
Path: []string{"root"},
|
|
||||||
Resources: map[string]*terraform.InstanceDiff{
|
|
||||||
"data.type.name": &terraform.InstanceDiff{
|
|
||||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
||||||
"A": &terraform.ResourceAttrDiff{
|
|
||||||
New: "B",
|
|
||||||
RequiresNew: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
opts := &FormatPlanOpts{
|
|
||||||
Plan: plan,
|
|
||||||
Color: &colorstring.Colorize{
|
|
||||||
Colors: colorstring.DefaultColors,
|
|
||||||
Disable: true,
|
|
||||||
},
|
|
||||||
ModuleDepth: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := FormatPlan(opts)
|
|
||||||
|
|
||||||
expected := strings.TrimSpace(`
|
|
||||||
<= data.type.name
|
|
||||||
A: "B"
|
|
||||||
`)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that data sources nested in modules get the same plan output
|
|
||||||
func TestFormatPlan_nestedDataSource(t *testing.T) {
|
|
||||||
plan := &terraform.Plan{
|
|
||||||
Diff: &terraform.Diff{
|
|
||||||
Modules: []*terraform.ModuleDiff{
|
|
||||||
&terraform.ModuleDiff{
|
|
||||||
Path: []string{"root", "nested"},
|
|
||||||
Resources: map[string]*terraform.InstanceDiff{
|
|
||||||
"data.type.name": &terraform.InstanceDiff{
|
|
||||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
|
||||||
"A": &terraform.ResourceAttrDiff{
|
|
||||||
New: "B",
|
|
||||||
RequiresNew: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
opts := &FormatPlanOpts{
|
|
||||||
Plan: plan,
|
|
||||||
Color: &colorstring.Colorize{
|
|
||||||
Colors: colorstring.DefaultColors,
|
|
||||||
Disable: true,
|
|
||||||
},
|
|
||||||
ModuleDepth: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := FormatPlan(opts)
|
|
||||||
|
|
||||||
expected := strings.TrimSpace(`
|
|
||||||
<= module.nested.data.type.name
|
|
||||||
A: "B"
|
|
||||||
`)
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/colorstring"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FormatStateOpts are the options for formatting a state.
|
|
||||||
type FormatStateOpts struct {
|
|
||||||
// State is the state to format. This is required.
|
|
||||||
State *terraform.State
|
|
||||||
|
|
||||||
// Color is the colorizer. This is optional.
|
|
||||||
Color *colorstring.Colorize
|
|
||||||
|
|
||||||
// ModuleDepth is the depth of the modules to expand. By default this
|
|
||||||
// is zero which will not expand modules at all.
|
|
||||||
ModuleDepth int
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatState takes a state and returns a string
|
|
||||||
func FormatState(opts *FormatStateOpts) string {
|
|
||||||
if opts.Color == nil {
|
|
||||||
panic("colorize not given")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := opts.State
|
|
||||||
if len(s.Modules) == 0 {
|
|
||||||
return "The state file is empty. No resources are represented."
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString("[reset]")
|
|
||||||
|
|
||||||
// Format all the modules
|
|
||||||
for _, m := range s.Modules {
|
|
||||||
if len(m.Path)-1 <= opts.ModuleDepth || opts.ModuleDepth == -1 {
|
|
||||||
formatStateModuleExpand(&buf, m, opts)
|
|
||||||
} else {
|
|
||||||
formatStateModuleSingle(&buf, m, opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the outputs for the root module
|
|
||||||
m := s.RootModule()
|
|
||||||
if len(m.Outputs) > 0 {
|
|
||||||
buf.WriteString("\nOutputs:\n\n")
|
|
||||||
|
|
||||||
// Sort the outputs
|
|
||||||
ks := make([]string, 0, len(m.Outputs))
|
|
||||||
for k, _ := range m.Outputs {
|
|
||||||
ks = append(ks, k)
|
|
||||||
}
|
|
||||||
sort.Strings(ks)
|
|
||||||
|
|
||||||
// Output each output k/v pair
|
|
||||||
for _, k := range ks {
|
|
||||||
v := m.Outputs[k]
|
|
||||||
switch output := v.Value.(type) {
|
|
||||||
case string:
|
|
||||||
buf.WriteString(fmt.Sprintf("%s = %s", k, output))
|
|
||||||
buf.WriteString("\n")
|
|
||||||
case []interface{}:
|
|
||||||
buf.WriteString(formatListOutput("", k, output))
|
|
||||||
buf.WriteString("\n")
|
|
||||||
case map[string]interface{}:
|
|
||||||
buf.WriteString(formatMapOutput("", k, output))
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts.Color.Color(strings.TrimSpace(buf.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatStateModuleExpand(
|
|
||||||
buf *bytes.Buffer, m *terraform.ModuleState, opts *FormatStateOpts) {
|
|
||||||
var moduleName string
|
|
||||||
if !m.IsRoot() {
|
|
||||||
moduleName = fmt.Sprintf("module.%s", strings.Join(m.Path[1:], "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
// First get the names of all the resources so we can show them
|
|
||||||
// in alphabetical order.
|
|
||||||
names := make([]string, 0, len(m.Resources))
|
|
||||||
for name, _ := range m.Resources {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
// Go through each resource and begin building up the output.
|
|
||||||
for _, k := range names {
|
|
||||||
name := k
|
|
||||||
if moduleName != "" {
|
|
||||||
name = moduleName + "." + name
|
|
||||||
}
|
|
||||||
|
|
||||||
rs := m.Resources[k]
|
|
||||||
is := rs.Primary
|
|
||||||
var id string
|
|
||||||
if is != nil {
|
|
||||||
id = is.ID
|
|
||||||
}
|
|
||||||
if id == "" {
|
|
||||||
id = "<not created>"
|
|
||||||
}
|
|
||||||
|
|
||||||
taintStr := ""
|
|
||||||
if rs.Primary != nil && rs.Primary.Tainted {
|
|
||||||
taintStr = " (tainted)"
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(fmt.Sprintf("%s:%s\n", name, taintStr))
|
|
||||||
buf.WriteString(fmt.Sprintf(" id = %s\n", id))
|
|
||||||
|
|
||||||
if is != nil {
|
|
||||||
// Sort the attributes
|
|
||||||
attrKeys := make([]string, 0, len(is.Attributes))
|
|
||||||
for ak, _ := range is.Attributes {
|
|
||||||
// Skip the id attribute since we just show the id directly
|
|
||||||
if ak == "id" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
attrKeys = append(attrKeys, ak)
|
|
||||||
}
|
|
||||||
sort.Strings(attrKeys)
|
|
||||||
|
|
||||||
// Output each attribute
|
|
||||||
for _, ak := range attrKeys {
|
|
||||||
av := is.Attributes[ak]
|
|
||||||
buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString("[reset]\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatStateModuleSingle(
|
|
||||||
buf *bytes.Buffer, m *terraform.ModuleState, opts *FormatStateOpts) {
|
|
||||||
// Header with the module name
|
|
||||||
buf.WriteString(fmt.Sprintf("module.%s\n", strings.Join(m.Path[1:], ".")))
|
|
||||||
|
|
||||||
// Now just write how many resources are in here.
|
|
||||||
buf.WriteString(fmt.Sprintf(" %d resource(s)\n", len(m.Resources)))
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package command
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
@ -28,19 +27,10 @@ func (c *GetCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
var path string
|
var path string
|
||||||
args = cmdFlags.Args()
|
path, err := ModulePath(cmdFlags.Args())
|
||||||
if len(args) > 1 {
|
|
||||||
c.Ui.Error("The get command expects one argument.\n")
|
|
||||||
cmdFlags.Usage()
|
|
||||||
return 1
|
|
||||||
} else if len(args) == 1 {
|
|
||||||
path = args[0]
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
path, err = os.Getwd()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
c.Ui.Error(err.Error())
|
||||||
}
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := module.GetModeGet
|
mode := module.GetModeGet
|
||||||
|
@ -48,12 +38,8 @@ func (c *GetCommand) Run(args []string) int {
|
||||||
mode = module.GetModeUpdate
|
mode = module.GetModeUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err := c.Context(contextOpts{
|
if err := getModules(&c.Meta, path, mode); err != nil {
|
||||||
Path: path,
|
c.Ui.Error(err.Error())
|
||||||
GetMode: mode,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,3 +72,17 @@ Options:
|
||||||
func (c *GetCommand) Synopsis() string {
|
func (c *GetCommand) Synopsis() string {
|
||||||
return "Download and install modules for the configuration"
|
return "Download and install modules for the configuration"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getModules(m *Meta, path string, mode module.GetMode) error {
|
||||||
|
mod, err := module.NewTreeModule("", path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error loading configuration: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mod.Load(m.moduleStorage(m.DataDir()), mode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error loading modules: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ package command
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/dag"
|
"github.com/hashicorp/terraform/dag"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
@ -34,34 +35,65 @@ func (c *GraphCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var path string
|
configPath, err := ModulePath(cmdFlags.Args())
|
||||||
args = cmdFlags.Args()
|
|
||||||
if len(args) > 1 {
|
|
||||||
c.Ui.Error("The graph command expects one argument.\n")
|
|
||||||
cmdFlags.Usage()
|
|
||||||
return 1
|
|
||||||
} else if len(args) == 1 {
|
|
||||||
path = args[0]
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
path, err = os.Getwd()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path is a plan
|
||||||
|
plan, err := c.Plan(configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if plan != nil {
|
||||||
|
// Reset for backend loading
|
||||||
|
configPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the module
|
||||||
|
var mod *module.Tree
|
||||||
|
if plan == nil {
|
||||||
|
mod, err = c.Module(configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, planFile, err := c.Context(contextOpts{
|
// Load the backend
|
||||||
Path: path,
|
b, err := c.Backend(&BackendOpts{
|
||||||
StatePath: "",
|
ConfigPath: configPath,
|
||||||
|
Plan: plan,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// We require a local backend
|
||||||
|
local, ok := b.(backend.Local)
|
||||||
|
if !ok {
|
||||||
|
c.Ui.Error(ErrUnsupportedLocalOp)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the operation
|
||||||
|
opReq := c.Operation()
|
||||||
|
opReq.Module = mod
|
||||||
|
opReq.Plan = plan
|
||||||
|
|
||||||
|
// Get the context
|
||||||
|
ctx, _, err := local.Context(opReq)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the graph type
|
// Determine the graph type
|
||||||
graphType := terraform.GraphTypePlan
|
graphType := terraform.GraphTypePlan
|
||||||
if planFile {
|
if plan != nil {
|
||||||
graphType = terraform.GraphTypeApply
|
graphType = terraform.GraphTypeApply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,13 +47,37 @@ func (c *ImportCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
// Load the module
|
||||||
ctx, _, err := c.Context(contextOpts{
|
var mod *module.Tree
|
||||||
Path: configPath,
|
if configPath != "" {
|
||||||
PathEmptyOk: true,
|
var err error
|
||||||
StatePath: c.Meta.statePath,
|
mod, err = c.Module(configPath)
|
||||||
Parallelism: c.Meta.parallelism,
|
if err != nil {
|
||||||
})
|
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the backend
|
||||||
|
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// We require a local backend
|
||||||
|
local, ok := b.(backend.Local)
|
||||||
|
if !ok {
|
||||||
|
c.Ui.Error(ErrUnsupportedLocalOp)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the operation
|
||||||
|
opReq := c.Operation()
|
||||||
|
opReq.Module = mod
|
||||||
|
|
||||||
|
// Get the context
|
||||||
|
ctx, state, err := local.Context(opReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
|
@ -76,7 +102,11 @@ func (c *ImportCommand) Run(args []string) int {
|
||||||
|
|
||||||
// Persist the final state
|
// Persist the final state
|
||||||
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
||||||
if err := c.Meta.PersistState(newState); err != nil {
|
if err := state.WriteState(newState); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := state.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
253
command/init.go
253
command/init.go
|
@ -1,7 +1,6 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -10,7 +9,6 @@ import (
|
||||||
"github.com/hashicorp/go-getter"
|
"github.com/hashicorp/go-getter"
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitCommand is a Command implementation that takes a Terraform
|
// InitCommand is a Command implementation that takes a Terraform
|
||||||
|
@ -20,39 +18,48 @@ type InitCommand struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *InitCommand) Run(args []string) int {
|
func (c *InitCommand) Run(args []string) int {
|
||||||
var remoteBackend string
|
var flagBackend, flagGet bool
|
||||||
|
var flagConfigFile string
|
||||||
args = c.Meta.process(args, false)
|
args = c.Meta.process(args, false)
|
||||||
remoteConfig := make(map[string]string)
|
cmdFlags := c.flagSet("init")
|
||||||
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
|
cmdFlags.BoolVar(&flagBackend, "backend", true, "")
|
||||||
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
|
cmdFlags.StringVar(&flagConfigFile, "backend-config", "", "")
|
||||||
cmdFlags.Var((*FlagStringKV)(&remoteConfig), "backend-config", "config")
|
cmdFlags.BoolVar(&flagGet, "get", true, "")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteBackend = strings.ToLower(remoteBackend)
|
// Validate the arg count
|
||||||
|
|
||||||
var path string
|
|
||||||
args = cmdFlags.Args()
|
args = cmdFlags.Args()
|
||||||
if len(args) > 2 {
|
if len(args) > 2 {
|
||||||
c.Ui.Error("The init command expects at most two arguments.\n")
|
c.Ui.Error("The init command expects at most two arguments.\n")
|
||||||
cmdFlags.Usage()
|
cmdFlags.Usage()
|
||||||
return 1
|
return 1
|
||||||
} else if len(args) < 1 {
|
}
|
||||||
c.Ui.Error("The init command expects at least one arguments.\n")
|
|
||||||
cmdFlags.Usage()
|
// Get our pwd. We don't always need it but always getting it is easier
|
||||||
|
// than the logic to determine if it is or isn't needed.
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) == 2 {
|
// Get the path and source module to copy
|
||||||
|
var path string
|
||||||
|
var source string
|
||||||
|
switch len(args) {
|
||||||
|
case 0:
|
||||||
|
path = pwd
|
||||||
|
case 1:
|
||||||
|
path = pwd
|
||||||
|
source = args[0]
|
||||||
|
case 2:
|
||||||
|
source = args[0]
|
||||||
path = args[1]
|
path = args[1]
|
||||||
} else {
|
default:
|
||||||
var err error
|
panic("assertion failed on arg count")
|
||||||
path, err = os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the state out path to be the path requested for the module
|
// Set the state out path to be the path requested for the module
|
||||||
|
@ -60,103 +67,152 @@ func (c *InitCommand) Run(args []string) int {
|
||||||
// proper directory.
|
// proper directory.
|
||||||
c.Meta.dataDir = filepath.Join(path, DefaultDataDir)
|
c.Meta.dataDir = filepath.Join(path, DefaultDataDir)
|
||||||
|
|
||||||
source := args[0]
|
// This will track whether we outputted anything so that we know whether
|
||||||
|
// to output a newline before the success message
|
||||||
|
var header bool
|
||||||
|
|
||||||
// Get our pwd since we need it
|
// If we have a source, copy it
|
||||||
pwd, err := os.Getwd()
|
if source != "" {
|
||||||
if err != nil {
|
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
||||||
|
"[reset][bold]"+
|
||||||
|
"Initializing configuration from: %q...", source)))
|
||||||
|
if err := c.copySource(path, source, pwd); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Error reading working directory: %s", err))
|
"Error copying source: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the directory is empty
|
header = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our directory is empty, then we're done. We can't get or setup
|
||||||
|
// the backend with an empty directory.
|
||||||
if empty, err := config.IsEmptyDir(path); err != nil {
|
if empty, err := config.IsEmptyDir(path); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Error checking on destination path: %s", err))
|
"Error checking configuration: %s", err))
|
||||||
return 1
|
|
||||||
} else if !empty {
|
|
||||||
c.Ui.Error(
|
|
||||||
"The destination path has Terraform configuration files. The\n" +
|
|
||||||
"init command can only be used on a directory without existing Terraform\n" +
|
|
||||||
"files.")
|
|
||||||
return 1
|
return 1
|
||||||
|
} else if empty {
|
||||||
|
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty)))
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect
|
// If we're performing a get or loading the backend, then we perform
|
||||||
source, err = getter.Detect(source, pwd, getter.Detectors)
|
// some extra tasks.
|
||||||
|
if flagGet || flagBackend {
|
||||||
|
// Load the configuration in this directory so that we can know
|
||||||
|
// if we have anything to get or any backend to configure. We do
|
||||||
|
// this to improve the UX. Practically, we could call the functions
|
||||||
|
// below without checking this to the same effect.
|
||||||
|
conf, err := config.LoadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Error with module source: %s", err))
|
"Error loading configuration: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get it!
|
// If we requested downloading modules and have modules in the config
|
||||||
if err := module.GetCopy(path, source); err != nil {
|
if flagGet && len(conf.Modules) > 0 {
|
||||||
|
header = true
|
||||||
|
|
||||||
|
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
||||||
|
"[reset][bold]" +
|
||||||
|
"Downloading modules (if any)...")))
|
||||||
|
if err := getModules(&c.Meta, path, module.GetModeGet); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"Error downloading modules: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're requesting backend configuration and configure it
|
||||||
|
hasBackend := conf.Terraform != nil && conf.Terraform.Backend != nil
|
||||||
|
if flagBackend && hasBackend {
|
||||||
|
header = true
|
||||||
|
|
||||||
|
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
||||||
|
"[reset][bold]" +
|
||||||
|
"Initializing the backend...")))
|
||||||
|
|
||||||
|
opts := &BackendOpts{
|
||||||
|
ConfigPath: path,
|
||||||
|
ConfigFile: flagConfigFile,
|
||||||
|
Init: true,
|
||||||
|
}
|
||||||
|
if _, err := c.Backend(opts); err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle remote state if configured
|
|
||||||
if remoteBackend != "" {
|
|
||||||
var remoteConf terraform.RemoteState
|
|
||||||
remoteConf.Type = remoteBackend
|
|
||||||
remoteConf.Config = remoteConfig
|
|
||||||
|
|
||||||
state, err := c.State()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error checking for state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if state != nil {
|
|
||||||
s := state.State()
|
|
||||||
if !s.Empty() {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"State file already exists and is not empty! Please remove this\n" +
|
|
||||||
"state file before initializing. Note that removing the state file\n" +
|
|
||||||
"may result in a loss of information since Terraform uses this\n" +
|
|
||||||
"to track your infrastructure."))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if s.IsRemote() {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"State file already exists with remote state enabled! Please remove this\n" +
|
|
||||||
"state file before initializing. Note that removing the state file\n" +
|
|
||||||
"may result in a loss of information since Terraform uses this\n" +
|
|
||||||
"to track your infrastructure."))
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a blank state file with remote enabled
|
// If we outputted information, then we need to output a newline
|
||||||
remoteCmd := &RemoteConfigCommand{
|
// so that our success message is nicely spaced out from prior text.
|
||||||
Meta: c.Meta,
|
if header {
|
||||||
remoteConf: &remoteConf,
|
c.Ui.Output("")
|
||||||
}
|
|
||||||
return remoteCmd.initBlankState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess)))
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *InitCommand) copySource(dst, src, pwd string) error {
|
||||||
|
// Verify the directory is empty
|
||||||
|
if empty, err := config.IsEmptyDir(dst); err != nil {
|
||||||
|
return fmt.Errorf("Error checking on destination path: %s", err)
|
||||||
|
} else if !empty {
|
||||||
|
return fmt.Errorf(strings.TrimSpace(errInitCopyNotEmpty))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect
|
||||||
|
source, err := getter.Detect(src, pwd, getter.Detectors)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error with module source: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get it!
|
||||||
|
return module.GetCopy(dst, source)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *InitCommand) Help() string {
|
func (c *InitCommand) Help() string {
|
||||||
helpText := `
|
helpText := `
|
||||||
Usage: terraform init [options] SOURCE [PATH]
|
Usage: terraform init [options] [SOURCE] [PATH]
|
||||||
|
|
||||||
Downloads the module given by SOURCE into the PATH. The PATH defaults
|
Initialize a new or existing Terraform environment by creating
|
||||||
to the working directory. PATH must be empty of any Terraform files.
|
initial files, loading any remote state, downloading modules, etc.
|
||||||
Any conflicting non-Terraform files will be overwritten.
|
|
||||||
|
|
||||||
The module downloaded is a copy. If you're downloading a module from
|
This is the first command that should be run for any new or existing
|
||||||
Git, it will not preserve the Git history, it will only copy the
|
Terraform configuration per machine. This sets up all the local data
|
||||||
latest files.
|
necessary to run Terraform that is typically not comitted to version
|
||||||
|
control.
|
||||||
|
|
||||||
|
This command is always safe to run multiple times. Though subsequent runs
|
||||||
|
may give errors, this command will never blow away your environment or state.
|
||||||
|
Even so, if you have important information, please back it up prior to
|
||||||
|
running this command just in case.
|
||||||
|
|
||||||
|
If no arguments are given, the configuration in this working directory
|
||||||
|
is initialized.
|
||||||
|
|
||||||
|
If one or two arguments are given, the first is a SOURCE of a module to
|
||||||
|
download to the second argument PATH. After downloading the module to PATH,
|
||||||
|
the configuration will be initialized as if this command were called pointing
|
||||||
|
only to that PATH. PATH must be empty of any Terraform files. Any
|
||||||
|
conflicting non-Terraform files will be overwritten. The module download
|
||||||
|
is a copy. If you're downloading a module from Git, it will not preserve
|
||||||
|
Git history.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-backend=atlas Specifies the type of remote backend. If not
|
-backend=true Configure the backend for this environment.
|
||||||
specified, local storage will be used.
|
|
||||||
|
|
||||||
-backend-config="k=v" Specifies configuration for the remote storage
|
-backend-config=path A path to load additional configuration for the backend.
|
||||||
backend. This can be specified multiple times.
|
This is merged with what is in the configuration file.
|
||||||
|
|
||||||
|
-get=true Download any modules for this configuration.
|
||||||
|
|
||||||
|
-input=true Ask for input if necessary. If false, will error if
|
||||||
|
input was required.
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
-no-color If specified, output won't contain any color.
|
||||||
|
|
||||||
|
@ -165,5 +221,32 @@ Options:
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *InitCommand) Synopsis() string {
|
func (c *InitCommand) Synopsis() string {
|
||||||
return "Initializes Terraform configuration from a module"
|
return "Initialize a new or existing Terraform configuration"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errInitCopyNotEmpty = `
|
||||||
|
The destination path contains Terraform configuration files. The init command
|
||||||
|
with a SOURCE parameter can only be used on a directory without existing
|
||||||
|
Terraform files.
|
||||||
|
|
||||||
|
Please resolve this issue and try again.
|
||||||
|
`
|
||||||
|
|
||||||
|
const outputInitEmpty = `
|
||||||
|
[reset][bold]Terraform initialized in an empty directory![reset]
|
||||||
|
|
||||||
|
The directory has no Terraform configuration files. You may begin working
|
||||||
|
with Terraform immediately by creating Terraform configuration files.
|
||||||
|
`
|
||||||
|
|
||||||
|
const outputInitSuccess = `
|
||||||
|
[reset][bold][green]Terraform has been successfully initialized![reset][green]
|
||||||
|
|
||||||
|
You may now begin working with Terraform. Try running "terraform plan" to see
|
||||||
|
any changes that are required for your infrastructure. All Terraform commands
|
||||||
|
should now work.
|
||||||
|
|
||||||
|
If you ever set or change modules or backend configuration for Terraform,
|
||||||
|
rerun this command to reinitialize your environment. If you forget, other
|
||||||
|
commands will detect it and remind you to do so if necessary.
|
||||||
|
`
|
||||||
|
|
|
@ -3,9 +3,10 @@ package command
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/helper/copy"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,6 +70,27 @@ func TestInit_cwd(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInit_empty(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
os.MkdirAll(td, 0755)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInit_multipleArgs(t *testing.T) {
|
func TestInit_multipleArgs(t *testing.T) {
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &InitCommand{
|
c := &InitCommand{
|
||||||
|
@ -87,21 +109,6 @@ func TestInit_multipleArgs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInit_noArgs(t *testing.T) {
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &InitCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{}
|
|
||||||
if code := c.Run(args); code != 1 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/hashicorp/terraform/issues/518
|
// https://github.com/hashicorp/terraform/issues/518
|
||||||
func TestInit_dstInSrc(t *testing.T) {
|
func TestInit_dstInSrc(t *testing.T) {
|
||||||
dir := tempDir(t)
|
dir := tempDir(t)
|
||||||
|
@ -144,6 +151,148 @@ func TestInit_dstInSrc(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInit_get(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("init-get"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check output
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if !strings.Contains(output, "Get: file://") {
|
||||||
|
t.Fatalf("doesn't look like get: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_copyGet(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
os.MkdirAll(td, 0755)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
testFixturePath("init-get"),
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check copy
|
||||||
|
if _, err := os.Stat("main.tf"); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if !strings.Contains(output, "Get: file://") {
|
||||||
|
t.Fatalf("doesn't look like get: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_backend(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("init-backend"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_backendConfigFile(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("init-backend-config-file"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"-backend-config", "input.config"}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read our saved backend config and verify we have our settings
|
||||||
|
state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
|
||||||
|
if v := state.Backend.Config["path"]; v != "hello" {
|
||||||
|
t.Fatalf("bad: %#v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_copyBackendDst(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
os.MkdirAll(td, 0755)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &InitCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(testProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
testFixturePath("init-backend"),
|
||||||
|
"dst",
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(
|
||||||
|
"dst", DefaultDataDir, DefaultStateFilename)); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func TestInit_remoteState(t *testing.T) {
|
func TestInit_remoteState(t *testing.T) {
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
@ -287,3 +436,4 @@ func TestInit_remoteStateWithRemote(t *testing.T) {
|
||||||
t.Fatalf("should have failed: \n%s", ui.OutputWriter.String())
|
t.Fatalf("should have failed: \n%s", ui.OutputWriter.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
248
command/meta.go
248
command/meta.go
|
@ -10,16 +10,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/errwrap"
|
|
||||||
"github.com/hashicorp/go-getter"
|
"github.com/hashicorp/go-getter"
|
||||||
"github.com/hashicorp/terraform/config"
|
|
||||||
"github.com/hashicorp/terraform/config/module"
|
|
||||||
"github.com/hashicorp/terraform/helper/experiment"
|
"github.com/hashicorp/terraform/helper/experiment"
|
||||||
"github.com/hashicorp/terraform/helper/variables"
|
"github.com/hashicorp/terraform/helper/variables"
|
||||||
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
||||||
"github.com/hashicorp/terraform/state"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/mitchellh/colorstring"
|
"github.com/mitchellh/colorstring"
|
||||||
|
@ -27,21 +24,31 @@ import (
|
||||||
|
|
||||||
// Meta are the meta-options that are available on all or most commands.
|
// Meta are the meta-options that are available on all or most commands.
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
Color bool
|
// The exported fields below should be set by anyone using a
|
||||||
ContextOpts *terraform.ContextOpts
|
// command with a Meta field. These are expected to be set externally
|
||||||
Ui cli.Ui
|
// (not from within the command itself).
|
||||||
|
|
||||||
// State read when calling `Context`. This is available after calling
|
Color bool // True if output should be colored
|
||||||
// `Context`.
|
ContextOpts *terraform.ContextOpts // Opts copied to initialize
|
||||||
state state.State
|
Ui cli.Ui // Ui for output
|
||||||
stateResult *StateResult
|
|
||||||
|
|
||||||
// This can be set by the command itself to provide extra hooks.
|
// ExtraHooks are extra hooks to add to the context.
|
||||||
extraHooks []terraform.Hook
|
ExtraHooks []terraform.Hook
|
||||||
|
|
||||||
// This can be set by tests to change some directories
|
//----------------------------------------------------------
|
||||||
|
// Protected: commands can set these
|
||||||
|
//----------------------------------------------------------
|
||||||
|
|
||||||
|
// Modify the data directory location. Defaults to DefaultDataDir
|
||||||
dataDir string
|
dataDir string
|
||||||
|
|
||||||
|
//----------------------------------------------------------
|
||||||
|
// Private: do not set these
|
||||||
|
//----------------------------------------------------------
|
||||||
|
|
||||||
|
// backendState is the currently active backend state
|
||||||
|
backendState *terraform.BackendState
|
||||||
|
|
||||||
// Variables for the context (private)
|
// Variables for the context (private)
|
||||||
autoKey string
|
autoKey string
|
||||||
autoVariables map[string]interface{}
|
autoVariables map[string]interface{}
|
||||||
|
@ -51,6 +58,7 @@ type Meta struct {
|
||||||
// Targets for this context (private)
|
// Targets for this context (private)
|
||||||
targets []string
|
targets []string
|
||||||
|
|
||||||
|
// Internal fields
|
||||||
color bool
|
color bool
|
||||||
oldUi cli.Ui
|
oldUi cli.Ui
|
||||||
|
|
||||||
|
@ -111,103 +119,6 @@ func (m *Meta) Colorize() *colorstring.Colorize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context returns a Terraform Context taking into account the context
|
|
||||||
// options used to initialize this meta configuration.
|
|
||||||
func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
|
||||||
opts := m.contextOpts()
|
|
||||||
|
|
||||||
// First try to just read the plan directly from the path given.
|
|
||||||
f, err := os.Open(copts.Path)
|
|
||||||
if err == nil {
|
|
||||||
plan, err := terraform.ReadPlan(f)
|
|
||||||
f.Close()
|
|
||||||
if err == nil {
|
|
||||||
// Setup our state, force it to use our plan's state
|
|
||||||
stateOpts := m.StateOpts()
|
|
||||||
if plan != nil {
|
|
||||||
stateOpts.ForceState = plan.State
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the state
|
|
||||||
result, err := State(stateOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, fmt.Errorf("Error loading plan: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set our state
|
|
||||||
m.state = result.State
|
|
||||||
|
|
||||||
// this is used for printing the saved location later
|
|
||||||
if m.stateOutPath == "" {
|
|
||||||
m.stateOutPath = result.StatePath
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.variables) > 0 {
|
|
||||||
return nil, false, fmt.Errorf(
|
|
||||||
"You can't set variables with the '-var' or '-var-file' flag\n" +
|
|
||||||
"when you're applying a plan file. The variables used when\n" +
|
|
||||||
"the plan was created will be used. If you wish to use different\n" +
|
|
||||||
"variable values, create a new plan file.")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, err := plan.Context(opts)
|
|
||||||
return ctx, true, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the statePath if not given
|
|
||||||
if copts.StatePath != "" {
|
|
||||||
m.statePath = copts.StatePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the context if we're in a destroy plan / apply
|
|
||||||
opts.Destroy = copts.Destroy
|
|
||||||
|
|
||||||
// Store the loaded state
|
|
||||||
state, err := m.State()
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the root module
|
|
||||||
var mod *module.Tree
|
|
||||||
if copts.Path != "" {
|
|
||||||
mod, err = module.NewTreeModule("", copts.Path)
|
|
||||||
|
|
||||||
// Check for the error where we have no config files but
|
|
||||||
// allow that. If that happens, clear the error.
|
|
||||||
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) &&
|
|
||||||
copts.PathEmptyOk {
|
|
||||||
log.Printf(
|
|
||||||
"[WARN] Empty configuration dir, ignoring: %s", copts.Path)
|
|
||||||
err = nil
|
|
||||||
mod = module.NewEmptyTree()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, fmt.Errorf("Error loading config: %s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mod = module.NewEmptyTree()
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, fmt.Errorf("Error downloading modules: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the module right away
|
|
||||||
if err := mod.Validate(); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.Module = mod
|
|
||||||
opts.Parallelism = copts.Parallelism
|
|
||||||
opts.State = state.State()
|
|
||||||
ctx, err := terraform.NewContext(opts)
|
|
||||||
return ctx, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataDir returns the directory where local data will be stored.
|
// DataDir returns the directory where local data will be stored.
|
||||||
func (m *Meta) DataDir() string {
|
func (m *Meta) DataDir() string {
|
||||||
dataDir := DefaultDataDir
|
dataDir := DefaultDataDir
|
||||||
|
@ -248,53 +159,6 @@ func (m *Meta) InputMode() terraform.InputMode {
|
||||||
return mode
|
return mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// State returns the state for this meta.
|
|
||||||
func (m *Meta) State() (state.State, error) {
|
|
||||||
if m.state != nil {
|
|
||||||
return m.state, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := State(m.StateOpts())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.state = result.State
|
|
||||||
m.stateOutPath = result.StatePath
|
|
||||||
m.stateResult = result
|
|
||||||
return m.state, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateRaw is used to setup the state manually.
|
|
||||||
func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) {
|
|
||||||
result, err := State(opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.state = result.State
|
|
||||||
m.stateOutPath = result.StatePath
|
|
||||||
m.stateResult = result
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateOpts returns the default state options
|
|
||||||
func (m *Meta) StateOpts() *StateOpts {
|
|
||||||
localPath := m.statePath
|
|
||||||
if localPath == "" {
|
|
||||||
localPath = DefaultStateFilename
|
|
||||||
}
|
|
||||||
remotePath := filepath.Join(m.DataDir(), DefaultStateFilename)
|
|
||||||
|
|
||||||
return &StateOpts{
|
|
||||||
LocalPath: localPath,
|
|
||||||
LocalPathOut: m.stateOutPath,
|
|
||||||
RemotePath: remotePath,
|
|
||||||
RemoteRefresh: true,
|
|
||||||
BackupPath: m.backupPath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIInput returns a UIInput object to be used for asking for input.
|
// UIInput returns a UIInput object to be used for asking for input.
|
||||||
func (m *Meta) UIInput() terraform.UIInput {
|
func (m *Meta) UIInput() terraform.UIInput {
|
||||||
return &UIInput{
|
return &UIInput{
|
||||||
|
@ -302,21 +166,6 @@ func (m *Meta) UIInput() terraform.UIInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PersistState is used to write out the state, handling backup of
|
|
||||||
// the existing state file and respecting path configurations.
|
|
||||||
func (m *Meta) PersistState(s *terraform.State) error {
|
|
||||||
if err := m.state.WriteState(s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.state.PersistState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input returns true if we should ask for input for context.
|
|
||||||
func (m *Meta) Input() bool {
|
|
||||||
return !test && m.input && len(m.variables) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// StdinPiped returns true if the input is piped.
|
// StdinPiped returns true if the input is piped.
|
||||||
func (m *Meta) StdinPiped() bool {
|
func (m *Meta) StdinPiped() bool {
|
||||||
fi, err := wrappedstreams.Stdin().Stat()
|
fi, err := wrappedstreams.Stdin().Stat()
|
||||||
|
@ -331,11 +180,16 @@ func (m *Meta) StdinPiped() bool {
|
||||||
// contextOpts returns the options to use to initialize a Terraform
|
// contextOpts returns the options to use to initialize a Terraform
|
||||||
// context with the settings from this Meta.
|
// context with the settings from this Meta.
|
||||||
func (m *Meta) contextOpts() *terraform.ContextOpts {
|
func (m *Meta) contextOpts() *terraform.ContextOpts {
|
||||||
var opts terraform.ContextOpts = *m.ContextOpts
|
var opts terraform.ContextOpts
|
||||||
|
if v := m.ContextOpts; v != nil {
|
||||||
|
opts = *v
|
||||||
|
}
|
||||||
|
|
||||||
opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
|
opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
|
||||||
|
if m.ContextOpts != nil {
|
||||||
opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
|
opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
|
||||||
opts.Hooks = append(opts.Hooks, m.extraHooks...)
|
}
|
||||||
|
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
|
||||||
|
|
||||||
vs := make(map[string]interface{})
|
vs := make(map[string]interface{})
|
||||||
for k, v := range opts.Variables {
|
for k, v := range opts.Variables {
|
||||||
|
@ -350,6 +204,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
|
||||||
opts.Variables = vs
|
opts.Variables = vs
|
||||||
opts.Targets = m.targets
|
opts.Targets = m.targets
|
||||||
opts.UIInput = m.UIInput()
|
opts.UIInput = m.UIInput()
|
||||||
|
opts.Parallelism = m.parallelism
|
||||||
opts.Shadow = m.shadow
|
opts.Shadow = m.shadow
|
||||||
|
|
||||||
return &opts
|
return &opts
|
||||||
|
@ -469,6 +324,24 @@ func (m *Meta) uiHook() *UiHook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// confirm asks a yes/no confirmation.
|
||||||
|
func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
|
||||||
|
for {
|
||||||
|
v, err := m.UIInput().Input(opts)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"Error asking for confirmation: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(v) {
|
||||||
|
case "no":
|
||||||
|
return false, nil
|
||||||
|
case "yes":
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ModuleDepthDefault is the default value for
|
// ModuleDepthDefault is the default value for
|
||||||
// module depth, which can be overridden by flag
|
// module depth, which can be overridden by flag
|
||||||
|
@ -530,28 +403,3 @@ func (m *Meta) outputShadowError(err error, output bool) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// contextOpts are the options used to load a context from a command.
|
|
||||||
type contextOpts struct {
|
|
||||||
// Path to the directory where the root module is.
|
|
||||||
//
|
|
||||||
// PathEmptyOk, when set, will allow paths that have no Terraform
|
|
||||||
// configurations. The result in that case will be an empty module.
|
|
||||||
Path string
|
|
||||||
PathEmptyOk bool
|
|
||||||
|
|
||||||
// StatePath is the path to the state file. If this is empty, then
|
|
||||||
// no state will be loaded. It is also okay for this to be a path to
|
|
||||||
// a file that doesn't exist; it is assumed that this means that there
|
|
||||||
// is simply no state.
|
|
||||||
StatePath string
|
|
||||||
|
|
||||||
// GetMode is the module.GetMode to use when loading the module tree.
|
|
||||||
GetMode module.GetMode
|
|
||||||
|
|
||||||
// Set to true when running a destroy plan/apply.
|
|
||||||
Destroy bool
|
|
||||||
|
|
||||||
// Number of concurrent operations allowed
|
|
||||||
Parallelism int
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/errwrap"
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: Temporary file until this branch is cleaned up.
|
||||||
|
|
||||||
|
// Input returns whether or not input asking is enabled.
|
||||||
|
func (m *Meta) Input() bool {
|
||||||
|
if test || !m.input {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
|
||||||
|
if v, err := strconv.ParseBool(envVar); err == nil && !v {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module loads the module tree for the given root path.
|
||||||
|
//
|
||||||
|
// It expects the modules to already be downloaded. This will never
|
||||||
|
// download any modules.
|
||||||
|
func (m *Meta) Module(path string) (*module.Tree, error) {
|
||||||
|
mod, err := module.NewTreeModule("", path)
|
||||||
|
if err != nil {
|
||||||
|
// Check for the error where we have no config files
|
||||||
|
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mod.Load(m.moduleStorage(m.DataDir()), module.GetModeNone)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error loading modules: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan returns the plan for the given path.
|
||||||
|
//
|
||||||
|
// This only has an effect if the path itself looks like a plan.
|
||||||
|
// If error is nil and the plan is nil, then the path didn't look like
|
||||||
|
// a plan.
|
||||||
|
//
|
||||||
|
// Error will be non-nil if path looks like a plan and loading the plan
|
||||||
|
// failed.
|
||||||
|
func (m *Meta) Plan(path string) (*terraform.Plan, error) {
|
||||||
|
// Open the path no matter if its a directory or file
|
||||||
|
f, err := os.Open(path)
|
||||||
|
defer f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Failed to load Terraform configuration or plan: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat it so we can check if its a directory
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Failed to load Terraform configuration or plan: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this path is a directory, then it can't be a plan. Not an error.
|
||||||
|
if fi.IsDir() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the plan
|
||||||
|
p, err := terraform.ReadPlan(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do a validation here that seems odd but if any plan is given,
|
||||||
|
// we must not have set any extra variables. The plan itself contains
|
||||||
|
// the variables and those aren't overwritten.
|
||||||
|
if len(m.variables) > 0 {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"You can't set variables with the '-var' or '-var-file' flag\n" +
|
||||||
|
"when you're applying a plan file. The variables used when\n" +
|
||||||
|
"the plan was created will be used. If you wish to use different\n" +
|
||||||
|
"variable values, create a new plan file.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
|
@ -20,13 +20,11 @@ func (c *OutputCommand) Run(args []string) int {
|
||||||
|
|
||||||
var module string
|
var module string
|
||||||
var jsonOutput bool
|
var jsonOutput bool
|
||||||
|
|
||||||
cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError)
|
cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError)
|
||||||
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||||
cmdFlags.StringVar(&module, "module", "", "module")
|
cmdFlags.StringVar(&module, "module", "", "module")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
|
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
@ -45,9 +43,17 @@ func (c *OutputCommand) Run(args []string) int {
|
||||||
name = args[0]
|
name = args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
stateStore, err := c.Meta.State()
|
// Load the backend
|
||||||
|
b, err := c.Backend(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
stateStore, err := b.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +68,6 @@ func (c *OutputCommand) Run(args []string) int {
|
||||||
|
|
||||||
state := stateStore.State()
|
state := stateStore.State()
|
||||||
mod := state.ModuleByPath(modPath)
|
mod := state.ModuleByPath(modPath)
|
||||||
|
|
||||||
if mod == nil {
|
if mod == nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"The module %s could not be found. There is nothing to output.",
|
"The module %s could not be found. There is nothing to output.",
|
||||||
|
@ -211,6 +216,7 @@ func formatNestedMap(indent string, outputMap map[string]interface{}) string {
|
||||||
|
|
||||||
return strings.TrimPrefix(outputBuf.String(), "\n")
|
return strings.TrimPrefix(outputBuf.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
|
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
|
||||||
ks := make([]string, 0, len(outputMap))
|
ks := make([]string, 0, len(outputMap))
|
||||||
for k, _ := range outputMap {
|
for k, _ := range outputMap {
|
||||||
|
|
215
command/plan.go
215
command/plan.go
|
@ -1,13 +1,12 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PlanCommand is a Command implementation that compares a Terraform
|
// PlanCommand is a Command implementation that compares a Terraform
|
||||||
|
@ -30,153 +29,88 @@ func (c *PlanCommand) Run(args []string) int {
|
||||||
cmdFlags.StringVar(&outPath, "out", "", "path")
|
cmdFlags.StringVar(&outPath, "out", "", "path")
|
||||||
cmdFlags.IntVar(
|
cmdFlags.IntVar(
|
||||||
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
|
&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism")
|
||||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
|
||||||
cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode")
|
cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var path string
|
configPath, err := ModulePath(cmdFlags.Args())
|
||||||
args = cmdFlags.Args()
|
|
||||||
if len(args) > 1 {
|
|
||||||
c.Ui.Error(
|
|
||||||
"The plan command expects at most one argument with the path\n" +
|
|
||||||
"to a Terraform configuration.\n")
|
|
||||||
cmdFlags.Usage()
|
|
||||||
return 1
|
|
||||||
} else if len(args) == 1 {
|
|
||||||
path = args[0]
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
path, err = os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
countHook := new(CountHook)
|
|
||||||
c.Meta.extraHooks = []terraform.Hook{countHook}
|
|
||||||
|
|
||||||
// This is going to keep track of shadow errors
|
|
||||||
var shadowErr error
|
|
||||||
|
|
||||||
ctx, planned, err := c.Context(contextOpts{
|
|
||||||
Destroy: destroy,
|
|
||||||
Path: path,
|
|
||||||
StatePath: c.Meta.statePath,
|
|
||||||
Parallelism: c.Meta.parallelism,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if planned {
|
|
||||||
c.Ui.Output(c.Colorize().Color(
|
|
||||||
"[reset][bold][yellow]" +
|
|
||||||
"The plan command received a saved plan file as input. This command\n" +
|
|
||||||
"will output the saved plan. This will not modify the already-existing\n" +
|
|
||||||
"plan. If you wish to generate a new plan, please pass in a configuration\n" +
|
|
||||||
"directory as an argument.\n\n"))
|
|
||||||
|
|
||||||
|
// Check if the path is a plan
|
||||||
|
plan, err := c.Plan(configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if plan != nil {
|
||||||
// Disable refreshing no matter what since we only want to show the plan
|
// Disable refreshing no matter what since we only want to show the plan
|
||||||
refresh = false
|
refresh = false
|
||||||
|
|
||||||
|
// Set the config path to empty for backend loading
|
||||||
|
configPath = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the module if we don't have one yet (not running from plan)
|
||||||
|
var mod *module.Tree
|
||||||
|
if plan == nil {
|
||||||
|
mod, err = c.Module(configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the backend
|
||||||
|
b, err := c.Backend(&BackendOpts{
|
||||||
|
ConfigPath: configPath,
|
||||||
|
Plan: plan,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the operation
|
||||||
|
opReq := c.Operation()
|
||||||
|
opReq.Destroy = destroy
|
||||||
|
opReq.Module = mod
|
||||||
|
opReq.Plan = plan
|
||||||
|
opReq.PlanRefresh = refresh
|
||||||
|
opReq.PlanOutPath = outPath
|
||||||
|
opReq.Type = backend.OperationTypePlan
|
||||||
|
|
||||||
|
// Perform the operation
|
||||||
|
op, err := b.Operation(context.Background(), opReq)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the operation to complete
|
||||||
|
<-op.Done()
|
||||||
|
if err := op.Err; err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
err = terraform.SetDebugInfo(DefaultDataDir)
|
err = terraform.SetDebugInfo(DefaultDataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if err := ctx.Input(c.InputMode()); err != nil {
|
if detailed && !op.PlanEmpty {
|
||||||
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record any shadow errors for later
|
|
||||||
if err := ctx.ShadowError(); err != nil {
|
|
||||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
|
||||||
err, "input operation:"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validateContext(ctx, c.Ui) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if refresh {
|
|
||||||
c.Ui.Output("Refreshing Terraform state in-memory prior to plan...")
|
|
||||||
c.Ui.Output("The refreshed state will be used to calculate this plan, but")
|
|
||||||
c.Ui.Output("will not be persisted to local or remote state storage.\n")
|
|
||||||
_, err := ctx.Refresh()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
c.Ui.Output("")
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, err := ctx.Plan()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error running plan: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if outPath != "" {
|
|
||||||
log.Printf("[INFO] Writing plan output to: %s", outPath)
|
|
||||||
f, err := os.Create(outPath)
|
|
||||||
if err == nil {
|
|
||||||
defer f.Close()
|
|
||||||
err = terraform.WritePlan(plan, f)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing plan file: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if plan.Diff.Empty() {
|
|
||||||
c.Ui.Output(
|
|
||||||
"No changes. Infrastructure is up-to-date. This means that Terraform\n" +
|
|
||||||
"could not detect any differences between your configuration and\n" +
|
|
||||||
"the real physical resources that exist. As a result, Terraform\n" +
|
|
||||||
"doesn't need to do anything.")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if outPath == "" {
|
|
||||||
c.Ui.Output(strings.TrimSpace(planHeaderNoOutput) + "\n")
|
|
||||||
} else {
|
|
||||||
c.Ui.Output(fmt.Sprintf(
|
|
||||||
strings.TrimSpace(planHeaderYesOutput)+"\n",
|
|
||||||
outPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Ui.Output(FormatPlan(&FormatPlanOpts{
|
|
||||||
Plan: plan,
|
|
||||||
Color: c.Colorize(),
|
|
||||||
ModuleDepth: moduleDepth,
|
|
||||||
}))
|
|
||||||
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset][bold]Plan:[reset] "+
|
|
||||||
"%d to add, %d to change, %d to destroy.",
|
|
||||||
countHook.ToAdd+countHook.ToRemoveAndAdd,
|
|
||||||
countHook.ToChange,
|
|
||||||
countHook.ToRemove+countHook.ToRemoveAndAdd)))
|
|
||||||
|
|
||||||
// Record any shadow errors for later
|
|
||||||
if err := ctx.ShadowError(); err != nil {
|
|
||||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
|
||||||
err, "plan operation:"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have an error in the shadow graph, let the user know.
|
|
||||||
c.outputShadowError(shadowErr, true)
|
|
||||||
|
|
||||||
if detailed {
|
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,28 +175,3 @@ Options:
|
||||||
func (c *PlanCommand) Synopsis() string {
|
func (c *PlanCommand) Synopsis() string {
|
||||||
return "Generate and show an execution plan"
|
return "Generate and show an execution plan"
|
||||||
}
|
}
|
||||||
|
|
||||||
const planHeaderNoOutput = `
|
|
||||||
The Terraform execution plan has been generated and is shown below.
|
|
||||||
Resources are shown in alphabetical order for quick scanning. Green resources
|
|
||||||
will be created (or destroyed and then created if an existing resource
|
|
||||||
exists), yellow resources are being changed in-place, and red resources
|
|
||||||
will be destroyed. Cyan entries are data sources to be read.
|
|
||||||
|
|
||||||
Note: You didn't specify an "-out" parameter to save this plan, so when
|
|
||||||
"apply" is called, Terraform can't guarantee this is what will execute.
|
|
||||||
`
|
|
||||||
|
|
||||||
const planHeaderYesOutput = `
|
|
||||||
The Terraform execution plan has been generated and is shown below.
|
|
||||||
Resources are shown in alphabetical order for quick scanning. Green resources
|
|
||||||
will be created (or destroyed and then created if an existing resource
|
|
||||||
exists), yellow resources are being changed in-place, and red resources
|
|
||||||
will be destroyed. Cyan entries are data sources to be read.
|
|
||||||
|
|
||||||
Your plan was also saved to the path below. Call the "apply" subcommand
|
|
||||||
with this plan file and Terraform will exactly execute this execution
|
|
||||||
plan.
|
|
||||||
|
|
||||||
Path: %s
|
|
||||||
`
|
|
||||||
|
|
|
@ -5,9 +5,11 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/copy"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -239,6 +241,128 @@ func TestPlan_outPathNoChange(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When using "-out" with a backend, the plan should encode the backend config
|
||||||
|
func TestPlan_outBackend(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("plan-out-backend"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Our state
|
||||||
|
originalState := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
originalState.Init()
|
||||||
|
|
||||||
|
// Setup our backend state
|
||||||
|
dataState, srv := testBackendState(t, originalState, 200)
|
||||||
|
defer srv.Close()
|
||||||
|
testStateFileRemote(t, dataState)
|
||||||
|
|
||||||
|
outPath := "foo"
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &PlanCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-out", outPath,
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := testReadPlan(t, outPath)
|
||||||
|
if !plan.Diff.Empty() {
|
||||||
|
t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
if plan.Backend.Empty() {
|
||||||
|
t.Fatal("should have backend info")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(plan.Backend, dataState.Backend) {
|
||||||
|
t.Fatalf("bad: %#v", plan.Backend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When using "-out" with a legacy remote state, the plan should encode
|
||||||
|
// the backend config
|
||||||
|
func TestPlan_outBackendLegacy(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("plan-out-backend-legacy"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Our state
|
||||||
|
originalState := &terraform.State{
|
||||||
|
Modules: []*terraform.ModuleState{
|
||||||
|
&terraform.ModuleState{
|
||||||
|
Path: []string{"root"},
|
||||||
|
Resources: map[string]*terraform.ResourceState{
|
||||||
|
"test_instance.foo": &terraform.ResourceState{
|
||||||
|
Type: "test_instance",
|
||||||
|
Primary: &terraform.InstanceState{
|
||||||
|
ID: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
originalState.Init()
|
||||||
|
|
||||||
|
// Setup our legacy state
|
||||||
|
remoteState, srv := testRemoteState(t, originalState, 200)
|
||||||
|
defer srv.Close()
|
||||||
|
dataState := terraform.NewState()
|
||||||
|
dataState.Remote = remoteState
|
||||||
|
testStateFileRemote(t, dataState)
|
||||||
|
|
||||||
|
outPath := "foo"
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &PlanCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-out", outPath,
|
||||||
|
}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := testReadPlan(t, outPath)
|
||||||
|
if !plan.Diff.Empty() {
|
||||||
|
t.Fatalf("Expected empty plan to be written to plan file, got: %s", plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
if plan.State.Remote.Empty() {
|
||||||
|
t.Fatal("should have remote info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlan_refresh(t *testing.T) {
|
func TestPlan_refresh(t *testing.T) {
|
||||||
p := testProvider()
|
p := testProvider()
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
@ -451,7 +575,7 @@ func TestPlan_validate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := ui.ErrorWriter.String()
|
actual := ui.ErrorWriter.String()
|
||||||
if !strings.Contains(actual, "can't reference") {
|
if !strings.Contains(actual, "cannot be computed") {
|
||||||
t.Fatalf("bad: %s", actual)
|
t.Fatalf("bad: %s", actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/atlas-go/archive"
|
"github.com/hashicorp/atlas-go/archive"
|
||||||
"github.com/hashicorp/atlas-go/v1"
|
"github.com/hashicorp/atlas-go/v1"
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,26 +64,14 @@ func (c *PushCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pwd is used for the configuration path if one is not given
|
|
||||||
pwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the path to the configuration depending on the args.
|
// Get the path to the configuration depending on the args.
|
||||||
var configPath string
|
configPath, err := ModulePath(cmdFlags.Args())
|
||||||
args = cmdFlags.Args()
|
if err != nil {
|
||||||
if len(args) > 1 {
|
c.Ui.Error(err.Error())
|
||||||
c.Ui.Error("The apply command expects at most one argument.")
|
|
||||||
cmdFlags.Usage()
|
|
||||||
return 1
|
return 1
|
||||||
} else if len(args) == 1 {
|
|
||||||
configPath = args[0]
|
|
||||||
} else {
|
|
||||||
configPath = pwd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// Verify the state is remote, we can't push without a remote state
|
// Verify the state is remote, we can't push without a remote state
|
||||||
s, err := c.State()
|
s, err := c.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -98,24 +87,63 @@ func (c *PushCommand) Run(args []string) int {
|
||||||
"command.")
|
"command.")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
// Check if the path is a plan
|
||||||
ctx, planned, err := c.Context(contextOpts{
|
plan, err := c.Plan(configPath)
|
||||||
Path: configPath,
|
|
||||||
StatePath: c.Meta.statePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if planned {
|
if plan != nil {
|
||||||
c.Ui.Error(
|
c.Ui.Error(
|
||||||
"A plan file cannot be given as the path to the configuration.\n" +
|
"A plan file cannot be given as the path to the configuration.\n" +
|
||||||
"A path to a module (directory with configuration) must be given.")
|
"A path to a module (directory with configuration) must be given.")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the module
|
||||||
|
mod, err := c.Module(configPath)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if mod == nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"No configuration files found in the directory: %s\n\n"+
|
||||||
|
"This command requires configuration to run.",
|
||||||
|
configPath))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the backend
|
||||||
|
b, err := c.Backend(&BackendOpts{
|
||||||
|
ConfigPath: configPath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// We require a local backend
|
||||||
|
local, ok := b.(backend.Local)
|
||||||
|
if !ok {
|
||||||
|
c.Ui.Error(ErrUnsupportedLocalOp)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the operation
|
||||||
|
opReq := c.Operation()
|
||||||
|
opReq.Module = mod
|
||||||
|
opReq.Plan = plan
|
||||||
|
|
||||||
|
// Get the context
|
||||||
|
ctx, _, err := local.Context(opReq)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
// Get the configuration
|
// Get the configuration
|
||||||
config := ctx.Module().Config()
|
config := ctx.Module().Config()
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,111 +28,50 @@ func (c *RefreshCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPath string
|
configPath, err := ModulePath(cmdFlags.Args())
|
||||||
args = cmdFlags.Args()
|
|
||||||
if len(args) > 1 {
|
|
||||||
c.Ui.Error("The refresh command expects at most one argument.")
|
|
||||||
cmdFlags.Usage()
|
|
||||||
return 1
|
|
||||||
} else if len(args) == 1 {
|
|
||||||
configPath = args[0]
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
configPath, err = os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if remote state is enabled
|
|
||||||
state, err := c.State()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the state path exists. The "ContextArg" function below
|
|
||||||
// will actually do this, but we want to provide a richer error message
|
|
||||||
// if possible.
|
|
||||||
if !state.State().IsRemote() {
|
|
||||||
if _, err := os.Stat(c.Meta.statePath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"The Terraform state file for your infrastructure does not\n"+
|
|
||||||
"exist. The 'refresh' command only works and only makes sense\n"+
|
|
||||||
"when there is existing state that Terraform is managing. Please\n"+
|
|
||||||
"double-check the value given below and try again. If you\n"+
|
|
||||||
"haven't created infrastructure with Terraform yet, use the\n"+
|
|
||||||
"'terraform apply' command.\n\n"+
|
|
||||||
"Path: %s",
|
|
||||||
c.Meta.statePath))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"There was an error reading the Terraform state that is needed\n"+
|
|
||||||
"for refreshing. The path and error are shown below.\n\n"+
|
|
||||||
"Path: %s\n\nError: %s",
|
|
||||||
c.Meta.statePath,
|
|
||||||
err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is going to keep track of shadow errors
|
|
||||||
var shadowErr error
|
|
||||||
|
|
||||||
// Build the context based on the arguments given
|
|
||||||
ctx, _, err := c.Context(contextOpts{
|
|
||||||
Path: configPath,
|
|
||||||
StatePath: c.Meta.statePath,
|
|
||||||
Parallelism: c.Meta.parallelism,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Input(c.InputMode()); err != nil {
|
// Load the module
|
||||||
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
|
mod, err := c.Module(configPath)
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record any shadow errors for later
|
|
||||||
if err := ctx.ShadowError(); err != nil {
|
|
||||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
|
||||||
err, "input operation:"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validateContext(ctx, c.Ui) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
newState, err := ctx.Refresh()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
// Load the backend
|
||||||
if err := c.Meta.PersistState(newState); err != nil {
|
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputs := outputsAsString(newState, terraform.RootModulePath, ctx.Module().Config().Outputs, true); outputs != "" {
|
// Build the operation
|
||||||
|
opReq := c.Operation()
|
||||||
|
opReq.Type = backend.OperationTypeRefresh
|
||||||
|
opReq.Module = mod
|
||||||
|
|
||||||
|
// Perform the operation
|
||||||
|
op, err := b.Operation(context.Background(), opReq)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the operation to complete
|
||||||
|
<-op.Done()
|
||||||
|
if err := op.Err; err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the outputs
|
||||||
|
if outputs := outputsAsString(op.State, terraform.RootModulePath, nil, true); outputs != "" {
|
||||||
c.Ui.Output(c.Colorize().Color(outputs))
|
c.Ui.Output(c.Colorize().Color(outputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record any shadow errors for later
|
|
||||||
if err := ctx.ShadowError(); err != nil {
|
|
||||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
|
||||||
err, "refresh operation:"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have an error in the shadow graph, let the user know.
|
|
||||||
c.outputShadowError(shadowErr, true)
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -712,6 +712,10 @@ func TestRefresh_disableBackup(t *testing.T) {
|
||||||
if err == nil || !os.IsNotExist(err) {
|
if err == nil || !os.IsNotExist(err) {
|
||||||
t.Fatalf("backup should not exist")
|
t.Fatalf("backup should not exist")
|
||||||
}
|
}
|
||||||
|
_, err = os.Stat("-")
|
||||||
|
if err == nil || !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("backup should not exist")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefresh_displaysOutputs(t *testing.T) {
|
func TestRefresh_displaysOutputs(t *testing.T) {
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemoteCommand struct {
|
|
||||||
Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemoteCommand) Run(argsRaw []string) int {
|
|
||||||
// Duplicate the args so we can munge them without affecting
|
|
||||||
// future subcommand invocations which will do the same.
|
|
||||||
args := make([]string, len(argsRaw))
|
|
||||||
copy(args, argsRaw)
|
|
||||||
args = c.Meta.process(args, false)
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
c.Ui.Error(c.Help())
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args[0] {
|
|
||||||
case "config":
|
|
||||||
cmd := &RemoteConfigCommand{Meta: c.Meta}
|
|
||||||
return cmd.Run(args[1:])
|
|
||||||
case "pull":
|
|
||||||
cmd := &RemotePullCommand{Meta: c.Meta}
|
|
||||||
return cmd.Run(args[1:])
|
|
||||||
case "push":
|
|
||||||
cmd := &RemotePushCommand{Meta: c.Meta}
|
|
||||||
return cmd.Run(args[1:])
|
|
||||||
default:
|
|
||||||
c.Ui.Error(c.Help())
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemoteCommand) Help() string {
|
|
||||||
helpText := `
|
|
||||||
Usage: terraform remote <subcommand> [options]
|
|
||||||
|
|
||||||
Configure remote state storage with Terraform.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
|
||||||
|
|
||||||
Available subcommands:
|
|
||||||
|
|
||||||
config Configure the remote storage settings.
|
|
||||||
pull Sync the remote storage by downloading to local storage.
|
|
||||||
push Sync the remote storage by uploading the local storage.
|
|
||||||
|
|
||||||
`
|
|
||||||
return strings.TrimSpace(helpText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemoteCommand) Synopsis() string {
|
|
||||||
return "Configure remote state storage"
|
|
||||||
}
|
|
|
@ -1,385 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/state"
|
|
||||||
"github.com/hashicorp/terraform/state/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
// remoteCommandConfig is used to encapsulate our configuration
|
|
||||||
type remoteCommandConfig struct {
|
|
||||||
disableRemote bool
|
|
||||||
pullOnDisable bool
|
|
||||||
|
|
||||||
statePath string
|
|
||||||
backupPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteConfigCommand is a Command implementation that is used to
|
|
||||||
// enable and disable remote state management
|
|
||||||
type RemoteConfigCommand struct {
|
|
||||||
Meta
|
|
||||||
conf remoteCommandConfig
|
|
||||||
remoteConf *terraform.RemoteState
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemoteConfigCommand) Run(args []string) int {
|
|
||||||
// we expect a zero struct value here, but it's not explicitly set in tests
|
|
||||||
if c.remoteConf == nil {
|
|
||||||
c.remoteConf = &terraform.RemoteState{}
|
|
||||||
}
|
|
||||||
|
|
||||||
args = c.Meta.process(args, false)
|
|
||||||
config := make(map[string]string)
|
|
||||||
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
|
|
||||||
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
|
|
||||||
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
|
|
||||||
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
|
|
||||||
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
|
|
||||||
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
|
|
||||||
cmdFlags.Var((*FlagStringKV)(&config), "backend-config", "config")
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("\nError parsing CLI flags: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lowercase the type
|
|
||||||
c.remoteConf.Type = strings.ToLower(c.remoteConf.Type)
|
|
||||||
|
|
||||||
// Set the local state path
|
|
||||||
c.statePath = c.conf.statePath
|
|
||||||
|
|
||||||
// Populate the various configurations
|
|
||||||
c.remoteConf.Config = config
|
|
||||||
|
|
||||||
// Get the state information. We specifically request the cache only
|
|
||||||
// for the remote state here because it is possible the remote state
|
|
||||||
// is invalid and we don't want to error.
|
|
||||||
stateOpts := c.StateOpts()
|
|
||||||
stateOpts.RemoteCacheOnly = true
|
|
||||||
if _, err := c.StateRaw(stateOpts); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error loading local state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the local and remote [cached] state
|
|
||||||
localState := c.stateResult.Local.State()
|
|
||||||
var remoteState *terraform.State
|
|
||||||
if remote := c.stateResult.Remote; remote != nil {
|
|
||||||
remoteState = remote.State()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if remote state is being disabled
|
|
||||||
if c.conf.disableRemote {
|
|
||||||
if !remoteState.IsRemote() {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if !localState.Empty() {
|
|
||||||
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
|
|
||||||
c.conf.statePath))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.disableRemoteState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure there is no conflict, and then do the correct operation
|
|
||||||
var result int
|
|
||||||
haveCache := !remoteState.Empty()
|
|
||||||
haveLocal := !localState.Empty()
|
|
||||||
switch {
|
|
||||||
case haveCache && haveLocal:
|
|
||||||
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
|
|
||||||
c.conf.statePath))
|
|
||||||
result = 1
|
|
||||||
|
|
||||||
case !haveCache && !haveLocal:
|
|
||||||
// If we don't have either state file, initialize a blank state file
|
|
||||||
result = c.initBlankState()
|
|
||||||
|
|
||||||
case haveCache && !haveLocal:
|
|
||||||
// Update the remote state target potentially
|
|
||||||
result = c.updateRemoteConfig()
|
|
||||||
|
|
||||||
case !haveCache && haveLocal:
|
|
||||||
// Enable remote state management
|
|
||||||
result = c.enableRemoteState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there was an error, return right away
|
|
||||||
if result != 0 {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not pulling, then do nothing
|
|
||||||
if !c.conf.pullOnDisable {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, refresh the state
|
|
||||||
stateResult, err := c.StateRaw(c.StateOpts())
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Error while performing the initial pull. The error message is shown\n"+
|
|
||||||
"below. Note that remote state was properly configured, so you don't\n"+
|
|
||||||
"need to reconfigure. You can now use `push` and `pull` directly.\n"+
|
|
||||||
"\n%s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
state := stateResult.State
|
|
||||||
if err := state.RefreshState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Error while performing the initial pull. The error message is shown\n"+
|
|
||||||
"below. Note that remote state was properly configured, so you don't\n"+
|
|
||||||
"need to reconfigure. You can now use `push` and `pull` directly.\n"+
|
|
||||||
"\n%s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset][bold][green]Remote state configured and pulled.")))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// disableRemoteState is used to disable remote state management,
|
|
||||||
// and move the state file into place.
|
|
||||||
func (c *RemoteConfigCommand) disableRemoteState() int {
|
|
||||||
if c.stateResult == nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Internal error. State() must be called internally before remote\n" +
|
|
||||||
"state can be disabled. Please report this as a bug."))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if !c.stateResult.State.State().IsRemote() {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Remote state is not enabled. Can't disable remote state."))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
local := c.stateResult.Local
|
|
||||||
remote := c.stateResult.Remote
|
|
||||||
|
|
||||||
// Ensure we have the latest state before disabling
|
|
||||||
if c.conf.pullOnDisable {
|
|
||||||
log.Printf("[INFO] Refreshing local state from remote server")
|
|
||||||
if err := remote.RefreshState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Failed to refresh from remote state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit if we were unable to update
|
|
||||||
if change := remote.RefreshResult(); !change.SuccessfulPull() {
|
|
||||||
c.Ui.Error(fmt.Sprintf("%s", change))
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
log.Printf("[INFO] %s", change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the remote management, and copy into place
|
|
||||||
newState := remote.State()
|
|
||||||
newState.Remote = nil
|
|
||||||
if err := local.WriteState(newState); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
|
|
||||||
c.conf.statePath, err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if err := local.PersistState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
|
|
||||||
c.conf.statePath, err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the old state file
|
|
||||||
if err := os.Remove(c.stateResult.RemotePath); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateRemoteConfig is used to verify that the remote configuration
|
|
||||||
// we have is valid
|
|
||||||
func (c *RemoteConfigCommand) validateRemoteConfig() error {
|
|
||||||
conf := c.remoteConf
|
|
||||||
_, err := remote.NewClient(conf.Type, conf.Config)
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"%s\n\n"+
|
|
||||||
"If the error message above mentions requiring or modifying configuration\n"+
|
|
||||||
"options, these are set using the `-backend-config` flag. Example:\n"+
|
|
||||||
"-backend-config=\"name=foo\" to set the `name` configuration",
|
|
||||||
err))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// initBlank state is used to initialize a blank state that is
|
|
||||||
// remote enabled
|
|
||||||
func (c *RemoteConfigCommand) initBlankState() int {
|
|
||||||
// Validate the remote configuration
|
|
||||||
if err := c.validateRemoteConfig(); err != nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a blank state, attach the remote configuration
|
|
||||||
blank := terraform.NewState()
|
|
||||||
blank.Remote = c.remoteConf
|
|
||||||
|
|
||||||
// Persist the state
|
|
||||||
remote := &state.LocalState{Path: c.stateResult.RemotePath}
|
|
||||||
if err := remote.WriteState(blank); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if err := remote.PersistState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
c.Ui.Output("Initialized blank state with remote state enabled!")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateRemoteConfig is used to update the configuration of the
|
|
||||||
// remote state store
|
|
||||||
func (c *RemoteConfigCommand) updateRemoteConfig() int {
|
|
||||||
// Validate the remote configuration
|
|
||||||
if err := c.validateRemoteConfig(); err != nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read in the local state, which is just the cache of the remote state
|
|
||||||
remote := c.stateResult.Remote.Cache
|
|
||||||
|
|
||||||
// Update the configuration
|
|
||||||
state := remote.State()
|
|
||||||
state.Remote = c.remoteConf
|
|
||||||
if err := remote.WriteState(state); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if err := remote.PersistState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
c.Ui.Output("Remote configuration updated")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// enableRemoteState is used to enable remote state management
|
|
||||||
// and to move a state file into place
|
|
||||||
func (c *RemoteConfigCommand) enableRemoteState() int {
|
|
||||||
// Validate the remote configuration
|
|
||||||
if err := c.validateRemoteConfig(); err != nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the local state
|
|
||||||
local := c.stateResult.Local
|
|
||||||
if err := local.RefreshState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup the state file before we modify it
|
|
||||||
backupPath := c.conf.backupPath
|
|
||||||
if backupPath != "-" {
|
|
||||||
// Provide default backup path if none provided
|
|
||||||
if backupPath == "" {
|
|
||||||
backupPath = c.conf.statePath + DefaultBackupExtension
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[INFO] Writing backup state to: %s", backupPath)
|
|
||||||
backup := &state.LocalState{Path: backupPath}
|
|
||||||
if err := backup.WriteState(local.State()); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if err := backup.PersistState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the local configuration, move into place
|
|
||||||
state := local.State()
|
|
||||||
state.Remote = c.remoteConf
|
|
||||||
remote := c.stateResult.Remote
|
|
||||||
if err := remote.WriteState(state); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if err := remote.PersistState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the original, local state file
|
|
||||||
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
|
|
||||||
if err := os.Remove(c.conf.statePath); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
|
|
||||||
c.conf.statePath, err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
c.Ui.Output("Remote state management enabled")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemoteConfigCommand) Help() string {
|
|
||||||
helpText := `
|
|
||||||
Usage: terraform remote config [options]
|
|
||||||
|
|
||||||
Configures Terraform to use a remote state server. This allows state
|
|
||||||
to be pulled down when necessary and then pushed to the server when
|
|
||||||
updated. In this mode, the state file does not need to be stored durably
|
|
||||||
since the remote server provides the durability.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-backend=Atlas Specifies the type of remote backend. Must be one
|
|
||||||
of Atlas, Consul, Etcd, GCS, HTTP, MAS, S3, or Swift.
|
|
||||||
Defaults to Atlas.
|
|
||||||
|
|
||||||
-backend-config="k=v" Specifies configuration for the remote storage
|
|
||||||
backend. This can be specified multiple times.
|
|
||||||
|
|
||||||
-backup=path Path to backup the existing state file before
|
|
||||||
modifying. Defaults to the "-state" path with
|
|
||||||
".backup" extension. Set to "-" to disable backup.
|
|
||||||
|
|
||||||
-disable Disables remote state management and migrates the state
|
|
||||||
to the -state path.
|
|
||||||
|
|
||||||
-pull=true If disabling, this controls if the remote state is
|
|
||||||
pulled before disabling. If enabling, this controls
|
|
||||||
if the remote state is pulled after enabling. This
|
|
||||||
defaults to true.
|
|
||||||
|
|
||||||
-state=path Path to read state. Defaults to "terraform.tfstate"
|
|
||||||
unless remote state is enabled.
|
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
|
||||||
|
|
||||||
`
|
|
||||||
return strings.TrimSpace(helpText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemoteConfigCommand) Synopsis() string {
|
|
||||||
return "Configures remote state management"
|
|
||||||
}
|
|
|
@ -1,449 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/state"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test disabling remote management
|
|
||||||
func TestRemoteConfig_disable(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
// Create remote state file, this should be pulled
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 10
|
|
||||||
conf, srv := testRemoteState(t, s, 200)
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
// Persist local remote state
|
|
||||||
s = terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
s.Remote = conf
|
|
||||||
|
|
||||||
// Write the state
|
|
||||||
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
||||||
state := &state.LocalState{Path: statePath}
|
|
||||||
if err := state.WriteState(s); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if err := state.PersistState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
args := []string{"-disable"}
|
|
||||||
if code := c.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local state file should be removed and the local cache should exist
|
|
||||||
testRemoteLocal(t, true)
|
|
||||||
testRemoteLocalCache(t, false)
|
|
||||||
|
|
||||||
// Check that the state file was updated
|
|
||||||
raw, _ := ioutil.ReadFile(DefaultStateFilename)
|
|
||||||
newState, err := terraform.ReadState(bytes.NewReader(raw))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we updated
|
|
||||||
if newState.Remote != nil {
|
|
||||||
t.Fatalf("remote configuration not removed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test disabling remote management without pulling
|
|
||||||
func TestRemoteConfig_disable_noPull(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
// Create remote state file, this should be pulled
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 10
|
|
||||||
conf, srv := testRemoteState(t, s, 200)
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
// Persist local remote state
|
|
||||||
s = terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
s.Remote = conf
|
|
||||||
|
|
||||||
// Write the state
|
|
||||||
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
||||||
state := &state.LocalState{Path: statePath}
|
|
||||||
if err := state.WriteState(s); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if err := state.PersistState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
args := []string{"-disable", "-pull=false"}
|
|
||||||
if code := c.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local state file should be removed and the local cache should exist
|
|
||||||
testRemoteLocal(t, true)
|
|
||||||
testRemoteLocalCache(t, false)
|
|
||||||
|
|
||||||
// Check that the state file was updated
|
|
||||||
raw, _ := ioutil.ReadFile(DefaultStateFilename)
|
|
||||||
newState, err := terraform.ReadState(bytes.NewReader(raw))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newState.Remote != nil {
|
|
||||||
t.Fatalf("remote configuration not removed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test disabling remote management when not enabled
|
|
||||||
func TestRemoteConfig_disable_notEnabled(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"-disable"}
|
|
||||||
if code := c.Run(args); code != 1 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test disabling remote management with a state file in the way
|
|
||||||
func TestRemoteConfig_disable_otherState(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
// Persist local remote state
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
|
|
||||||
// Write the state
|
|
||||||
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
||||||
state := &state.LocalState{Path: statePath}
|
|
||||||
if err := state.WriteState(s); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if err := state.PersistState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also put a file at the default path
|
|
||||||
fh, err := os.Create(DefaultStateFilename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
err = terraform.WriteState(s, fh)
|
|
||||||
fh.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"-disable"}
|
|
||||||
if code := c.Run(args); code != 1 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the case where both managed and non managed state present
|
|
||||||
func TestRemoteConfig_managedAndNonManaged(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
// Persist local remote state
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
|
|
||||||
// Write the state
|
|
||||||
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
||||||
state := &state.LocalState{Path: statePath}
|
|
||||||
if err := state.WriteState(s); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if err := state.PersistState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also put a file at the default path
|
|
||||||
fh, err := os.Create(DefaultStateFilename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
err = terraform.WriteState(s, fh)
|
|
||||||
fh.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{}
|
|
||||||
if code := c.Run(args); code != 1 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test initializing blank state
|
|
||||||
func TestRemoteConfig_initBlank(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"-backend=http",
|
|
||||||
"-backend-config", "address=http://example.com",
|
|
||||||
"-backend-config", "access_token=test",
|
|
||||||
"-pull=false",
|
|
||||||
}
|
|
||||||
if code := c.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
|
|
||||||
ls := &state.LocalState{Path: remotePath}
|
|
||||||
if err := ls.RefreshState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
local := ls.State()
|
|
||||||
if local.Remote.Type != "http" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
if local.Remote.Config["address"] != "http://example.com" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
if local.Remote.Config["access_token"] != "test" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test initializing without remote settings
|
|
||||||
func TestRemoteConfig_initBlank_missingRemote(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{}
|
|
||||||
if code := c.Run(args); code != 1 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test updating remote config
|
|
||||||
func TestRemoteConfig_updateRemote(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
// Persist local remote state
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
s.Remote = &terraform.RemoteState{
|
|
||||||
Type: "invalid",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the state
|
|
||||||
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
||||||
ls := &state.LocalState{Path: statePath}
|
|
||||||
if err := ls.WriteState(s); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if err := ls.PersistState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"-backend=http",
|
|
||||||
"-backend-config", "address=http://example.com",
|
|
||||||
"-backend-config", "access_token=test",
|
|
||||||
"-pull=false",
|
|
||||||
}
|
|
||||||
if code := c.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
|
|
||||||
ls = &state.LocalState{Path: remotePath}
|
|
||||||
if err := ls.RefreshState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
local := ls.State()
|
|
||||||
|
|
||||||
if local.Remote.Type != "http" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
if local.Remote.Config["address"] != "http://example.com" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
if local.Remote.Config["access_token"] != "test" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test enabling remote state
|
|
||||||
func TestRemoteConfig_enableRemote(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
// Create a non-remote enabled state
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
|
|
||||||
// Add the state at the default path
|
|
||||||
fh, err := os.Create(DefaultStateFilename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
err = terraform.WriteState(s, fh)
|
|
||||||
fh.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemoteConfigCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"-backend=http",
|
|
||||||
"-backend-config", "address=http://example.com",
|
|
||||||
"-backend-config", "access_token=test",
|
|
||||||
"-pull=false",
|
|
||||||
}
|
|
||||||
if code := c.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
|
|
||||||
ls := &state.LocalState{Path: remotePath}
|
|
||||||
if err := ls.RefreshState(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
local := ls.State()
|
|
||||||
|
|
||||||
if local.Remote.Type != "http" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
if local.Remote.Config["address"] != "http://example.com" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
if local.Remote.Config["access_token"] != "test" {
|
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup file should exist, state file should not
|
|
||||||
testRemoteLocal(t, false)
|
|
||||||
testRemoteLocalBackup(t, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRemoteLocal(t *testing.T, exists bool) {
|
|
||||||
_, err := os.Stat(DefaultStateFilename)
|
|
||||||
if os.IsNotExist(err) && !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == nil && exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("bad: %#v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRemoteLocalBackup(t *testing.T, exists bool) {
|
|
||||||
_, err := os.Stat(DefaultStateFilename + DefaultBackupExtension)
|
|
||||||
if os.IsNotExist(err) && !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == nil && exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == nil && !exists {
|
|
||||||
t.Fatal("expected local backup to exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("bad: %#v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRemoteLocalCache(t *testing.T, exists bool) {
|
|
||||||
_, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename))
|
|
||||||
if os.IsNotExist(err) && !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == nil && exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == nil && !exists {
|
|
||||||
t.Fatal("expected local cache to exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("bad: %#v", err)
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/state"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemotePullCommand struct {
|
|
||||||
Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemotePullCommand) Run(args []string) int {
|
|
||||||
args = c.Meta.process(args, false)
|
|
||||||
cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError)
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read out our state
|
|
||||||
s, err := c.State()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
localState := s.State()
|
|
||||||
|
|
||||||
// If remote state isn't enabled, it is a problem.
|
|
||||||
if !localState.IsRemote() {
|
|
||||||
c.Ui.Error("Remote state not enabled!")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need the CacheState structure in order to do anything
|
|
||||||
var cache *state.CacheState
|
|
||||||
if bs, ok := s.(*state.BackupState); ok {
|
|
||||||
if cs, ok := bs.Real.(*state.CacheState); ok {
|
|
||||||
cache = cs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cache == nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Failed to extract internal CacheState from remote state.\n" +
|
|
||||||
"This is an internal error, please report it as a bug."))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the state
|
|
||||||
if err := cache.RefreshState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Failed to refresh from remote state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use an error exit code if the update was not a success
|
|
||||||
change := cache.RefreshResult()
|
|
||||||
if !change.SuccessfulPull() {
|
|
||||||
c.Ui.Error(fmt.Sprintf("%s", change))
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
|
||||||
"[reset][bold][green]%s", change)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemotePullCommand) Help() string {
|
|
||||||
helpText := `
|
|
||||||
Usage: terraform pull [options]
|
|
||||||
|
|
||||||
Refreshes the cached state file from the remote server.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
|
||||||
`
|
|
||||||
return strings.TrimSpace(helpText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemotePullCommand) Synopsis() string {
|
|
||||||
return "Refreshes the local state copy from the remote server"
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRemotePull_noRemote(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemotePullCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{}
|
|
||||||
if code := c.Run(args); code != 1 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemotePull_local(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 10
|
|
||||||
conf, srv := testRemoteState(t, s, 200)
|
|
||||||
|
|
||||||
s = terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
s.Remote = conf
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
// Store the local state
|
|
||||||
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
f, err := os.Create(statePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
err = terraform.WriteState(s, f)
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemotePullCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
args := []string{}
|
|
||||||
if code := c.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testRemoteState is used to make a test HTTP server to
|
|
||||||
// return a given state file
|
|
||||||
func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) {
|
|
||||||
var b64md5 string
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
cb := func(resp http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.Method == "PUT" {
|
|
||||||
resp.WriteHeader(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s == nil {
|
|
||||||
resp.WriteHeader(404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Header().Set("Content-MD5", b64md5)
|
|
||||||
resp.Write(buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(cb))
|
|
||||||
remote := &terraform.RemoteState{
|
|
||||||
Type: "http",
|
|
||||||
Config: map[string]string{"address": srv.URL},
|
|
||||||
}
|
|
||||||
|
|
||||||
if s != nil {
|
|
||||||
// Set the remote data
|
|
||||||
s.Remote = remote
|
|
||||||
|
|
||||||
enc := json.NewEncoder(buf)
|
|
||||||
if err := enc.Encode(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
md5 := md5.Sum(buf.Bytes())
|
|
||||||
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
|
|
||||||
}
|
|
||||||
|
|
||||||
return remote, srv
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/state"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemotePushCommand struct {
|
|
||||||
Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemotePushCommand) Run(args []string) int {
|
|
||||||
var force bool
|
|
||||||
args = c.Meta.process(args, false)
|
|
||||||
cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError)
|
|
||||||
cmdFlags.BoolVar(&force, "force", false, "")
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read out our state
|
|
||||||
s, err := c.State()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
localState := s.State()
|
|
||||||
|
|
||||||
// If remote state isn't enabled, it is a problem.
|
|
||||||
if !localState.IsRemote() {
|
|
||||||
c.Ui.Error("Remote state not enabled!")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need the CacheState structure in order to do anything
|
|
||||||
var cache *state.CacheState
|
|
||||||
if bs, ok := s.(*state.BackupState); ok {
|
|
||||||
if cs, ok := bs.Real.(*state.CacheState); ok {
|
|
||||||
cache = cs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cache == nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Failed to extract internal CacheState from remote state.\n" +
|
|
||||||
"This is an internal error, please report it as a bug."))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the cache state
|
|
||||||
if err := cache.Cache.RefreshState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Failed to refresh from remote state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write it to the real storage
|
|
||||||
remote := cache.Durable
|
|
||||||
if err := remote.WriteState(cache.Cache.State()); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if err := remote.PersistState(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error saving state: %s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Ui.Output(c.Colorize().Color(
|
|
||||||
"[reset][bold][green]State successfully pushed!"))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemotePushCommand) Help() string {
|
|
||||||
helpText := `
|
|
||||||
Usage: terraform push [options]
|
|
||||||
|
|
||||||
Uploads the latest state to the remote server.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
-no-color If specified, output won't contain any color.
|
|
||||||
|
|
||||||
-force Forces the upload of the local state, ignoring any
|
|
||||||
conflicts. This should be used carefully, as force pushing
|
|
||||||
can cause remote state information to be lost.
|
|
||||||
|
|
||||||
`
|
|
||||||
return strings.TrimSpace(helpText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RemotePushCommand) Synopsis() string {
|
|
||||||
return "Uploads the local state to the remote server"
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRemotePush_noRemote(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemotePushCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{}
|
|
||||||
if code := c.Run(args); code != 1 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemotePush_local(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 5
|
|
||||||
conf, srv := testRemoteState(t, s, 200)
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
s = terraform.NewState()
|
|
||||||
s.Serial = 10
|
|
||||||
s.Remote = conf
|
|
||||||
|
|
||||||
// Store the local state
|
|
||||||
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
f, err := os.Create(statePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
err = terraform.WriteState(s, f)
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
|
||||||
c := &RemotePushCommand{
|
|
||||||
Meta: Meta{
|
|
||||||
ContextOpts: testCtxConfig(testProvider()),
|
|
||||||
Ui: ui,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
args := []string{}
|
|
||||||
if code := c.Run(args); code != 0 {
|
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/command/format"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,14 +67,26 @@ func (c *ShowCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stateOpts := c.StateOpts()
|
// Load the backend
|
||||||
stateOpts.RemoteCacheOnly = true
|
b, err := c.Backend(nil)
|
||||||
result, err := State(stateOpts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
state = result.State.State()
|
|
||||||
|
// Get the state
|
||||||
|
stateStore, err := b.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stateStore.RefreshState(); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
state = stateStore.State()
|
||||||
if state == nil {
|
if state == nil {
|
||||||
c.Ui.Output("No state.")
|
c.Ui.Output("No state.")
|
||||||
return 0
|
return 0
|
||||||
|
@ -92,7 +105,7 @@ func (c *ShowCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if plan != nil {
|
if plan != nil {
|
||||||
c.Ui.Output(FormatPlan(&FormatPlanOpts{
|
c.Ui.Output(format.Plan(&format.PlanOpts{
|
||||||
Plan: plan,
|
Plan: plan,
|
||||||
Color: c.Colorize(),
|
Color: c.Colorize(),
|
||||||
ModuleDepth: moduleDepth,
|
ModuleDepth: moduleDepth,
|
||||||
|
@ -100,7 +113,7 @@ func (c *ShowCommand) Run(args []string) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Ui.Output(FormatState(&FormatStateOpts{
|
c.Ui.Output(format.State(&format.StateOpts{
|
||||||
State: state,
|
State: state,
|
||||||
Color: c.Colorize(),
|
Color: c.Colorize(),
|
||||||
ModuleDepth: moduleDepth,
|
ModuleDepth: moduleDepth,
|
||||||
|
|
|
@ -129,20 +129,11 @@ func TestShow_noArgsRemoteState(t *testing.T) {
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
// Pretend like we have a local cache of remote state
|
// Create some legacy remote state
|
||||||
remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
legacyState := testState()
|
||||||
if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil {
|
_, srv := testRemoteState(t, legacyState, 200)
|
||||||
t.Fatalf("err: %s", err)
|
defer srv.Close()
|
||||||
}
|
testStateFileRemote(t, legacyState)
|
||||||
f, err := os.Create(remoteStatePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
err = terraform.WriteState(testState(), f)
|
|
||||||
f.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ShowCommand{
|
c := &ShowCommand{
|
||||||
|
|
|
@ -24,10 +24,18 @@ func (c *StateListCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
args = cmdFlags.Args()
|
args = cmdFlags.Args()
|
||||||
|
|
||||||
state, err := c.State()
|
// Load the backend
|
||||||
|
b, err := c.Backend(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
return cli.RunResultHelp
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
state, err := b.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
stateReal := state.State()
|
stateReal := state.State()
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
backendlocal "github.com/hashicorp/terraform/backend/local"
|
||||||
"github.com/hashicorp/terraform/state"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
@ -19,17 +20,31 @@ func (c *StateMeta) State(m *Meta) (state.State, error) {
|
||||||
// Disable backups since we wrap it manually below
|
// Disable backups since we wrap it manually below
|
||||||
m.backupPath = "-"
|
m.backupPath = "-"
|
||||||
|
|
||||||
// Get the state (shouldn't be wrapped in a backup)
|
// Load the backend
|
||||||
s, err := m.State()
|
b, err := m.Backend(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
s, err := b.State()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a local backend
|
||||||
|
localRaw, err := m.Backend(&BackendOpts{ForceLocal: true})
|
||||||
|
if err != nil {
|
||||||
|
// This should never fail
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
localB := localRaw.(*backendlocal.Local)
|
||||||
|
|
||||||
// Determine the backup path. stateOutPath is set to the resulting
|
// Determine the backup path. stateOutPath is set to the resulting
|
||||||
// file where state is written (cached in the case of remote state)
|
// file where state is written (cached in the case of remote state)
|
||||||
backupPath := fmt.Sprintf(
|
backupPath := fmt.Sprintf(
|
||||||
"%s.%d%s",
|
"%s.%d%s",
|
||||||
m.stateOutPath,
|
localB.StateOutPath,
|
||||||
time.Now().UTC().Unix(),
|
time.Now().UTC().Unix(),
|
||||||
DefaultBackupExtension)
|
DefaultBackupExtension)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatePullCommand is a Command implementation that shows a single resource.
|
||||||
|
type StatePullCommand struct {
|
||||||
|
Meta
|
||||||
|
StateMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatePullCommand) Run(args []string) int {
|
||||||
|
args = c.Meta.process(args, true)
|
||||||
|
|
||||||
|
cmdFlags := c.Meta.flagSet("state pull")
|
||||||
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
args = cmdFlags.Args()
|
||||||
|
|
||||||
|
// Load the backend
|
||||||
|
b, err := c.Backend(nil)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
state, err := b.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := state.RefreshState(); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := terraform.WriteState(state.State(), &buf); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Ui.Output(buf.String())
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatePullCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: terraform state pull [options]
|
||||||
|
|
||||||
|
Pull the state from its location and output it to stdout.
|
||||||
|
|
||||||
|
This command "pulls" the current state and outputs it to stdout.
|
||||||
|
The primary use of this is for state stored remotely. This command
|
||||||
|
will still work with local state but is less useful for this.
|
||||||
|
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatePullCommand) Synopsis() string {
|
||||||
|
return "Pull current state and output to stdout"
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatePull(t *testing.T) {
|
||||||
|
tmp, cwd := testCwd(t)
|
||||||
|
defer testFixCwd(t, tmp, cwd)
|
||||||
|
|
||||||
|
// Create some legacy remote state
|
||||||
|
legacyState := testState()
|
||||||
|
_, srv := testRemoteState(t, legacyState, 200)
|
||||||
|
defer srv.Close()
|
||||||
|
testStateFileRemote(t, legacyState)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StatePullCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "test_instance.foo"
|
||||||
|
actual := ui.OutputWriter.String()
|
||||||
|
if !strings.Contains(actual, expected) {
|
||||||
|
t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatePushCommand is a Command implementation that shows a single resource.
|
||||||
|
type StatePushCommand struct {
|
||||||
|
Meta
|
||||||
|
StateMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatePushCommand) Run(args []string) int {
|
||||||
|
args = c.Meta.process(args, true)
|
||||||
|
|
||||||
|
var flagForce bool
|
||||||
|
cmdFlags := c.Meta.flagSet("state push")
|
||||||
|
cmdFlags.BoolVar(&flagForce, "force", false, "")
|
||||||
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
args = cmdFlags.Args()
|
||||||
|
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.Ui.Error("Exactly one argument expected: path to state to push")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the state
|
||||||
|
f, err := os.Open(args[0])
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
sourceState, err := terraform.ReadState(f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error reading source state %q: %s", args[0], err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the backend
|
||||||
|
b, err := c.Backend(nil)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
state, err := b.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := state.RefreshState(); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
dstState := state.State()
|
||||||
|
|
||||||
|
// If we're not forcing, then perform safety checks
|
||||||
|
if !flagForce && !dstState.Empty() {
|
||||||
|
if !dstState.SameLineage(sourceState) {
|
||||||
|
c.Ui.Error(strings.TrimSpace(errStatePushLineage))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
age, err := dstState.CompareAges(sourceState)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(err.Error())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if age == terraform.StateAgeReceiverNewer {
|
||||||
|
c.Ui.Error(strings.TrimSpace(errStatePushSerialNewer))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite it
|
||||||
|
if err := state.WriteState(sourceState); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := state.PersistState(); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatePushCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: terraform state push [options] PATH
|
||||||
|
|
||||||
|
Update remote state from a local state file at PATH.
|
||||||
|
|
||||||
|
This command "pushes" a local state and overwrites remote state
|
||||||
|
with a local state file. The command will protect you against writing
|
||||||
|
an older serial or a different state file lineage unless you specify the
|
||||||
|
"-force" flag.
|
||||||
|
|
||||||
|
This command works with local state (it will overwrite the local
|
||||||
|
state), but is less useful for this use case.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-force Write the state even if lineages don't match or the
|
||||||
|
remote serial is higher.
|
||||||
|
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatePushCommand) Synopsis() string {
|
||||||
|
return "Update remote state from a local state file"
|
||||||
|
}
|
||||||
|
|
||||||
|
const errStatePushLineage = `
|
||||||
|
The lineages do not match! The state will not be pushed.
|
||||||
|
|
||||||
|
The "lineage" is a unique identifier given to a state on creation. It helps
|
||||||
|
protect Terraform from overwriting a seemingly unrelated state file since it
|
||||||
|
represents potentially losing real state.
|
||||||
|
|
||||||
|
Please verify you're pushing the correct state. If you're sure you are, you
|
||||||
|
can force the behavior with the "-force" flag.
|
||||||
|
`
|
||||||
|
|
||||||
|
const errStatePushSerialNewer = `
|
||||||
|
The destination state has a higher serial number! The state will not be pushed.
|
||||||
|
|
||||||
|
A higher serial could indicate that there is data in the destination state
|
||||||
|
that was not present when the source state was created. As a protection measure,
|
||||||
|
Terraform will not automatically overwrite this state.
|
||||||
|
|
||||||
|
Please verify you're pushing the correct state. If you're sure you are, you
|
||||||
|
can force the behavior with the "-force" flag.
|
||||||
|
`
|
|
@ -0,0 +1,154 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/copy"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatePush_empty(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("state-push-good"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
expected := testStateRead(t, "replace.tfstate")
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StatePushCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"replace.tfstate"}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := testStateRead(t, "local-state.tfstate")
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatePush_replaceMatch(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("state-push-replace-match"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
expected := testStateRead(t, "replace.tfstate")
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StatePushCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"replace.tfstate"}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := testStateRead(t, "local-state.tfstate")
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatePush_lineageMismatch(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("state-push-bad-lineage"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
expected := testStateRead(t, "local-state.tfstate")
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StatePushCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"replace.tfstate"}
|
||||||
|
if code := c.Run(args); code != 1 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := testStateRead(t, "local-state.tfstate")
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatePush_serialNewer(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("state-push-serial-newer"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
expected := testStateRead(t, "local-state.tfstate")
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StatePushCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"replace.tfstate"}
|
||||||
|
if code := c.Run(args); code != 1 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := testStateRead(t, "local-state.tfstate")
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatePush_serialOlder(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("state-push-serial-older"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
expected := testStateRead(t, "replace.tfstate")
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
c := &StatePushCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
ContextOpts: testCtxConfig(p),
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"replace.tfstate"}
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := testStateRead(t, "local-state.tfstate")
|
||||||
|
if !actual.Equal(expected) {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,10 +26,18 @@ func (c *StateShowCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
args = cmdFlags.Args()
|
args = cmdFlags.Args()
|
||||||
|
|
||||||
state, err := c.Meta.State()
|
// Load the backend
|
||||||
|
b, err := c.Backend(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
return cli.RunResultHelp
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
state, err := b.State()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
stateReal := state.State()
|
stateReal := state.State()
|
||||||
|
|
|
@ -56,8 +56,15 @@ func (c *TaintCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the state that we'll be modifying
|
// Load the backend
|
||||||
state, err := c.State()
|
b, err := c.Backend(nil)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the state
|
||||||
|
state, err := b.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -122,7 +129,11 @@ func (c *TaintCommand) Run(args []string) int {
|
||||||
rs.Taint()
|
rs.Taint()
|
||||||
|
|
||||||
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
||||||
if err := c.Meta.PersistState(s); err != nil {
|
if err := state.WriteState(s); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := state.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state-2.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state-old.tfstate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "legacy"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "configured"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state-2.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"remote": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state-old.tfstate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-new-legacy"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 8,
|
||||||
|
"lineage": "remote",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 8,
|
||||||
|
"lineage": "local",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 8,
|
||||||
|
"lineage": "backend-new-migrate",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
This directory is empty on purpose.
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
This directory has no configuration on purpose.
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "different"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"remote": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
No configs on purpose
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 10,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Hello
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state-old.tfstate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-unchanged-with-legacy"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "configured"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "configuredUnchanged"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state-old.tfstate"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "legacy"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty, we're unsetting
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "configuredUnset"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty, unset!
|
|
@ -0,0 +1 @@
|
||||||
|
path = "hello"
|
|
@ -0,0 +1,3 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "foo"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty
|
|
@ -0,0 +1,3 @@
|
||||||
|
module "foo" {
|
||||||
|
source = "./foo"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
ami = "bar"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
terraform {
|
||||||
|
backend "http" {
|
||||||
|
test = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_instance" "foo" {
|
||||||
|
ami = "bar"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 1,
|
||||||
|
"lineage": "mismatch"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 2,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
terraform {
|
||||||
|
backend "local" {
|
||||||
|
path = "local-state.tfstate"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "hello"
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue