github.com/opentofu/opentofu@v1.7.1/internal/command/e2etest/automation_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/opentofu/opentofu/internal/e2e" 16 "github.com/opentofu/opentofu/internal/plans" 17 ) 18 19 // TestPlanApplyInAutomation runs through the "main case" of init, plan, apply 20 // using the specific command line options suggested in the guide. 21 func TestPlanApplyInAutomation(t *testing.T) { 22 t.Parallel() 23 24 // This test reaches out to registry.opentofu.org to download the 25 // template and null providers, so it can only run if network access is 26 // allowed. 27 skipIfCannotAccessNetwork(t) 28 29 fixturePath := filepath.Join("testdata", "full-workflow-null") 30 tf := e2e.NewBinary(t, tofuBin, fixturePath) 31 32 // We advertise that _any_ non-empty value works, so we'll test something 33 // unconventional here. 34 tf.AddEnv("TF_IN_AUTOMATION=yes-please") 35 36 //// INIT 37 stdout, stderr, err := tf.Run("init", "-input=false") 38 if err != nil { 39 t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) 40 } 41 42 // Make sure we actually downloaded the plugins, rather than picking up 43 // copies that might be already installed globally on the system. 44 if !strings.Contains(stdout, "Installing hashicorp/template v") { 45 t.Errorf("template provider download message is missing from init output:\n%s", stdout) 46 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 47 } 48 if !strings.Contains(stdout, "Installing hashicorp/null v") { 49 t.Errorf("null provider download message is missing from init output:\n%s", stdout) 50 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 51 } 52 53 //// PLAN 54 stdout, stderr, err = tf.Run("plan", "-out=tfplan", "-input=false") 55 if err != nil { 56 t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) 57 } 58 59 if !strings.Contains(stdout, "1 to add, 0 to change, 0 to destroy") { 60 t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout) 61 } 62 63 // Because we're running with TF_IN_AUTOMATION set, we should not see 64 // any mention of the plan file in the output. 65 if strings.Contains(stdout, "tfplan") { 66 t.Errorf("unwanted mention of \"tfplan\" file in plan output\n%s", stdout) 67 } 68 69 plan, err := tf.Plan("tfplan") 70 if err != nil { 71 t.Fatalf("failed to read plan file: %s", err) 72 } 73 74 // stateResources := plan.Changes.Resources 75 diffResources := plan.Changes.Resources 76 if len(diffResources) != 1 { 77 t.Errorf("incorrect number of resources in plan") 78 } 79 80 expected := map[string]plans.Action{ 81 "null_resource.test": plans.Create, 82 } 83 84 for _, r := range diffResources { 85 expectedAction, ok := expected[r.Addr.String()] 86 if !ok { 87 t.Fatalf("unexpected change for %q", r.Addr) 88 } 89 if r.Action != expectedAction { 90 t.Fatalf("unexpected action %q for %q", r.Action, r.Addr) 91 } 92 } 93 94 //// APPLY 95 stdout, stderr, err = tf.Run("apply", "-input=false", "tfplan") 96 if err != nil { 97 t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) 98 } 99 100 if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") { 101 t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout) 102 } 103 104 state, err := tf.LocalState() 105 if err != nil { 106 t.Fatalf("failed to read state file: %s", err) 107 } 108 109 stateResources := state.RootModule().Resources 110 var gotResources []string 111 for n := range stateResources { 112 gotResources = append(gotResources, n) 113 } 114 sort.Strings(gotResources) 115 116 wantResources := []string{ 117 "data.template_file.test", 118 "null_resource.test", 119 } 120 121 if !reflect.DeepEqual(gotResources, wantResources) { 122 t.Errorf("wrong resources in state\ngot: %#v\nwant: %#v", gotResources, wantResources) 123 } 124 } 125 126 // TestAutoApplyInAutomation tests the scenario where the caller skips creating 127 // an explicit plan and instead forces automatic application of changes. 128 func TestAutoApplyInAutomation(t *testing.T) { 129 t.Parallel() 130 131 // This test reaches out to registry.opentofu.org to download the 132 // template and null providers, so it can only run if network access is 133 // allowed. 134 skipIfCannotAccessNetwork(t) 135 136 fixturePath := filepath.Join("testdata", "full-workflow-null") 137 tf := e2e.NewBinary(t, tofuBin, fixturePath) 138 139 // We advertise that _any_ non-empty value works, so we'll test something 140 // unconventional here. 141 tf.AddEnv("TF_IN_AUTOMATION=very-much-so") 142 143 //// INIT 144 stdout, stderr, err := tf.Run("init", "-input=false") 145 if err != nil { 146 t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) 147 } 148 149 // Make sure we actually downloaded the plugins, rather than picking up 150 // copies that might be already installed globally on the system. 151 if !strings.Contains(stdout, "Installing hashicorp/template v") { 152 t.Errorf("template provider download message is missing from init output:\n%s", stdout) 153 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 154 } 155 if !strings.Contains(stdout, "Installing hashicorp/null v") { 156 t.Errorf("null provider download message is missing from init output:\n%s", stdout) 157 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 158 } 159 160 //// APPLY 161 stdout, stderr, err = tf.Run("apply", "-input=false", "-auto-approve") 162 if err != nil { 163 t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) 164 } 165 166 if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") { 167 t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout) 168 } 169 170 state, err := tf.LocalState() 171 if err != nil { 172 t.Fatalf("failed to read state file: %s", err) 173 } 174 175 stateResources := state.RootModule().Resources 176 var gotResources []string 177 for n := range stateResources { 178 gotResources = append(gotResources, n) 179 } 180 sort.Strings(gotResources) 181 182 wantResources := []string{ 183 "data.template_file.test", 184 "null_resource.test", 185 } 186 187 if !reflect.DeepEqual(gotResources, wantResources) { 188 t.Errorf("wrong resources in state\ngot: %#v\nwant: %#v", gotResources, wantResources) 189 } 190 } 191 192 // TestPlanOnlyInAutomation tests the scenario of creating a "throwaway" plan, 193 // which we recommend as a way to verify a pull request. 194 func TestPlanOnlyInAutomation(t *testing.T) { 195 t.Parallel() 196 197 // This test reaches out to registry.opentofu.org to download the 198 // template and null providers, so it can only run if network access is 199 // allowed. 200 skipIfCannotAccessNetwork(t) 201 202 fixturePath := filepath.Join("testdata", "full-workflow-null") 203 tf := e2e.NewBinary(t, tofuBin, fixturePath) 204 205 // We advertise that _any_ non-empty value works, so we'll test something 206 // unconventional here. 207 tf.AddEnv("TF_IN_AUTOMATION=verily") 208 209 //// INIT 210 stdout, stderr, err := tf.Run("init", "-input=false") 211 if err != nil { 212 t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) 213 } 214 215 // Make sure we actually downloaded the plugins, rather than picking up 216 // copies that might be already installed globally on the system. 217 if !strings.Contains(stdout, "Installing hashicorp/template v") { 218 t.Errorf("template provider download message is missing from init output:\n%s", stdout) 219 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 220 } 221 if !strings.Contains(stdout, "Installing hashicorp/null v") { 222 t.Errorf("null provider download message is missing from init output:\n%s", stdout) 223 t.Logf("(this can happen if you have a copy of the plugin in one of the global plugin search dirs)") 224 } 225 226 //// PLAN 227 stdout, stderr, err = tf.Run("plan", "-input=false") 228 if err != nil { 229 t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) 230 } 231 232 if !strings.Contains(stdout, "1 to add, 0 to change, 0 to destroy") { 233 t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout) 234 } 235 236 // Because we're running with TF_IN_AUTOMATION set, we should not see 237 // any mention of the "tofu apply" command in the output. 238 if strings.Contains(stdout, "tofu apply") { 239 t.Errorf("unwanted mention of \"tofu apply\" in plan output\n%s", stdout) 240 } 241 242 if tf.FileExists("tfplan") { 243 t.Error("plan file was created, but was not expected") 244 } 245 }