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 }