github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/e2etest/primary_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package e2etest
     5  
     6  import (
     7  	"path/filepath"
     8  	"reflect"
     9  	"sort"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/davecgh/go-spew/spew"
    14  	"github.com/terramate-io/tf/e2e"
    15  	"github.com/terramate-io/tf/plans"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  // The tests in this file are for the "primary workflow", which includes
    20  // variants of the following sequence, with different details:
    21  // terraform init
    22  // terraform plan
    23  // terraform apply
    24  // terraform destroy
    25  
    26  func TestPrimarySeparatePlan(t *testing.T) {
    27  	t.Parallel()
    28  
    29  	// This test reaches out to releases.hashicorp.com to download the
    30  	// template and null providers, so it can only run if network access is
    31  	// allowed.
    32  	skipIfCannotAccessNetwork(t)
    33  
    34  	fixturePath := filepath.Join("testdata", "full-workflow-null")
    35  	tf := e2e.NewBinary(t, terraformBin, fixturePath)
    36  
    37  	//// INIT
    38  	stdout, stderr, err := tf.Run("init")
    39  	if err != nil {
    40  		t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
    41  	}
    42  
    43  	// Make sure we actually downloaded the plugins, rather than picking up
    44  	// copies that might be already installed globally on the system.
    45  	if !strings.Contains(stdout, "Installing hashicorp/template v") {
    46  		t.Errorf("template provider download message is missing from init output:\n%s", stdout)
    47  		t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
    48  	}
    49  	if !strings.Contains(stdout, "Installing hashicorp/null v") {
    50  		t.Errorf("null provider download message is missing from init output:\n%s", stdout)
    51  		t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)")
    52  	}
    53  
    54  	//// PLAN
    55  	stdout, stderr, err = tf.Run("plan", "-out=tfplan")
    56  	if err != nil {
    57  		t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
    58  	}
    59  
    60  	if !strings.Contains(stdout, "1 to add, 0 to change, 0 to destroy") {
    61  		t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout)
    62  	}
    63  
    64  	if !strings.Contains(stdout, "Saved the plan to: tfplan") {
    65  		t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout)
    66  	}
    67  	if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
    68  		t.Errorf("missing next-step instruction in plan output\n%s", stdout)
    69  	}
    70  
    71  	plan, err := tf.Plan("tfplan")
    72  	if err != nil {
    73  		t.Fatalf("failed to read plan file: %s", err)
    74  	}
    75  
    76  	diffResources := plan.Changes.Resources
    77  	if len(diffResources) != 1 {
    78  		t.Errorf("incorrect number of resources in plan")
    79  	}
    80  
    81  	expected := map[string]plans.Action{
    82  		"null_resource.test": plans.Create,
    83  	}
    84  
    85  	for _, r := range diffResources {
    86  		expectedAction, ok := expected[r.Addr.String()]
    87  		if !ok {
    88  			t.Fatalf("unexpected change for %q", r.Addr)
    89  		}
    90  		if r.Action != expectedAction {
    91  			t.Fatalf("unexpected action %q for %q", r.Action, r.Addr)
    92  		}
    93  	}
    94  
    95  	//// APPLY
    96  	stdout, stderr, err = tf.Run("apply", "tfplan")
    97  	if err != nil {
    98  		t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
    99  	}
   100  
   101  	if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
   102  		t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
   103  	}
   104  
   105  	state, err := tf.LocalState()
   106  	if err != nil {
   107  		t.Fatalf("failed to read state file: %s", err)
   108  	}
   109  
   110  	stateResources := state.RootModule().Resources
   111  	var gotResources []string
   112  	for n := range stateResources {
   113  		gotResources = append(gotResources, n)
   114  	}
   115  	sort.Strings(gotResources)
   116  
   117  	wantResources := []string{
   118  		"data.template_file.test",
   119  		"null_resource.test",
   120  	}
   121  
   122  	if !reflect.DeepEqual(gotResources, wantResources) {
   123  		t.Errorf("wrong resources in state\ngot: %#v\nwant: %#v", gotResources, wantResources)
   124  	}
   125  
   126  	//// DESTROY
   127  	stdout, stderr, err = tf.Run("destroy", "-auto-approve")
   128  	if err != nil {
   129  		t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
   130  	}
   131  
   132  	if !strings.Contains(stdout, "Resources: 1 destroyed") {
   133  		t.Errorf("incorrect destroy tally; want 1 destroyed:\n%s", stdout)
   134  	}
   135  
   136  	state, err = tf.LocalState()
   137  	if err != nil {
   138  		t.Fatalf("failed to read state file after destroy: %s", err)
   139  	}
   140  
   141  	stateResources = state.RootModule().Resources
   142  	if len(stateResources) != 0 {
   143  		t.Errorf("wrong resources in state after destroy; want none, but still have:%s", spew.Sdump(stateResources))
   144  	}
   145  
   146  }
   147  
   148  func TestPrimaryChdirOption(t *testing.T) {
   149  	t.Parallel()
   150  
   151  	// This test case does not include any provider dependencies, so it's
   152  	// safe to run it even when network access is disallowed.
   153  
   154  	fixturePath := filepath.Join("testdata", "chdir-option")
   155  	tf := e2e.NewBinary(t, terraformBin, fixturePath)
   156  
   157  	//// INIT
   158  	_, stderr, err := tf.Run("-chdir=subdir", "init")
   159  	if err != nil {
   160  		t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
   161  	}
   162  
   163  	//// PLAN
   164  	stdout, stderr, err := tf.Run("-chdir=subdir", "plan", "-out=tfplan")
   165  	if err != nil {
   166  		t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
   167  	}
   168  
   169  	if want := "You can apply this plan to save these new output values"; !strings.Contains(stdout, want) {
   170  		t.Errorf("missing expected message for an outputs-only plan\ngot:\n%s\n\nwant substring: %s", stdout, want)
   171  	}
   172  
   173  	if !strings.Contains(stdout, "Saved the plan to: tfplan") {
   174  		t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout)
   175  	}
   176  	if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
   177  		t.Errorf("missing next-step instruction in plan output\n%s", stdout)
   178  	}
   179  
   180  	// The saved plan is in the subdirectory because -chdir switched there
   181  	plan, err := tf.Plan("subdir/tfplan")
   182  	if err != nil {
   183  		t.Fatalf("failed to read plan file: %s", err)
   184  	}
   185  
   186  	diffResources := plan.Changes.Resources
   187  	if len(diffResources) != 0 {
   188  		t.Errorf("incorrect diff in plan; want no resource changes, but have:\n%s", spew.Sdump(diffResources))
   189  	}
   190  
   191  	//// APPLY
   192  	stdout, stderr, err = tf.Run("-chdir=subdir", "apply", "tfplan")
   193  	if err != nil {
   194  		t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
   195  	}
   196  
   197  	if !strings.Contains(stdout, "Resources: 0 added, 0 changed, 0 destroyed") {
   198  		t.Errorf("incorrect apply tally; want 0 added:\n%s", stdout)
   199  	}
   200  
   201  	// The state file is in subdir because -chdir changed the current working directory.
   202  	state, err := tf.StateFromFile("subdir/terraform.tfstate")
   203  	if err != nil {
   204  		t.Fatalf("failed to read state file: %s", err)
   205  	}
   206  
   207  	gotOutput := state.RootModule().OutputValues["cwd"]
   208  	wantOutputValue := cty.StringVal(filepath.ToSlash(tf.Path())) // path.cwd returns the original path, because path.root is how we get the overridden path
   209  	if gotOutput == nil || !wantOutputValue.RawEquals(gotOutput.Value) {
   210  		t.Errorf("incorrect value for cwd output\ngot: %#v\nwant Value: %#v", gotOutput, wantOutputValue)
   211  	}
   212  
   213  	gotOutput = state.RootModule().OutputValues["root"]
   214  	wantOutputValue = cty.StringVal(filepath.ToSlash(tf.Path("subdir"))) // path.root is a relative path, but the text fixture uses abspath on it.
   215  	if gotOutput == nil || !wantOutputValue.RawEquals(gotOutput.Value) {
   216  		t.Errorf("incorrect value for root output\ngot: %#v\nwant Value: %#v", gotOutput, wantOutputValue)
   217  	}
   218  
   219  	if len(state.RootModule().Resources) != 0 {
   220  		t.Errorf("unexpected resources in state")
   221  	}
   222  
   223  	//// DESTROY
   224  	stdout, stderr, err = tf.Run("-chdir=subdir", "destroy", "-auto-approve")
   225  	if err != nil {
   226  		t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
   227  	}
   228  
   229  	if !strings.Contains(stdout, "Resources: 0 destroyed") {
   230  		t.Errorf("incorrect destroy tally; want 0 destroyed:\n%s", stdout)
   231  	}
   232  }