github.com/opentofu/opentofu@v1.7.1/internal/builtin/provisioners/local-exec/resource_provisioner_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 localexec 7 8 import ( 9 "fmt" 10 "os" 11 "runtime" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/mitchellh/cli" 17 "github.com/opentofu/opentofu/internal/provisioners" 18 "github.com/zclconf/go-cty/cty" 19 ) 20 21 func TestResourceProvider_Apply(t *testing.T) { 22 defer os.Remove("test_out") 23 output := cli.NewMockUi() 24 p := New() 25 schema := p.GetSchema().Provisioner 26 c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ 27 "command": cty.StringVal("echo foo > test_out"), 28 })) 29 if err != nil { 30 t.Fatal(err) 31 } 32 33 resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{ 34 Config: c, 35 UIOutput: output, 36 }) 37 38 if resp.Diagnostics.HasErrors() { 39 t.Fatalf("err: %v", resp.Diagnostics.Err()) 40 } 41 42 // Check the file 43 raw, err := os.ReadFile("test_out") 44 if err != nil { 45 t.Fatalf("err: %v", err) 46 } 47 48 actual := strings.TrimSpace(string(raw)) 49 expected := "foo" 50 if actual != expected { 51 t.Fatalf("bad: %#v", actual) 52 } 53 } 54 55 func TestResourceProvider_stop(t *testing.T) { 56 output := cli.NewMockUi() 57 p := New() 58 schema := p.GetSchema().Provisioner 59 60 c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ 61 // bash/zsh/ksh will exec a single command in the same process. This 62 // makes certain there's a subprocess in the shell. 63 "command": cty.StringVal("sleep 30; sleep 30"), 64 })) 65 if err != nil { 66 t.Fatal(err) 67 } 68 69 doneCh := make(chan struct{}) 70 startTime := time.Now() 71 go func() { 72 defer close(doneCh) 73 // The functionality of p.Apply is tested in TestResourceProvider_Apply. 74 // Because p.Apply is called in a goroutine, trying to t.Fatal() on its 75 // result would be ignored or would cause a panic if the parent goroutine 76 // has already completed. 77 _ = p.ProvisionResource(provisioners.ProvisionResourceRequest{ 78 Config: c, 79 UIOutput: output, 80 }) 81 }() 82 83 mustExceed := (50 * time.Millisecond) 84 select { 85 case <-doneCh: 86 t.Fatalf("expected to finish sometime after %s finished in %s", mustExceed, time.Since(startTime)) 87 case <-time.After(mustExceed): 88 t.Logf("correctly took longer than %s", mustExceed) 89 } 90 91 // Stop it 92 stopTime := time.Now() 93 p.Stop() 94 95 maxTempl := "expected to finish under %s, finished in %s" 96 finishWithin := (2 * time.Second) 97 select { 98 case <-doneCh: 99 t.Logf(maxTempl, finishWithin, time.Since(stopTime)) 100 case <-time.After(finishWithin): 101 t.Fatalf(maxTempl, finishWithin, time.Since(stopTime)) 102 } 103 } 104 105 func TestResourceProvider_ApplyCustomInterpreter(t *testing.T) { 106 output := cli.NewMockUi() 107 p := New() 108 109 schema := p.GetSchema().Provisioner 110 111 c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ 112 "interpreter": cty.ListVal([]cty.Value{cty.StringVal("echo"), cty.StringVal("is")}), 113 "command": cty.StringVal("not really an interpreter"), 114 })) 115 if err != nil { 116 t.Fatal(err) 117 } 118 119 resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{ 120 Config: c, 121 UIOutput: output, 122 }) 123 124 if resp.Diagnostics.HasErrors() { 125 t.Fatal(resp.Diagnostics.Err()) 126 } 127 128 got := strings.TrimSpace(output.OutputWriter.String()) 129 want := `Executing: ["echo" "is" "not really an interpreter"] 130 is not really an interpreter` 131 if got != want { 132 t.Errorf("wrong output\ngot: %s\nwant: %s", got, want) 133 } 134 } 135 136 func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) { 137 testdir := "working_dir_test" 138 os.Mkdir(testdir, 0755) 139 defer os.Remove(testdir) 140 141 output := cli.NewMockUi() 142 p := New() 143 schema := p.GetSchema().Provisioner 144 145 command := "echo `pwd`" 146 if runtime.GOOS == "windows" { 147 command = "echo %cd%" 148 } 149 c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ 150 "working_dir": cty.StringVal(testdir), 151 "command": cty.StringVal(command), 152 })) 153 if err != nil { 154 t.Fatal(err) 155 } 156 157 resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{ 158 Config: c, 159 UIOutput: output, 160 }) 161 162 if resp.Diagnostics.HasErrors() { 163 t.Fatal(resp.Diagnostics.Err()) 164 } 165 166 dir, err := os.Getwd() 167 if err != nil { 168 t.Fatalf("err: %v", err) 169 } 170 171 got := strings.TrimSpace(output.OutputWriter.String()) 172 want := "Executing: [\"/bin/sh\" \"-c\" \"echo `pwd`\"]\n" + dir + "/" + testdir 173 if runtime.GOOS == "windows" { 174 want = "Executing: [\"cmd\" \"/C\" \"echo %cd%\"]\n" + dir + "\\" + testdir 175 } 176 if got != want { 177 t.Errorf("wrong output\ngot: %s\nwant: %s", got, want) 178 } 179 } 180 181 func TestResourceProvider_ApplyCustomEnv(t *testing.T) { 182 output := cli.NewMockUi() 183 p := New() 184 schema := p.GetSchema().Provisioner 185 command := "echo $FOO $BAR $BAZ" 186 if runtime.GOOS == "windows" { 187 command = "echo %FOO% %BAR% %BAZ%" 188 } 189 c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ 190 "command": cty.StringVal(command), 191 "environment": cty.MapVal(map[string]cty.Value{ 192 "FOO": cty.StringVal("BAR"), 193 "BAR": cty.StringVal("1"), 194 "BAZ": cty.StringVal("true"), 195 }), 196 })) 197 if err != nil { 198 t.Fatal(err) 199 } 200 201 resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{ 202 Config: c, 203 UIOutput: output, 204 }) 205 if resp.Diagnostics.HasErrors() { 206 t.Fatal(resp.Diagnostics.Err()) 207 } 208 209 got := strings.TrimSpace(output.OutputWriter.String()) 210 211 want := "Executing: [\"/bin/sh\" \"-c\" \"echo $FOO $BAR $BAZ\"]\nBAR 1 true" 212 if runtime.GOOS == "windows" { 213 want = "Executing: [\"cmd\" \"/C\" \"echo %FOO% %BAR% %BAZ%\"]\nBAR 1 true" 214 } 215 216 if got != want { 217 t.Errorf("wrong output\ngot: %s\nwant: %s", got, want) 218 } 219 } 220 221 // Validate that Stop can Close can be called even when not provisioning. 222 func TestResourceProvisioner_StopClose(t *testing.T) { 223 p := New() 224 p.Stop() 225 p.Close() 226 } 227 228 func TestResourceProvisioner_nullsInOptionals(t *testing.T) { 229 output := cli.NewMockUi() 230 p := New() 231 schema := p.GetSchema().Provisioner 232 233 for i, cfg := range []cty.Value{ 234 cty.ObjectVal(map[string]cty.Value{ 235 "command": cty.StringVal("echo OK"), 236 "environment": cty.MapVal(map[string]cty.Value{ 237 "FOO": cty.NullVal(cty.String), 238 }), 239 }), 240 cty.ObjectVal(map[string]cty.Value{ 241 "command": cty.StringVal("echo OK"), 242 "environment": cty.NullVal(cty.Map(cty.String)), 243 }), 244 cty.ObjectVal(map[string]cty.Value{ 245 "command": cty.StringVal("echo OK"), 246 "interpreter": cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), 247 }), 248 cty.ObjectVal(map[string]cty.Value{ 249 "command": cty.StringVal("echo OK"), 250 "interpreter": cty.NullVal(cty.List(cty.String)), 251 }), 252 cty.ObjectVal(map[string]cty.Value{ 253 "command": cty.StringVal("echo OK"), 254 "working_dir": cty.NullVal(cty.String), 255 }), 256 } { 257 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 258 259 cfg, err := schema.CoerceValue(cfg) 260 if err != nil { 261 t.Fatal(err) 262 } 263 264 // verifying there are no panics 265 p.ProvisionResource(provisioners.ProvisionResourceRequest{ 266 Config: cfg, 267 UIOutput: output, 268 }) 269 }) 270 } 271 }