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  }