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