Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 337 additions & 0 deletions internal/command/e2etest/primary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,343 @@ func TestPrimarySeparatePlan(t *testing.T) {

}

// There is NO error if you apply a plan against the wrong workspace and:
// 1) The plan doesn't depend on prior state (only adds resources)
// 2) there's no state for that 'wrong' workspace yet
func TestPrimarySeparatePlan_incorrectWorkspace_noPriorState(t *testing.T) {
t.Parallel()

// This test reaches out to releases.hashicorp.com to download the
// template and null providers, so it can only run if network access is
// allowed.
skipIfCannotAccessNetwork(t)

fixturePath := filepath.Join("testdata", "full-workflow")
tf := e2e.NewBinary(t, terraformBin, fixturePath)

//// INIT
stdout, stderr, err := tf.Run("init", "-no-color")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}

// Make sure we actually downloaded the plugins, rather than picking up
// copies that might be already installed globally on the system.
if !strings.Contains(stdout, "Installing hashicorp/null v") {
t.Errorf("null provider download message is missing from init output:\n%s", stdout)
t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
}

//// PLAN
stdout, stderr, err = tf.Run("plan", "-out=tfplan", "-no-color")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "1 to add, 0 to change, 0 to destroy") {
t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout)
}

if !strings.Contains(stdout, "Saved the plan to: tfplan") {
t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout)
}
if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
t.Errorf("missing next-step instruction in plan output\n%s", stdout)
}

plan, err := tf.Plan("tfplan")
if err != nil {
t.Fatalf("failed to read plan file: %s", err)
}

if plan.Backend.Workspace != "default" {
t.Fatalf("expected plan to contain Workspace %q, got %q", "default", plan.Backend.Workspace)
}

// Create and select a workspace that doesn't match the plan made above
newWorkspace := "foobar"
stdout, stderr, err = tf.Run("workspace", "new", newWorkspace, "-no-color")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}
expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace)
if !strings.Contains(stdout, expectedMsg) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
}

//// APPLY
stdout, stderr, err = tf.Run("apply", "tfplan", "-no-color")
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
}

// Read the state saved for the custom workspace 'foobar'
filename := fmt.Sprintf("%s/terraform.tfstate.d/%s/terraform.tfstate", tf.WorkDir(), newWorkspace)
f, err := os.OpenFile(filename, os.O_RDWR, 0666)
if err != nil {
t.Fatalf("Error opening file at %s: %s", filename, err)
}
defer f.Close()

stateFile, err := statefile.Read(f)
if err != nil {
t.Fatalf("Error reading statefile: %s", err)
}
state := stateFile.State

stateResources := state.RootModule().Resources
var gotResources []string
for n := range stateResources {
gotResources = append(gotResources, n)
}
sort.Strings(gotResources)

wantResources := []string{
"null_resource.test",
}

if !reflect.DeepEqual(gotResources, wantResources) {
t.Errorf("wrong resources in state\ngot: %#v\nwant: %#v", gotResources, wantResources)
}

//// DESTROY
stdout, stderr, err = tf.Run("destroy", "-auto-approve", "-no-color")
if err != nil {
t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Resources: 1 destroyed") {
t.Errorf("incorrect destroy tally; want 1 destroyed:\n%s", stdout)
}

f, err = os.OpenFile(filename, os.O_RDWR, 0666)
if err != nil {
t.Fatalf("Error opening file at %s: %s", filename, err)
}
defer f.Close()

stateFile, err = statefile.Read(f)
if err != nil {
t.Fatalf("Error reading statefile: %s", err)
}
state = stateFile.State

stateResources = state.RootModule().Resources
if len(stateResources) != 0 {
t.Errorf("wrong resources in state after destroy; want none, but still have:%s", spew.Sdump(stateResources))
}

}

// There is an error if you apply a plan against the wrong workspace, where that plan is making resources for the first time.
//
// In this case there is a state for the 'wrong' workspace, which is mismatched with the state used for the plan.
func TestPrimarySeparatePlan_incorrectWorkspace_withPriorState(t *testing.T) {
t.Parallel()

// This test reaches out to releases.hashicorp.com to download the
// template and null providers, so it can only run if network access is
// allowed.
skipIfCannotAccessNetwork(t)

fixturePath := filepath.Join("testdata", "full-workflow")
tf := e2e.NewBinary(t, terraformBin, fixturePath)

//// INIT
stdout, stderr, err := tf.Run("init", "-no-color")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}

// Make sure we actually downloaded the plugins, rather than picking up
// copies that might be already installed globally on the system.
if !strings.Contains(stdout, "Installing hashicorp/null v") {
t.Errorf("null provider download message is missing from init output:\n%s", stdout)
t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
}

