command: convert to use backends
This commit is contained in:
parent
9654387771
commit
ad7b063262
255
command/apply.go
255
command/apply.go
|
@ -2,15 +2,16 @@ package command
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/helper/experiment"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -43,7 +44,7 @@ func (c *ApplyCommand) Run(args []string) int {
|
|||
cmdFlags.BoolVar(&refresh, "refresh", true, "refresh")
|
||||
cmdFlags.IntVar(
|
||||
&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.backupPath, "backup", "", "path")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
|
@ -51,32 +52,25 @@ func (c *ApplyCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var configPath string
|
||||
maybeInit := true
|
||||
// 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()
|
||||
if len(args) > 1 {
|
||||
c.Ui.Error("The apply command expects at most one argument.")
|
||||
cmdFlags.Usage()
|
||||
maybeInit := len(args) == 1
|
||||
configPath, err := ModulePath(args)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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 {
|
||||
// We need the pwd for the getter operation below
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Do a detect to determine if we need to do an init + apply.
|
||||
if detected, err := getter.Detect(configPath, pwd, getter.Detectors); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
|
@ -96,37 +90,58 @@ func (c *ApplyCommand) Run(args []string) int {
|
|||
}
|
||||
}
|
||||
|
||||
terraform.SetDebugInfo(DefaultDataDir)
|
||||
|
||||
// Check for the legacy graph
|
||||
if experiment.Enabled(experiment.X_legacyGraph) {
|
||||
c.Ui.Output(c.Colorize().Color(
|
||||
"[reset][bold][yellow]" +
|
||||
"Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" +
|
||||
"to execute this operation. This will be removed in the future so\n" +
|
||||
"please report any issues causing you to use this to the Terraform\n" +
|
||||
"project.\n\n"))
|
||||
}
|
||||
|
||||
// This is going to keep track of shadow errors
|
||||
var shadowErr error
|
||||
|
||||
// Build the context based on the arguments given
|
||||
ctx, planned, err := c.Context(contextOpts{
|
||||
Destroy: c.Destroy,
|
||||
Path: configPath,
|
||||
StatePath: c.Meta.statePath,
|
||||
Parallelism: c.Meta.parallelism,
|
||||
})
|
||||
// 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 && planned {
|
||||
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)
|
||||
|
||||
// Check for the legacy graph
|
||||
if experiment.Enabled(experiment.X_legacyGraph) {
|
||||
c.Ui.Output(c.Colorize().Color(
|
||||
"[reset][bold][yellow]" +
|
||||
"Legacy graph enabled! This will use the graph from Terraform 0.7.x\n" +
|
||||
"to execute this operation. This will be removed in the future so\n" +
|
||||
"please report any issues causing you to use this to the Terraform\n" +
|
||||
"project.\n\n"))
|
||||
}
|
||||
*/
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// If we're not forcing and we're destroying, verify with the
|
||||
// user at this point.
|
||||
if !destroyForce && c.Destroy {
|
||||
// Default destroy message
|
||||
desc := "Terraform will delete all your managed infrastructure.\n" +
|
||||
|
@ -159,80 +174,32 @@ func (c *ApplyCommand) Run(args []string) int {
|
|||
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
|
||||
if err := ctx.ShadowError(); err != nil {
|
||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
||||
err, "input operation:"))
|
||||
}
|
||||
}
|
||||
if !validateContext(ctx, c.Ui) {
|
||||
// Build the operation
|
||||
opReq := c.Operation()
|
||||
opReq.Destroy = c.Destroy
|
||||
opReq.Module = mod
|
||||
opReq.Plan = plan
|
||||
opReq.PlanRefresh = refresh
|
||||
opReq.Type = backend.OperationTypeApply
|
||||
|
||||
// Perform the operation
|
||||
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||
defer ctxCancel()
|
||||
op, err := b.Operation(ctx, opReq)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error starting operation: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Plan if we haven't already
|
||||
if !planned {
|
||||
if refresh {
|
||||
if _, err := ctx.Refresh(); err != nil {
|
||||
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 {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error reading state: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
stateHook.State = state
|
||||
}
|
||||
|
||||
// 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
|
||||
// Wait for the operation to complete or an interrupt to occur
|
||||
select {
|
||||
case <-c.ShutdownCh:
|
||||
c.Ui.Output("Interrupt received. Gracefully shutting down...")
|
||||
// Cancel our context so we can start gracefully exiting
|
||||
ctxCancel()
|
||||
|
||||
// Stop execution
|
||||
go ctx.Stop()
|
||||
// Notify the user
|
||||
c.Ui.Output("Interrupt received. Gracefully shutting down...")
|
||||
|
||||
// Still get the result, since there is still one
|
||||
select {
|
||||
|
@ -241,65 +208,27 @@ func (c *ApplyCommand) Run(args []string) int {
|
|||
"Two interrupts received. Exiting immediately. Note that data\n" +
|
||||
"loss may have occurred.")
|
||||
return 1
|
||||
case <-doneCh:
|
||||
case <-op.Done():
|
||||
}
|
||||
case <-doneCh:
|
||||
}
|
||||
|
||||
// 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))
|
||||
case <-op.Done():
|
||||
if err := op.Err; err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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 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))
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an error in the shadow graph, let the user know.
|
||||
c.outputShadowError(shadowErr, applyErr == nil)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
|
@ -519,7 +519,7 @@ func TestApply_plan(t *testing.T) {
|
|||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
"-state-out", statePath,
|
||||
planPath,
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
|
@ -564,7 +564,7 @@ func TestApply_plan_backup(t *testing.T) {
|
|||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
"-state-out", statePath,
|
||||
"-backup", backupPath,
|
||||
planPath,
|
||||
}
|
||||
|
@ -601,7 +601,7 @@ func TestApply_plan_noBackup(t *testing.T) {
|
|||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
"-state-out", statePath,
|
||||
"-backup", "-",
|
||||
planPath,
|
||||
}
|
||||
|
@ -670,12 +670,13 @@ func TestApply_plan_remoteState(t *testing.T) {
|
|||
|
||||
// State file should be not be installed
|
||||
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
|
||||
if _, err := os.Stat(remoteStatePath); err != nil {
|
||||
t.Fatalf("missing remote state: %s", err)
|
||||
// Check that there is no remote state config
|
||||
if _, err := os.Stat(remoteStatePath); err == nil {
|
||||
t.Fatalf("has remote state config")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -710,7 +711,7 @@ func TestApply_planWithVarFile(t *testing.T) {
|
|||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
"-state-out", statePath,
|
||||
planPath,
|
||||
}
|
||||
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 {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@ package command
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/cli"
|
||||
|
@ -27,6 +28,47 @@ const DefaultBackupExtension = ".backup"
|
|||
// operations as it walks the dependency graph.
|
||||
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 {
|
||||
log.Println("[INFO] Validating the context...")
|
||||
ws, es := ctx.Validate()
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -164,7 +170,20 @@ func testState() *terraform.State {
|
|||
},
|
||||
}
|
||||
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 {
|
||||
|
@ -220,9 +239,8 @@ func testStateFileRemote(t *testing.T, s *terraform.State) string {
|
|||
return path
|
||||
}
|
||||
|
||||
// testStateOutput tests that the state at the given path contains
|
||||
// the expected state string.
|
||||
func testStateOutput(t *testing.T, path string, expected string) {
|
||||
// testStateRead reads the state from a file
|
||||
func testStateRead(t *testing.T, path string) *terraform.State {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
|
@ -234,6 +252,13 @@ func testStateOutput(t *testing.T, path string, expected string) {
|
|||
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())
|
||||
expected = strings.TrimSpace(expected)
|
||||
if actual != expected {
|
||||
|
@ -401,3 +426,106 @@ func testStdoutCapture(t *testing.T, dst io.Writer) func() {
|
|||
<-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 (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
||||
"github.com/hashicorp/terraform/repl"
|
||||
|
||||
|
@ -30,30 +30,39 @@ func (c *ConsoleCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
configPath, err := ModulePath(cmdFlags.Args())
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
var configPath string
|
||||
args = cmdFlags.Args()
|
||||
if len(args) > 1 {
|
||||
c.Ui.Error("The console command expects at most one argument.")
|
||||
cmdFlags.Usage()
|
||||
// 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
|
||||
} else if len(args) == 1 {
|
||||
configPath = args[0]
|
||||
} else {
|
||||
configPath = pwd
|
||||
}
|
||||
|
||||
// Build the context based on the arguments given
|
||||
ctx, _, err := c.Context(contextOpts{
|
||||
Path: configPath,
|
||||
PathEmptyOk: true,
|
||||
StatePath: c.Meta.statePath,
|
||||
})
|
||||
// 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, _, err := local.Context(opReq)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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 (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
|
@ -28,19 +27,10 @@ func (c *GetCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
var path string
|
||||
args = cmdFlags.Args()
|
||||
if len(args) > 1 {
|
||||
c.Ui.Error("The get command expects one argument.\n")
|
||||
cmdFlags.Usage()
|
||||
path, err := ModulePath(cmdFlags.Args())
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
mode := module.GetModeGet
|
||||
|
@ -48,12 +38,8 @@ func (c *GetCommand) Run(args []string) int {
|
|||
mode = module.GetModeUpdate
|
||||
}
|
||||
|
||||
_, _, err := c.Context(contextOpts{
|
||||
Path: path,
|
||||
GetMode: mode,
|
||||
})
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err))
|
||||
if err := getModules(&c.Meta, path, mode); err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
|
@ -86,3 +72,17 @@ Options:
|
|||
func (c *GetCommand) Synopsis() string {
|
||||
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 (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/dag"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
@ -34,34 +35,65 @@ func (c *GraphCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
var path string
|
||||
args = cmdFlags.Args()
|
||||
if len(args) > 1 {
|
||||
c.Ui.Error("The graph command expects one argument.\n")
|
||||
cmdFlags.Usage()
|
||||
configPath, err := ModulePath(cmdFlags.Args())
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
} else if len(args) == 1 {
|
||||
path = args[0]
|
||||
} else {
|
||||
var err error
|
||||
path, err = os.Getwd()
|
||||
}
|
||||
|
||||
// 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("Error getting pwd: %s", err))
|
||||
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
ctx, planFile, err := c.Context(contextOpts{
|
||||
Path: path,
|
||||
StatePath: "",
|
||||
// Load the backend
|
||||
b, err := c.Backend(&BackendOpts{
|
||||
ConfigPath: configPath,
|
||||
Plan: plan,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
// Determine the graph type
|
||||
graphType := terraform.GraphTypePlan
|
||||
if planFile {
|
||||
if plan != nil {
|
||||
graphType = terraform.GraphTypeApply
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -45,13 +47,37 @@ func (c *ImportCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
// Build the context based on the arguments given
|
||||
ctx, _, err := c.Context(contextOpts{
|
||||
Path: configPath,
|
||||
PathEmptyOk: true,
|
||||
StatePath: c.Meta.statePath,
|
||||
Parallelism: c.Meta.parallelism,
|
||||
})
|
||||
// Load the module
|
||||
var mod *module.Tree
|
||||
if configPath != "" {
|
||||
var err error
|
||||
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})
|
||||
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 {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
|
@ -76,7 +102,11 @@ func (c *ImportCommand) Run(args []string) int {
|
|||
|
||||
// Persist the final state
|
||||
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))
|
||||
return 1
|
||||
}
|
||||
|
|
273
command/init.go
273
command/init.go
|
@ -1,7 +1,6 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -10,7 +9,6 @@ import (
|
|||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// InitCommand is a Command implementation that takes a Terraform
|
||||
|
@ -20,39 +18,48 @@ type InitCommand struct {
|
|||
}
|
||||
|
||||
func (c *InitCommand) Run(args []string) int {
|
||||
var remoteBackend string
|
||||
var flagBackend, flagGet bool
|
||||
var flagConfigFile string
|
||||
args = c.Meta.process(args, false)
|
||||
remoteConfig := make(map[string]string)
|
||||
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
|
||||
cmdFlags.Var((*FlagStringKV)(&remoteConfig), "backend-config", "config")
|
||||
cmdFlags := c.flagSet("init")
|
||||
cmdFlags.BoolVar(&flagBackend, "backend", true, "")
|
||||
cmdFlags.StringVar(&flagConfigFile, "backend-config", "", "")
|
||||
cmdFlags.BoolVar(&flagGet, "get", true, "")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
remoteBackend = strings.ToLower(remoteBackend)
|
||||
|
||||
var path string
|
||||
// Validate the arg count
|
||||
args = cmdFlags.Args()
|
||||
if len(args) > 2 {
|
||||
c.Ui.Error("The init command expects at most two arguments.\n")
|
||||
cmdFlags.Usage()
|
||||
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
|
||||
}
|
||||
|
||||
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]
|
||||
} else {
|
||||
var err error
|
||||
path, err = os.Getwd()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
||||
}
|
||||
default:
|
||||
panic("assertion failed on arg count")
|
||||
}
|
||||
|
||||
// Set the state out path to be the path requested for the module
|
||||
|
@ -60,110 +67,186 @@ func (c *InitCommand) Run(args []string) int {
|
|||
// proper directory.
|
||||
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
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error reading working directory: %s", err))
|
||||
return 1
|
||||
// If we have a source, copy it
|
||||
if source != "" {
|
||||
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(
|
||||
"Error copying source: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
header = true
|
||||
}
|
||||
|
||||
// Verify the directory is empty
|
||||
// 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 {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error checking on destination path: %s", err))
|
||||
"Error checking configuration: %s", err))
|
||||
return 1
|
||||
} else if empty {
|
||||
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty)))
|
||||
return 0
|
||||
}
|
||||
|
||||
// If we're performing a get or loading the backend, then we perform
|
||||
// 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 {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error loading configuration: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// If we requested downloading modules and have modules in the config
|
||||
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())
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we outputted information, then we need to output a newline
|
||||
// so that our success message is nicely spaced out from prior text.
|
||||
if header {
|
||||
c.Ui.Output("")
|
||||
}
|
||||
|
||||
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess)))
|
||||
|
||||
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 {
|
||||
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 fmt.Errorf(strings.TrimSpace(errInitCopyNotEmpty))
|
||||
}
|
||||
|
||||
// Detect
|
||||
source, err = getter.Detect(source, pwd, getter.Detectors)
|
||||
source, err := getter.Detect(src, pwd, getter.Detectors)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error with module source: %s", err))
|
||||
return 1
|
||||
return fmt.Errorf("Error with module source: %s", err)
|
||||
}
|
||||
|
||||
// Get it!
|
||||
if err := module.GetCopy(path, source); err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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
|
||||
remoteCmd := &RemoteConfigCommand{
|
||||
Meta: c.Meta,
|
||||
remoteConf: &remoteConf,
|
||||
}
|
||||
return remoteCmd.initBlankState()
|
||||
}
|
||||
return 0
|
||||
return module.GetCopy(dst, source)
|
||||
}
|
||||
|
||||
func (c *InitCommand) Help() string {
|
||||
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
|
||||
to the working directory. PATH must be empty of any Terraform files.
|
||||
Any conflicting non-Terraform files will be overwritten.
|
||||
Initialize a new or existing Terraform environment by creating
|
||||
initial files, loading any remote state, downloading modules, etc.
|
||||
|
||||
The module downloaded is a copy. If you're downloading a module from
|
||||
Git, it will not preserve the Git history, it will only copy the
|
||||
latest files.
|
||||
This is the first command that should be run for any new or existing
|
||||
Terraform configuration per machine. This sets up all the local data
|
||||
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:
|
||||
|
||||
-backend=atlas Specifies the type of remote backend. If not
|
||||
specified, local storage will be used.
|
||||
-backend=true Configure the backend for this environment.
|
||||
|
||||
-backend-config="k=v" Specifies configuration for the remote storage
|
||||
backend. This can be specified multiple times.
|
||||
-backend-config=path A path to load additional configuration for the backend.
|
||||
This is merged with what is in the configuration file.
|
||||
|
||||
-no-color If specified, output won't contain any color.
|
||||
-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.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
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 (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/hashicorp/terraform/helper/copy"
|
||||
"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) {
|
||||
ui := new(cli.MockUi)
|
||||
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
|
||||
func TestInit_dstInSrc(t *testing.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) {
|
||||
tmp, cwd := testCwd(t)
|
||||
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())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
250
command/meta.go
250
command/meta.go
|
@ -10,16 +10,13 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"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/variables"
|
||||
"github.com/hashicorp/terraform/helper/wrappedstreams"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
|
@ -27,21 +24,31 @@ import (
|
|||
|
||||
// Meta are the meta-options that are available on all or most commands.
|
||||
type Meta struct {
|
||||
Color bool
|
||||
ContextOpts *terraform.ContextOpts
|
||||
Ui cli.Ui
|
||||
// The exported fields below should be set by anyone using a
|
||||
// command with a Meta field. These are expected to be set externally
|
||||
// (not from within the command itself).
|
||||
|
||||
// State read when calling `Context`. This is available after calling
|
||||
// `Context`.
|
||||
state state.State
|
||||
stateResult *StateResult
|
||||
Color bool // True if output should be colored
|
||||
ContextOpts *terraform.ContextOpts // Opts copied to initialize
|
||||
Ui cli.Ui // Ui for output
|
||||
|
||||
// This can be set by the command itself to provide extra hooks.
|
||||
extraHooks []terraform.Hook
|
||||
// ExtraHooks are extra hooks to add to the context.
|
||||
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
|
||||
|
||||
//----------------------------------------------------------
|
||||
// Private: do not set these
|
||||
//----------------------------------------------------------
|
||||
|
||||
// backendState is the currently active backend state
|
||||
backendState *terraform.BackendState
|
||||
|
||||
// Variables for the context (private)
|
||||
autoKey string
|
||||
autoVariables map[string]interface{}
|
||||
|
@ -51,6 +58,7 @@ type Meta struct {
|
|||
// Targets for this context (private)
|
||||
targets []string
|
||||
|
||||
// Internal fields
|
||||
color bool
|
||||
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.
|
||||
func (m *Meta) DataDir() string {
|
||||
dataDir := DefaultDataDir
|
||||
|
@ -248,53 +159,6 @@ func (m *Meta) InputMode() terraform.InputMode {
|
|||
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.
|
||||
func (m *Meta) UIInput() terraform.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.
|
||||
func (m *Meta) StdinPiped() bool {
|
||||
fi, err := wrappedstreams.Stdin().Stat()
|
||||
|
@ -331,11 +180,16 @@ func (m *Meta) StdinPiped() bool {
|
|||
// contextOpts returns the options to use to initialize a Terraform
|
||||
// context with the settings from this Meta.
|
||||
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 = append(opts.Hooks, m.ContextOpts.Hooks...)
|
||||
opts.Hooks = append(opts.Hooks, m.extraHooks...)
|
||||
if m.ContextOpts != nil {
|
||||
opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
|
||||
}
|
||||
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
|
||||
|
||||
vs := make(map[string]interface{})
|
||||
for k, v := range opts.Variables {
|
||||
|
@ -350,6 +204,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
|
|||
opts.Variables = vs
|
||||
opts.Targets = m.targets
|
||||
opts.UIInput = m.UIInput()
|
||||
opts.Parallelism = m.parallelism
|
||||
opts.Shadow = m.shadow
|
||||
|
||||
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 (
|
||||
// ModuleDepthDefault is the default value for
|
||||
// module depth, which can be overridden by flag
|
||||
|
@ -530,28 +403,3 @@ func (m *Meta) outputShadowError(err error, output bool) bool {
|
|||
|
||||
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 jsonOutput bool
|
||||
|
||||
cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError)
|
||||
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||
cmdFlags.StringVar(&module, "module", "", "module")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
@ -45,9 +43,17 @@ func (c *OutputCommand) Run(args []string) int {
|
|||
name = args[0]
|
||||
}
|
||||
|
||||
stateStore, err := c.Meta.State()
|
||||
// Load the backend
|
||||
b, err := c.Backend(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
|
||||
}
|
||||
|
||||
|
@ -62,7 +68,6 @@ func (c *OutputCommand) Run(args []string) int {
|
|||
|
||||
state := stateStore.State()
|
||||
mod := state.ModuleByPath(modPath)
|
||||
|
||||
if mod == nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"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")
|
||||
}
|
||||
|
||||
func formatMapOutput(indent, outputName string, outputMap map[string]interface{}) string {
|
||||
ks := make([]string, 0, len(outputMap))
|
||||
for k, _ := range outputMap {
|
||||
|
|
209
command/plan.go
209
command/plan.go
|
@ -1,13 +1,12 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
)
|
||||
|
||||
// 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.IntVar(
|
||||
&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.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
var path string
|
||||
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,
|
||||
})
|
||||
configPath, err := ModulePath(cmdFlags.Args())
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
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
|
||||
refresh = false
|
||||
|
||||
// Set the config path to empty for backend loading
|
||||
configPath = ""
|
||||
}
|
||||
|
||||
err = terraform.SetDebugInfo(DefaultDataDir)
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
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()
|
||||
/*
|
||||
err = terraform.SetDebugInfo(DefaultDataDir)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
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 {
|
||||
if detailed && !op.PlanEmpty {
|
||||
return 2
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
@ -241,28 +175,3 @@ Options:
|
|||
func (c *PlanCommand) Synopsis() string {
|
||||
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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/copy"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"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) {
|
||||
p := testProvider()
|
||||
ui := new(cli.MockUi)
|
||||
|
@ -451,7 +575,7 @@ func TestPlan_validate(t *testing.T) {
|
|||
}
|
||||
|
||||
actual := ui.ErrorWriter.String()
|
||||
if !strings.Contains(actual, "can't reference") {
|
||||
if !strings.Contains(actual, "cannot be computed") {
|
||||
t.Fatalf("bad: %s", actual)
|
||||
}
|
||||
}
|
||||
|
|
112
command/push.go
112
command/push.go
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/atlas-go/archive"
|
||||
"github.com/hashicorp/atlas-go/v1"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -63,59 +64,86 @@ 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.
|
||||
var configPath string
|
||||
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
|
||||
}
|
||||
|
||||
// Verify the state is remote, we can't push without a remote state
|
||||
s, err := c.State()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
|
||||
return 1
|
||||
}
|
||||
if !s.State().IsRemote() {
|
||||
c.Ui.Error(
|
||||
"Remote state is not enabled. For Atlas to run Terraform\n" +
|
||||
"for you, remote state must be used and configured. Remote\n" +
|
||||
"state via any backend is accepted, not just Atlas. To\n" +
|
||||
"configure remote state, use the `terraform remote config`\n" +
|
||||
"command.")
|
||||
return 1
|
||||
}
|
||||
|
||||
// Build the context based on the arguments given
|
||||
ctx, planned, err := c.Context(contextOpts{
|
||||
Path: configPath,
|
||||
StatePath: c.Meta.statePath,
|
||||
})
|
||||
|
||||
configPath, err := ModulePath(cmdFlags.Args())
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if planned {
|
||||
|
||||
/*
|
||||
// Verify the state is remote, we can't push without a remote state
|
||||
s, err := c.State()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
|
||||
return 1
|
||||
}
|
||||
if !s.State().IsRemote() {
|
||||
c.Ui.Error(
|
||||
"Remote state is not enabled. For Atlas to run Terraform\n" +
|
||||
"for you, remote state must be used and configured. Remote\n" +
|
||||
"state via any backend is accepted, not just Atlas. To\n" +
|
||||
"configure remote state, use the `terraform remote config`\n" +
|
||||
"command.")
|
||||
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 {
|
||||
c.Ui.Error(
|
||||
"A plan file cannot be given as the path to the configuration.\n" +
|
||||
"A path to a module (directory with configuration) must be given.")
|
||||
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
|
||||
config := ctx.Module().Config()
|
||||
if name == "" {
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -29,111 +28,50 @@ func (c *RefreshCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
var configPath string
|
||||
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,
|
||||
})
|
||||
configPath, err := ModulePath(cmdFlags.Args())
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
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
|
||||
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()
|
||||
// Load the module
|
||||
mod, err := c.Module(configPath)
|
||||
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
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
||||
if err := c.Meta.PersistState(newState); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||
// 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
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -712,6 +712,10 @@ func TestRefresh_disableBackup(t *testing.T) {
|
|||
if err == nil || !os.IsNotExist(err) {
|
||||
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) {
|
||||
|
|
|
@ -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"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/command/format"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
|
@ -66,14 +67,26 @@ func (c *ShowCommand) Run(args []string) int {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
stateOpts := c.StateOpts()
|
||||
stateOpts.RemoteCacheOnly = true
|
||||
result, err := State(stateOpts)
|
||||
// Load the backend
|
||||
b, err := c.Backend(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
|
||||
}
|
||||
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 {
|
||||
c.Ui.Output("No state.")
|
||||
return 0
|
||||
|
@ -92,7 +105,7 @@ func (c *ShowCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
if plan != nil {
|
||||
c.Ui.Output(FormatPlan(&FormatPlanOpts{
|
||||
c.Ui.Output(format.Plan(&format.PlanOpts{
|
||||
Plan: plan,
|
||||
Color: c.Colorize(),
|
||||
ModuleDepth: moduleDepth,
|
||||
|
@ -100,7 +113,7 @@ func (c *ShowCommand) Run(args []string) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
c.Ui.Output(FormatState(&FormatStateOpts{
|
||||
c.Ui.Output(format.State(&format.StateOpts{
|
||||
State: state,
|
||||
Color: c.Colorize(),
|
||||
ModuleDepth: moduleDepth,
|
||||
|
|
|
@ -129,20 +129,11 @@ func TestShow_noArgsRemoteState(t *testing.T) {
|
|||
tmp, cwd := testCwd(t)
|
||||
defer testFixCwd(t, tmp, cwd)
|
||||
|
||||
// Pretend like we have a local cache of remote state
|
||||
remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||
if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
// Create some legacy remote state
|
||||
legacyState := testState()
|
||||
_, srv := testRemoteState(t, legacyState, 200)
|
||||
defer srv.Close()
|
||||
testStateFileRemote(t, legacyState)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &ShowCommand{
|
||||
|
|
|
@ -24,10 +24,18 @@ func (c *StateListCommand) Run(args []string) int {
|
|||
}
|
||||
args = cmdFlags.Args()
|
||||
|
||||
state, err := c.State()
|
||||
// Load the backend
|
||||
b, err := c.Backend(nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
|
||||
return cli.RunResultHelp
|
||||
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
|
||||
}
|
||||
|
||||
stateReal := state.State()
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
backendlocal "github.com/hashicorp/terraform/backend/local"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"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
|
||||
m.backupPath = "-"
|
||||
|
||||
// Get the state (shouldn't be wrapped in a backup)
|
||||
s, err := m.State()
|
||||
// Load the backend
|
||||
b, err := m.Backend(nil)
|
||||
if err != nil {
|
||||
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
|
||||
// file where state is written (cached in the case of remote state)
|
||||
backupPath := fmt.Sprintf(
|
||||
"%s.%d%s",
|
||||
m.stateOutPath,
|
||||
localB.StateOutPath,
|
||||
time.Now().UTC().Unix(),
|
||||
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()
|
||||
|
||||
state, err := c.Meta.State()
|
||||
// Load the backend
|
||||
b, err := c.Backend(nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
|
||||
return cli.RunResultHelp
|
||||
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
|
||||
}
|
||||
|
||||
stateReal := state.State()
|
||||
|
|
|
@ -56,8 +56,15 @@ func (c *TaintCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
// Get the state that we'll be modifying
|
||||
state, err := c.State()
|
||||
// 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
|
||||
|
@ -122,7 +129,11 @@ func (c *TaintCommand) Run(args []string) int {
|
|||
rs.Taint()
|
||||
|
||||
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))
|
||||
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