github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/hook_json_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"fmt"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/terramate-io/tf/addrs"
    13  	"github.com/terramate-io/tf/plans"
    14  	"github.com/terramate-io/tf/states"
    15  	"github.com/terramate-io/tf/terminal"
    16  	"github.com/terramate-io/tf/terraform"
    17  	"github.com/zclconf/go-cty/cty"
    18  )
    19  
    20  // Test a sequence of hooks associated with creating a resource
    21  func TestJSONHook_create(t *testing.T) {
    22  	streams, done := terminal.StreamsForTesting(t)
    23  	hook := newJSONHook(NewJSONView(NewView(streams)))
    24  
    25  	var nowMu sync.Mutex
    26  	now := time.Now()
    27  	hook.timeNow = func() time.Time {
    28  		nowMu.Lock()
    29  		defer nowMu.Unlock()
    30  		return now
    31  	}
    32  
    33  	after := make(chan time.Time, 1)
    34  	hook.timeAfter = func(time.Duration) <-chan time.Time { return after }
    35  
    36  	addr := addrs.Resource{
    37  		Mode: addrs.ManagedResourceMode,
    38  		Type: "test_instance",
    39  		Name: "boop",
    40  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
    41  	priorState := cty.NullVal(cty.Object(map[string]cty.Type{
    42  		"id":  cty.String,
    43  		"bar": cty.List(cty.String),
    44  	}))
    45  	plannedNewState := cty.ObjectVal(map[string]cty.Value{
    46  		"id": cty.StringVal("test"),
    47  		"bar": cty.ListVal([]cty.Value{
    48  			cty.StringVal("baz"),
    49  		}),
    50  	})
    51  
    52  	action, err := hook.PreApply(addr, states.CurrentGen, plans.Create, priorState, plannedNewState)
    53  	testHookReturnValues(t, action, err)
    54  
    55  	action, err = hook.PreProvisionInstanceStep(addr, "local-exec")
    56  	testHookReturnValues(t, action, err)
    57  
    58  	hook.ProvisionOutput(addr, "local-exec", `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`)
    59  
    60  	action, err = hook.PostProvisionInstanceStep(addr, "local-exec", nil)
    61  	testHookReturnValues(t, action, err)
    62  
    63  	// Travel 10s into the future, notify the progress goroutine, and sleep
    64  	// briefly to allow it to execute
    65  	nowMu.Lock()
    66  	now = now.Add(10 * time.Second)
    67  	after <- now
    68  	nowMu.Unlock()
    69  	time.Sleep(1 * time.Millisecond)
    70  
    71  	// Travel 10s into the future, notify the progress goroutine, and sleep
    72  	// briefly to allow it to execute
    73  	nowMu.Lock()
    74  	now = now.Add(10 * time.Second)
    75  	after <- now
    76  	nowMu.Unlock()
    77  	time.Sleep(1 * time.Millisecond)
    78  
    79  	// Travel 2s into the future. We have arrived!
    80  	nowMu.Lock()
    81  	now = now.Add(2 * time.Second)
    82  	nowMu.Unlock()
    83  
    84  	action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, nil)
    85  	testHookReturnValues(t, action, err)
    86  
    87  	// Shut down the progress goroutine if still active
    88  	hook.applyingLock.Lock()
    89  	for key, progress := range hook.applying {
    90  		close(progress.done)
    91  		<-progress.heartbeatDone
    92  		delete(hook.applying, key)
    93  	}
    94  	hook.applyingLock.Unlock()
    95  
    96  	wantResource := map[string]interface{}{
    97  		"addr":             string("test_instance.boop"),
    98  		"implied_provider": string("test"),
    99  		"module":           string(""),
   100  		"resource":         string("test_instance.boop"),
   101  		"resource_key":     nil,
   102  		"resource_name":    string("boop"),
   103  		"resource_type":    string("test_instance"),
   104  	}
   105  	want := []map[string]interface{}{
   106  		{
   107  			"@level":   "info",
   108  			"@message": "test_instance.boop: Creating...",
   109  			"@module":  "terraform.ui",
   110  			"type":     "apply_start",
   111  			"hook": map[string]interface{}{
   112  				"action":   string("create"),
   113  				"resource": wantResource,
   114  			},
   115  		},
   116  		{
   117  			"@level":   "info",
   118  			"@message": "test_instance.boop: Provisioning with 'local-exec'...",
   119  			"@module":  "terraform.ui",
   120  			"type":     "provision_start",
   121  			"hook": map[string]interface{}{
   122  				"provisioner": "local-exec",
   123  				"resource":    wantResource,
   124  			},
   125  		},
   126  		{
   127  			"@level":   "info",
   128  			"@message": `test_instance.boop: (local-exec): Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
   129  			"@module":  "terraform.ui",
   130  			"type":     "provision_progress",
   131  			"hook": map[string]interface{}{
   132  				"output":      `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
   133  				"provisioner": "local-exec",
   134  				"resource":    wantResource,
   135  			},
   136  		},
   137  		{
   138  			"@level":   "info",
   139  			"@message": "test_instance.boop: (local-exec) Provisioning complete",
   140  			"@module":  "terraform.ui",
   141  			"type":     "provision_complete",
   142  			"hook": map[string]interface{}{
   143  				"provisioner": "local-exec",
   144  				"resource":    wantResource,
   145  			},
   146  		},
   147  		{
   148  			"@level":   "info",
   149  			"@message": "test_instance.boop: Still creating... [10s elapsed]",
   150  			"@module":  "terraform.ui",
   151  			"type":     "apply_progress",
   152  			"hook": map[string]interface{}{
   153  				"action":          string("create"),
   154  				"elapsed_seconds": float64(10),
   155  				"resource":        wantResource,
   156  			},
   157  		},
   158  		{
   159  			"@level":   "info",
   160  			"@message": "test_instance.boop: Still creating... [20s elapsed]",
   161  			"@module":  "terraform.ui",
   162  			"type":     "apply_progress",
   163  			"hook": map[string]interface{}{
   164  				"action":          string("create"),
   165  				"elapsed_seconds": float64(20),
   166  				"resource":        wantResource,
   167  			},
   168  		},
   169  		{
   170  			"@level":   "info",
   171  			"@message": "test_instance.boop: Creation complete after 22s [id=test]",
   172  			"@module":  "terraform.ui",
   173  			"type":     "apply_complete",
   174  			"hook": map[string]interface{}{
   175  				"action":          string("create"),
   176  				"elapsed_seconds": float64(22),
   177  				"id_key":          "id",
   178  				"id_value":        "test",
   179  				"resource":        wantResource,
   180  			},
   181  		},
   182  	}
   183  
   184  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   185  }
   186  
   187  func TestJSONHook_errors(t *testing.T) {
   188  	streams, done := terminal.StreamsForTesting(t)
   189  	hook := newJSONHook(NewJSONView(NewView(streams)))
   190  
   191  	addr := addrs.Resource{
   192  		Mode: addrs.ManagedResourceMode,
   193  		Type: "test_instance",
   194  		Name: "boop",
   195  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   196  	priorState := cty.NullVal(cty.Object(map[string]cty.Type{
   197  		"id":  cty.String,
   198  		"bar": cty.List(cty.String),
   199  	}))
   200  	plannedNewState := cty.ObjectVal(map[string]cty.Value{
   201  		"id": cty.StringVal("test"),
   202  		"bar": cty.ListVal([]cty.Value{
   203  			cty.StringVal("baz"),
   204  		}),
   205  	})
   206  
   207  	action, err := hook.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState)
   208  	testHookReturnValues(t, action, err)
   209  
   210  	provisionError := fmt.Errorf("provisioner didn't want to")
   211  	action, err = hook.PostProvisionInstanceStep(addr, "local-exec", provisionError)
   212  	testHookReturnValues(t, action, err)
   213  
   214  	applyError := fmt.Errorf("provider was sad")
   215  	action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, applyError)
   216  	testHookReturnValues(t, action, err)
   217  
   218  	// Shut down the progress goroutine
   219  	hook.applyingLock.Lock()
   220  	for key, progress := range hook.applying {
   221  		close(progress.done)
   222  		<-progress.heartbeatDone
   223  		delete(hook.applying, key)
   224  	}
   225  	hook.applyingLock.Unlock()
   226  
   227  	wantResource := map[string]interface{}{
   228  		"addr":             string("test_instance.boop"),
   229  		"implied_provider": string("test"),
   230  		"module":           string(""),
   231  		"resource":         string("test_instance.boop"),
   232  		"resource_key":     nil,
   233  		"resource_name":    string("boop"),
   234  		"resource_type":    string("test_instance"),
   235  	}
   236  	want := []map[string]interface{}{
   237  		{
   238  			"@level":   "info",
   239  			"@message": "test_instance.boop: Destroying...",
   240  			"@module":  "terraform.ui",
   241  			"type":     "apply_start",
   242  			"hook": map[string]interface{}{
   243  				"action":   string("delete"),
   244  				"resource": wantResource,
   245  			},
   246  		},
   247  		{
   248  			"@level":   "info",
   249  			"@message": "test_instance.boop: (local-exec) Provisioning errored",
   250  			"@module":  "terraform.ui",
   251  			"type":     "provision_errored",
   252  			"hook": map[string]interface{}{
   253  				"provisioner": "local-exec",
   254  				"resource":    wantResource,
   255  			},
   256  		},
   257  		{
   258  			"@level":   "info",
   259  			"@message": "test_instance.boop: Destruction errored after 0s",
   260  			"@module":  "terraform.ui",
   261  			"type":     "apply_errored",
   262  			"hook": map[string]interface{}{
   263  				"action":          string("delete"),
   264  				"elapsed_seconds": float64(0),
   265  				"resource":        wantResource,
   266  			},
   267  		},
   268  	}
   269  
   270  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   271  }
   272  
   273  func TestJSONHook_refresh(t *testing.T) {
   274  	streams, done := terminal.StreamsForTesting(t)
   275  	hook := newJSONHook(NewJSONView(NewView(streams)))
   276  
   277  	addr := addrs.Resource{
   278  		Mode: addrs.DataResourceMode,
   279  		Type: "test_data_source",
   280  		Name: "beep",
   281  	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
   282  	state := cty.ObjectVal(map[string]cty.Value{
   283  		"id": cty.StringVal("honk"),
   284  		"bar": cty.ListVal([]cty.Value{
   285  			cty.StringVal("baz"),
   286  		}),
   287  	})
   288  
   289  	action, err := hook.PreRefresh(addr, states.CurrentGen, state)
   290  	testHookReturnValues(t, action, err)
   291  
   292  	action, err = hook.PostRefresh(addr, states.CurrentGen, state, state)
   293  	testHookReturnValues(t, action, err)
   294  
   295  	wantResource := map[string]interface{}{
   296  		"addr":             string("data.test_data_source.beep"),
   297  		"implied_provider": string("test"),
   298  		"module":           string(""),
   299  		"resource":         string("data.test_data_source.beep"),
   300  		"resource_key":     nil,
   301  		"resource_name":    string("beep"),
   302  		"resource_type":    string("test_data_source"),
   303  	}
   304  	want := []map[string]interface{}{
   305  		{
   306  			"@level":   "info",
   307  			"@message": "data.test_data_source.beep: Refreshing state... [id=honk]",
   308  			"@module":  "terraform.ui",
   309  			"type":     "refresh_start",
   310  			"hook": map[string]interface{}{
   311  				"resource": wantResource,
   312  				"id_key":   "id",
   313  				"id_value": "honk",
   314  			},
   315  		},
   316  		{
   317  			"@level":   "info",
   318  			"@message": "data.test_data_source.beep: Refresh complete [id=honk]",
   319  			"@module":  "terraform.ui",
   320  			"type":     "refresh_complete",
   321  			"hook": map[string]interface{}{
   322  				"resource": wantResource,
   323  				"id_key":   "id",
   324  				"id_value": "honk",
   325  			},
   326  		},
   327  	}
   328  
   329  	testJSONViewOutputEquals(t, done(t).Stdout(), want)
   330  }
   331  
   332  func testHookReturnValues(t *testing.T, action terraform.HookAction, err error) {
   333  	t.Helper()
   334  
   335  	if err != nil {
   336  		t.Fatal(err)
   337  	}
   338  	if action != terraform.HookActionContinue {
   339  		t.Fatalf("Expected hook to continue, given: %#v", action)
   340  	}
   341  }