// CREATE WORKSPACE
//
// Create and select a custom workspace
newWorkspace := "foobar"
stdout, stderr, err = tf.Run("workspace", "new", newWorkspace, "-no-color")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}
expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace)
if !strings.Contains(stdout, expectedMsg) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
}

//// APPLY
//
// This will make the prior state for the custom workspace
stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color")
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
}

//// SELECT WORKSPACE
//
// Select the default workspace, so we can make a plan with that
// workspace baked in.
stdout, stderr, err = tf.Run("workspace", "select", "default", "-no-color")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Switched to workspace \"default\"") {
t.Fatalf("unexpected output:\n%s", stdout)
}

//// PLAN
stdout, stderr, err = tf.Run("plan", "-out=tfplan", "-no-color")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "1 to add, 0 to change, 0 to destroy") {
t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout)
}

if !strings.Contains(stdout, "Saved the plan to: tfplan") {
t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout)
}
if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
t.Errorf("missing next-step instruction in plan output\n%s", stdout)
}

plan, err := tf.Plan("tfplan")
if err != nil {
t.Fatalf("failed to read plan file: %s", err)
}

if plan.Backend.Workspace != "default" {
t.Fatalf("expected plan to contain Workspace %q, got %q", "default", plan.Backend.Workspace)
}

//// SELECT WORKSPACE
//
// Select the custom workspace, so we can try to use the plan against
// that workspace with its prior state
stdout, stderr, err = tf.Run("workspace", "select", newWorkspace, "-no-color")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Switched to workspace \"foobar\".") {
t.Fatalf("unexpected output:\n%s", stdout)
}

//// APPLY
//
// With the plan intended for the default workspace
stdout, stderr, err = tf.Run("apply", "tfplan", "-no-color")
if err == nil {
t.Fatalf("expected an error but got none: %s\nstdout:\n%s", err, stdout)
}

if !strings.Contains(stderr, "Saved plan is stale") {
t.Errorf("expected error to report the plan as stale, got:\n%s", stderr)
}
}

// There is an error if you apply a plan against the wrong workspace and that plan relies on prior state (makes changes or removes resources)
//
// In this case there is no state for the 'wrong' workspace.
// Also, the error specifies lineage as the reason for the error.
func TestPrimarySeparatePlan_incorrectWorkspace_planChangingExistingResources_noPriorState(t *testing.T) {
t.Parallel()

// This test reaches out to releases.hashicorp.com to download the
// template and null providers, so it can only run if network access is
// allowed.
skipIfCannotAccessNetwork(t)

fixturePath := filepath.Join("testdata", "full-workflow")
tf := e2e.NewBinary(t, terraformBin, fixturePath)

//// INIT
stdout, stderr, err := tf.Run("init", "-no-color")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}

// Make sure we actually downloaded the plugins, rather than picking up
// copies that might be already installed globally on the system.
if !strings.Contains(stdout, "Installing hashicorp/null v") {
t.Errorf("null provider download message is missing from init output:\n%s", stdout)
t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
}

//// APPLY
//
// Create state for the default workspace
stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color")
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
}

//// PLAN
stdout, stderr, err = tf.Run("plan", "-var=name=Sarah", "-out=tfplan", "-no-color")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Plan: 1 to add, 0 to change, 1 to destroy.") {
t.Fatalf("unexpected output: %s", stdout)
}

if !strings.Contains(stdout, "Saved the plan to: tfplan") {
t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout)
}
if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
t.Errorf("missing next-step instruction in plan output\n%s", stdout)
}

plan, err := tf.Plan("tfplan")
if err != nil {
t.Fatalf("failed to read plan file: %s", err)
}

if plan.Backend.Workspace != "default" {
t.Fatalf("expected plan to contain Workspace %q, got %q", "default", plan.Backend.Workspace)
}

// Create and select a workspace that doesn't match the plan made above
newWorkspace := "foobar"
stdout, stderr, err = tf.Run("workspace", "new", newWorkspace, "-no-color")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}
expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace)
if !strings.Contains(stdout, expectedMsg) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
}

//// APPLY
stdout, stderr, err = tf.Run("apply", "tfplan", "-no-color")
if err == nil {
t.Fatalf("expected error but got none: %s\nstdout:\n%s", err, stdout)
}

if !strings.Contains(stderr, "different state lineage") {
t.Errorf("expected error to be due to lineage, but got:\n%s", stderr)
}
}

func TestPrimaryChdirOption(t *testing.T) {
t.Parallel()

Expand Down
14 changes: 14 additions & 0 deletions internal/command/e2etest/testdata/full-workflow/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

variable "name" {
default = "world"
}

resource "null_resource" "test" {
triggers = {
greeting = "Hello ${var.name}"
}
}

output "greeting" {
value = null_resource.test.triggers["greeting"]
}