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