github.com/quite/nomad@v0.8.6/nomad/deploymentwatcher/deployments_watcher_test.go (about)

     1  package deploymentwatcher
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  	"time"
     7  
     8  	memdb "github.com/hashicorp/go-memdb"
     9  	"github.com/hashicorp/nomad/helper"
    10  	"github.com/hashicorp/nomad/helper/uuid"
    11  	"github.com/hashicorp/nomad/nomad/mock"
    12  	"github.com/hashicorp/nomad/nomad/structs"
    13  	"github.com/hashicorp/nomad/testutil"
    14  	"github.com/stretchr/testify/assert"
    15  	mocker "github.com/stretchr/testify/mock"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  func testDeploymentWatcher(t *testing.T, qps float64, batchDur time.Duration) (*Watcher, *mockBackend) {
    20  	m := newMockBackend(t)
    21  	w := NewDeploymentsWatcher(testLogger(), m, qps, batchDur)
    22  	return w, m
    23  }
    24  
    25  func defaultTestDeploymentWatcher(t *testing.T) (*Watcher, *mockBackend) {
    26  	return testDeploymentWatcher(t, LimitStateQueriesPerSecond, CrossDeploymentUpdateBatchDuration)
    27  }
    28  
    29  // Tests that the watcher properly watches for deployments and reconciles them
    30  func TestWatcher_WatchDeployments(t *testing.T) {
    31  	t.Parallel()
    32  	assert := assert.New(t)
    33  	w, m := defaultTestDeploymentWatcher(t)
    34  
    35  	// Create three jobs
    36  	j1, j2, j3 := mock.Job(), mock.Job(), mock.Job()
    37  	assert.Nil(m.state.UpsertJob(100, j1))
    38  	assert.Nil(m.state.UpsertJob(101, j2))
    39  	assert.Nil(m.state.UpsertJob(102, j3))
    40  
    41  	// Create three deployments all running
    42  	d1, d2, d3 := mock.Deployment(), mock.Deployment(), mock.Deployment()
    43  	d1.JobID = j1.ID
    44  	d2.JobID = j2.ID
    45  	d3.JobID = j3.ID
    46  
    47  	// Upsert the first deployment
    48  	assert.Nil(m.state.UpsertDeployment(103, d1))
    49  
    50  	// Next list 3
    51  	block1 := make(chan time.Time)
    52  	go func() {
    53  		<-block1
    54  		assert.Nil(m.state.UpsertDeployment(104, d2))
    55  		assert.Nil(m.state.UpsertDeployment(105, d3))
    56  	}()
    57  
    58  	//// Next list 3 but have one be terminal
    59  	block2 := make(chan time.Time)
    60  	d3terminal := d3.Copy()
    61  	d3terminal.Status = structs.DeploymentStatusFailed
    62  	go func() {
    63  		<-block2
    64  		assert.Nil(m.state.UpsertDeployment(106, d3terminal))
    65  	}()
    66  
    67  	w.SetEnabled(true, m.state)
    68  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
    69  		func(err error) { assert.Equal(1, len(w.watchers), "1 deployment returned") })
    70  
    71  	close(block1)
    72  	testutil.WaitForResult(func() (bool, error) { return 3 == len(w.watchers), nil },
    73  		func(err error) { assert.Equal(3, len(w.watchers), "3 deployment returned") })
    74  
    75  	close(block2)
    76  	testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil },
    77  		func(err error) { assert.Equal(3, len(w.watchers), "3 deployment returned - 1 terminal") })
    78  }
    79  
    80  // Tests that calls against an unknown deployment fail
    81  func TestWatcher_UnknownDeployment(t *testing.T) {
    82  	t.Parallel()
    83  	assert := assert.New(t)
    84  	w, m := defaultTestDeploymentWatcher(t)
    85  	w.SetEnabled(true, m.state)
    86  
    87  	// The expected error is that it should be an unknown deployment
    88  	dID := uuid.Generate()
    89  	expected := fmt.Sprintf("unknown deployment %q", dID)
    90  
    91  	// Request setting the health against an unknown deployment
    92  	req := &structs.DeploymentAllocHealthRequest{
    93  		DeploymentID:         dID,
    94  		HealthyAllocationIDs: []string{uuid.Generate()},
    95  	}
    96  	var resp structs.DeploymentUpdateResponse
    97  	err := w.SetAllocHealth(req, &resp)
    98  	if assert.NotNil(err, "should have error for unknown deployment") {
    99  		assert.Contains(err.Error(), expected)
   100  	}
   101  
   102  	// Request promoting against an unknown deployment
   103  	req2 := &structs.DeploymentPromoteRequest{
   104  		DeploymentID: dID,
   105  		All:          true,
   106  	}
   107  	err = w.PromoteDeployment(req2, &resp)
   108  	if assert.NotNil(err, "should have error for unknown deployment") {
   109  		assert.Contains(err.Error(), expected)
   110  	}
   111  
   112  	// Request pausing against an unknown deployment
   113  	req3 := &structs.DeploymentPauseRequest{
   114  		DeploymentID: dID,
   115  		Pause:        true,
   116  	}
   117  	err = w.PauseDeployment(req3, &resp)
   118  	if assert.NotNil(err, "should have error for unknown deployment") {
   119  		assert.Contains(err.Error(), expected)
   120  	}
   121  
   122  	// Request failing against an unknown deployment
   123  	req4 := &structs.DeploymentFailRequest{
   124  		DeploymentID: dID,
   125  	}
   126  	err = w.FailDeployment(req4, &resp)
   127  	if assert.NotNil(err, "should have error for unknown deployment") {
   128  		assert.Contains(err.Error(), expected)
   129  	}
   130  }
   131  
   132  // Test setting an unknown allocation's health
   133  func TestWatcher_SetAllocHealth_Unknown(t *testing.T) {
   134  	t.Parallel()
   135  	assert := assert.New(t)
   136  	w, m := defaultTestDeploymentWatcher(t)
   137  
   138  	// Create a job, and a deployment
   139  	j := mock.Job()
   140  	d := mock.Deployment()
   141  	d.JobID = j.ID
   142  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   143  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   144  
   145  	// Assert that we get a call to UpsertDeploymentAllocHealth
   146  	a := mock.Alloc()
   147  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   148  		DeploymentID: d.ID,
   149  		Healthy:      []string{a.ID},
   150  		Eval:         true,
   151  	}
   152  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   153  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   154  
   155  	w.SetEnabled(true, m.state)
   156  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   157  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   158  
   159  	// Call SetAllocHealth
   160  	req := &structs.DeploymentAllocHealthRequest{
   161  		DeploymentID:         d.ID,
   162  		HealthyAllocationIDs: []string{a.ID},
   163  	}
   164  	var resp structs.DeploymentUpdateResponse
   165  	err := w.SetAllocHealth(req, &resp)
   166  	if assert.NotNil(err, "Set health of unknown allocation") {
   167  		assert.Contains(err.Error(), "unknown")
   168  	}
   169  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   170  }
   171  
   172  // Test setting allocation health
   173  func TestWatcher_SetAllocHealth_Healthy(t *testing.T) {
   174  	t.Parallel()
   175  	assert := assert.New(t)
   176  	w, m := defaultTestDeploymentWatcher(t)
   177  
   178  	// Create a job, alloc, and a deployment
   179  	j := mock.Job()
   180  	d := mock.Deployment()
   181  	d.JobID = j.ID
   182  	a := mock.Alloc()
   183  	a.DeploymentID = d.ID
   184  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   185  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   186  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   187  
   188  	// Assert that we get a call to UpsertDeploymentAllocHealth
   189  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   190  		DeploymentID: d.ID,
   191  		Healthy:      []string{a.ID},
   192  		Eval:         true,
   193  	}
   194  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   195  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   196  
   197  	w.SetEnabled(true, m.state)
   198  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   199  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   200  
   201  	// Call SetAllocHealth
   202  	req := &structs.DeploymentAllocHealthRequest{
   203  		DeploymentID:         d.ID,
   204  		HealthyAllocationIDs: []string{a.ID},
   205  	}
   206  	var resp structs.DeploymentUpdateResponse
   207  	err := w.SetAllocHealth(req, &resp)
   208  	assert.Nil(err, "SetAllocHealth")
   209  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   210  	m.AssertCalled(t, "UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher))
   211  }
   212  
   213  // Test setting allocation unhealthy
   214  func TestWatcher_SetAllocHealth_Unhealthy(t *testing.T) {
   215  	t.Parallel()
   216  	assert := assert.New(t)
   217  	w, m := defaultTestDeploymentWatcher(t)
   218  
   219  	// Create a job, alloc, and a deployment
   220  	j := mock.Job()
   221  	d := mock.Deployment()
   222  	d.JobID = j.ID
   223  	a := mock.Alloc()
   224  	a.DeploymentID = d.ID
   225  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   226  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   227  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   228  
   229  	// Assert that we get a call to UpsertDeploymentAllocHealth
   230  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   231  		DeploymentID: d.ID,
   232  		Unhealthy:    []string{a.ID},
   233  		Eval:         true,
   234  		DeploymentUpdate: &structs.DeploymentStatusUpdate{
   235  			DeploymentID:      d.ID,
   236  			Status:            structs.DeploymentStatusFailed,
   237  			StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations,
   238  		},
   239  	}
   240  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   241  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   242  
   243  	w.SetEnabled(true, m.state)
   244  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   245  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   246  
   247  	// Call SetAllocHealth
   248  	req := &structs.DeploymentAllocHealthRequest{
   249  		DeploymentID:           d.ID,
   250  		UnhealthyAllocationIDs: []string{a.ID},
   251  	}
   252  	var resp structs.DeploymentUpdateResponse
   253  	err := w.SetAllocHealth(req, &resp)
   254  	assert.Nil(err, "SetAllocHealth")
   255  
   256  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   257  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   258  	m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1)
   259  }
   260  
   261  // Test setting allocation unhealthy and that there should be a rollback
   262  func TestWatcher_SetAllocHealth_Unhealthy_Rollback(t *testing.T) {
   263  	t.Parallel()
   264  	assert := assert.New(t)
   265  	w, m := defaultTestDeploymentWatcher(t)
   266  
   267  	// Create a job, alloc, and a deployment
   268  	j := mock.Job()
   269  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   270  	j.TaskGroups[0].Update.MaxParallel = 2
   271  	j.TaskGroups[0].Update.AutoRevert = true
   272  	j.TaskGroups[0].Update.ProgressDeadline = 0
   273  	j.Stable = true
   274  	d := mock.Deployment()
   275  	d.JobID = j.ID
   276  	d.TaskGroups["web"].AutoRevert = true
   277  	a := mock.Alloc()
   278  	a.DeploymentID = d.ID
   279  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   280  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   281  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   282  
   283  	// Upsert the job again to get a new version
   284  	j2 := j.Copy()
   285  	j2.Stable = false
   286  	// Modify the job to make its specification different
   287  	j2.Meta["foo"] = "bar"
   288  
   289  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
   290  
   291  	// Assert that we get a call to UpsertDeploymentAllocHealth
   292  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   293  		DeploymentID: d.ID,
   294  		Unhealthy:    []string{a.ID},
   295  		Eval:         true,
   296  		DeploymentUpdate: &structs.DeploymentStatusUpdate{
   297  			DeploymentID:      d.ID,
   298  			Status:            structs.DeploymentStatusFailed,
   299  			StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations,
   300  		},
   301  		JobVersion: helper.Uint64ToPtr(0),
   302  	}
   303  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   304  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   305  
   306  	w.SetEnabled(true, m.state)
   307  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   308  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   309  
   310  	// Call SetAllocHealth
   311  	req := &structs.DeploymentAllocHealthRequest{
   312  		DeploymentID:           d.ID,
   313  		UnhealthyAllocationIDs: []string{a.ID},
   314  	}
   315  	var resp structs.DeploymentUpdateResponse
   316  	err := w.SetAllocHealth(req, &resp)
   317  	assert.Nil(err, "SetAllocHealth")
   318  
   319  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   320  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   321  	m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1)
   322  }
   323  
   324  // Test setting allocation unhealthy on job with identical spec and there should be no rollback
   325  func TestWatcher_SetAllocHealth_Unhealthy_NoRollback(t *testing.T) {
   326  	t.Parallel()
   327  	assert := assert.New(t)
   328  	w, m := defaultTestDeploymentWatcher(t)
   329  
   330  	// Create a job, alloc, and a deployment
   331  	j := mock.Job()
   332  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   333  	j.TaskGroups[0].Update.MaxParallel = 2
   334  	j.TaskGroups[0].Update.AutoRevert = true
   335  	j.TaskGroups[0].Update.ProgressDeadline = 0
   336  	j.Stable = true
   337  	d := mock.Deployment()
   338  	d.JobID = j.ID
   339  	d.TaskGroups["web"].AutoRevert = true
   340  	a := mock.Alloc()
   341  	a.DeploymentID = d.ID
   342  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   343  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   344  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   345  
   346  	// Upsert the job again to get a new version
   347  	j2 := j.Copy()
   348  	j2.Stable = false
   349  
   350  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
   351  
   352  	// Assert that we get a call to UpsertDeploymentAllocHealth
   353  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   354  		DeploymentID: d.ID,
   355  		Unhealthy:    []string{a.ID},
   356  		Eval:         true,
   357  		DeploymentUpdate: &structs.DeploymentStatusUpdate{
   358  			DeploymentID:      d.ID,
   359  			Status:            structs.DeploymentStatusFailed,
   360  			StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations,
   361  		},
   362  		JobVersion: nil,
   363  	}
   364  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   365  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   366  
   367  	w.SetEnabled(true, m.state)
   368  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   369  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   370  
   371  	// Call SetAllocHealth
   372  	req := &structs.DeploymentAllocHealthRequest{
   373  		DeploymentID:           d.ID,
   374  		UnhealthyAllocationIDs: []string{a.ID},
   375  	}
   376  	var resp structs.DeploymentUpdateResponse
   377  	err := w.SetAllocHealth(req, &resp)
   378  	assert.Nil(err, "SetAllocHealth")
   379  
   380  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   381  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   382  	m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1)
   383  }
   384  
   385  // Test promoting a deployment
   386  func TestWatcher_PromoteDeployment_HealthyCanaries(t *testing.T) {
   387  	t.Parallel()
   388  	assert := assert.New(t)
   389  	w, m := defaultTestDeploymentWatcher(t)
   390  
   391  	// Create a job, canary alloc, and a deployment
   392  	j := mock.Job()
   393  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   394  	j.TaskGroups[0].Update.MaxParallel = 2
   395  	j.TaskGroups[0].Update.Canary = 1
   396  	j.TaskGroups[0].Update.ProgressDeadline = 0
   397  	d := mock.Deployment()
   398  	d.JobID = j.ID
   399  	a := mock.Alloc()
   400  	d.TaskGroups[a.TaskGroup].DesiredCanaries = 1
   401  	d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID}
   402  	a.DeploymentStatus = &structs.AllocDeploymentStatus{
   403  		Healthy: helper.BoolToPtr(true),
   404  	}
   405  	a.DeploymentID = d.ID
   406  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   407  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   408  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   409  
   410  	// Assert that we get a call to UpsertDeploymentPromotion
   411  	matchConfig := &matchDeploymentPromoteRequestConfig{
   412  		Promotion: &structs.DeploymentPromoteRequest{
   413  			DeploymentID: d.ID,
   414  			All:          true,
   415  		},
   416  		Eval: true,
   417  	}
   418  	matcher := matchDeploymentPromoteRequest(matchConfig)
   419  	m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil)
   420  
   421  	// We may get an update for the desired transition.
   422  	m1 := matchUpdateAllocDesiredTransitions([]string{d.ID})
   423  	m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once()
   424  
   425  	w.SetEnabled(true, m.state)
   426  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   427  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   428  
   429  	// Call PromoteDeployment
   430  	req := &structs.DeploymentPromoteRequest{
   431  		DeploymentID: d.ID,
   432  		All:          true,
   433  	}
   434  	var resp structs.DeploymentUpdateResponse
   435  	err := w.PromoteDeployment(req, &resp)
   436  	assert.Nil(err, "PromoteDeployment")
   437  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   438  	m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher))
   439  }
   440  
   441  // Test promoting a deployment with unhealthy canaries
   442  func TestWatcher_PromoteDeployment_UnhealthyCanaries(t *testing.T) {
   443  	t.Parallel()
   444  	assert := assert.New(t)
   445  	w, m := defaultTestDeploymentWatcher(t)
   446  
   447  	// Create a job, canary alloc, and a deployment
   448  	j := mock.Job()
   449  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   450  	j.TaskGroups[0].Update.MaxParallel = 2
   451  	j.TaskGroups[0].Update.Canary = 2
   452  	j.TaskGroups[0].Update.ProgressDeadline = 0
   453  	d := mock.Deployment()
   454  	d.JobID = j.ID
   455  	a := mock.Alloc()
   456  	d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID}
   457  	d.TaskGroups[a.TaskGroup].DesiredCanaries = 2
   458  	a.DeploymentID = d.ID
   459  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   460  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   461  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   462  
   463  	// Assert that we get a call to UpsertDeploymentPromotion
   464  	matchConfig := &matchDeploymentPromoteRequestConfig{
   465  		Promotion: &structs.DeploymentPromoteRequest{
   466  			DeploymentID: d.ID,
   467  			All:          true,
   468  		},
   469  		Eval: true,
   470  	}
   471  	matcher := matchDeploymentPromoteRequest(matchConfig)
   472  	m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil)
   473  
   474  	w.SetEnabled(true, m.state)
   475  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   476  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   477  
   478  	// Call SetAllocHealth
   479  	req := &structs.DeploymentPromoteRequest{
   480  		DeploymentID: d.ID,
   481  		All:          true,
   482  	}
   483  	var resp structs.DeploymentUpdateResponse
   484  	err := w.PromoteDeployment(req, &resp)
   485  	if assert.NotNil(err, "PromoteDeployment") {
   486  		assert.Contains(err.Error(), `Task group "web" has 0/2 healthy allocations`, "Should error because canary isn't marked healthy")
   487  	}
   488  
   489  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   490  	m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher))
   491  }
   492  
   493  // Test pausing a deployment that is running
   494  func TestWatcher_PauseDeployment_Pause_Running(t *testing.T) {
   495  	t.Parallel()
   496  	assert := assert.New(t)
   497  	w, m := defaultTestDeploymentWatcher(t)
   498  
   499  	// Create a job and a deployment
   500  	j := mock.Job()
   501  	d := mock.Deployment()
   502  	d.JobID = j.ID
   503  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   504  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   505  
   506  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   507  	matchConfig := &matchDeploymentStatusUpdateConfig{
   508  		DeploymentID:      d.ID,
   509  		Status:            structs.DeploymentStatusPaused,
   510  		StatusDescription: structs.DeploymentStatusDescriptionPaused,
   511  	}
   512  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   513  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   514  
   515  	w.SetEnabled(true, m.state)
   516  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   517  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   518  
   519  	// Call PauseDeployment
   520  	req := &structs.DeploymentPauseRequest{
   521  		DeploymentID: d.ID,
   522  		Pause:        true,
   523  	}
   524  	var resp structs.DeploymentUpdateResponse
   525  	err := w.PauseDeployment(req, &resp)
   526  	assert.Nil(err, "PauseDeployment")
   527  
   528  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   529  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   530  }
   531  
   532  // Test pausing a deployment that is paused
   533  func TestWatcher_PauseDeployment_Pause_Paused(t *testing.T) {
   534  	t.Parallel()
   535  	assert := assert.New(t)
   536  	w, m := defaultTestDeploymentWatcher(t)
   537  
   538  	// Create a job and a deployment
   539  	j := mock.Job()
   540  	d := mock.Deployment()
   541  	d.JobID = j.ID
   542  	d.Status = structs.DeploymentStatusPaused
   543  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   544  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   545  
   546  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   547  	matchConfig := &matchDeploymentStatusUpdateConfig{
   548  		DeploymentID:      d.ID,
   549  		Status:            structs.DeploymentStatusPaused,
   550  		StatusDescription: structs.DeploymentStatusDescriptionPaused,
   551  	}
   552  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   553  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   554  
   555  	w.SetEnabled(true, m.state)
   556  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   557  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   558  
   559  	// Call PauseDeployment
   560  	req := &structs.DeploymentPauseRequest{
   561  		DeploymentID: d.ID,
   562  		Pause:        true,
   563  	}
   564  	var resp structs.DeploymentUpdateResponse
   565  	err := w.PauseDeployment(req, &resp)
   566  	assert.Nil(err, "PauseDeployment")
   567  
   568  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   569  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   570  }
   571  
   572  // Test unpausing a deployment that is paused
   573  func TestWatcher_PauseDeployment_Unpause_Paused(t *testing.T) {
   574  	t.Parallel()
   575  	assert := assert.New(t)
   576  	w, m := defaultTestDeploymentWatcher(t)
   577  
   578  	// Create a job and a deployment
   579  	j := mock.Job()
   580  	d := mock.Deployment()
   581  	d.JobID = j.ID
   582  	d.Status = structs.DeploymentStatusPaused
   583  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   584  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   585  
   586  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   587  	matchConfig := &matchDeploymentStatusUpdateConfig{
   588  		DeploymentID:      d.ID,
   589  		Status:            structs.DeploymentStatusRunning,
   590  		StatusDescription: structs.DeploymentStatusDescriptionRunning,
   591  		Eval:              true,
   592  	}
   593  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   594  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   595  
   596  	w.SetEnabled(true, m.state)
   597  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   598  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   599  
   600  	// Call PauseDeployment
   601  	req := &structs.DeploymentPauseRequest{
   602  		DeploymentID: d.ID,
   603  		Pause:        false,
   604  	}
   605  	var resp structs.DeploymentUpdateResponse
   606  	err := w.PauseDeployment(req, &resp)
   607  	assert.Nil(err, "PauseDeployment")
   608  
   609  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   610  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   611  }
   612  
   613  // Test unpausing a deployment that is running
   614  func TestWatcher_PauseDeployment_Unpause_Running(t *testing.T) {
   615  	t.Parallel()
   616  	assert := assert.New(t)
   617  	w, m := defaultTestDeploymentWatcher(t)
   618  
   619  	// Create a job and a deployment
   620  	j := mock.Job()
   621  	d := mock.Deployment()
   622  	d.JobID = j.ID
   623  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   624  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   625  
   626  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   627  	matchConfig := &matchDeploymentStatusUpdateConfig{
   628  		DeploymentID:      d.ID,
   629  		Status:            structs.DeploymentStatusRunning,
   630  		StatusDescription: structs.DeploymentStatusDescriptionRunning,
   631  		Eval:              true,
   632  	}
   633  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   634  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   635  
   636  	w.SetEnabled(true, m.state)
   637  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   638  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   639  
   640  	// Call PauseDeployment
   641  	req := &structs.DeploymentPauseRequest{
   642  		DeploymentID: d.ID,
   643  		Pause:        false,
   644  	}
   645  	var resp structs.DeploymentUpdateResponse
   646  	err := w.PauseDeployment(req, &resp)
   647  	assert.Nil(err, "PauseDeployment")
   648  
   649  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   650  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   651  }
   652  
   653  // Test failing a deployment that is running
   654  func TestWatcher_FailDeployment_Running(t *testing.T) {
   655  	t.Parallel()
   656  	assert := assert.New(t)
   657  	w, m := defaultTestDeploymentWatcher(t)
   658  
   659  	// Create a job and a deployment
   660  	j := mock.Job()
   661  	d := mock.Deployment()
   662  	d.JobID = j.ID
   663  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   664  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   665  
   666  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   667  	matchConfig := &matchDeploymentStatusUpdateConfig{
   668  		DeploymentID:      d.ID,
   669  		Status:            structs.DeploymentStatusFailed,
   670  		StatusDescription: structs.DeploymentStatusDescriptionFailedByUser,
   671  		Eval:              true,
   672  	}
   673  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   674  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   675  
   676  	w.SetEnabled(true, m.state)
   677  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   678  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   679  
   680  	// Call PauseDeployment
   681  	req := &structs.DeploymentFailRequest{
   682  		DeploymentID: d.ID,
   683  	}
   684  	var resp structs.DeploymentUpdateResponse
   685  	err := w.FailDeployment(req, &resp)
   686  	assert.Nil(err, "FailDeployment")
   687  
   688  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   689  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   690  }
   691  
   692  // Tests that the watcher properly watches for allocation changes and takes the
   693  // proper actions
   694  func TestDeploymentWatcher_Watch_NoProgressDeadline(t *testing.T) {
   695  	t.Parallel()
   696  	assert := assert.New(t)
   697  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
   698  
   699  	// Create a job, alloc, and a deployment
   700  	j := mock.Job()
   701  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   702  	j.TaskGroups[0].Update.MaxParallel = 2
   703  	j.TaskGroups[0].Update.AutoRevert = true
   704  	j.TaskGroups[0].Update.ProgressDeadline = 0
   705  	j.Stable = true
   706  	d := mock.Deployment()
   707  	d.JobID = j.ID
   708  	d.TaskGroups["web"].AutoRevert = true
   709  	a := mock.Alloc()
   710  	a.DeploymentID = d.ID
   711  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   712  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   713  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   714  
   715  	// Upsert the job again to get a new version
   716  	j2 := j.Copy()
   717  	// Modify the job to make its specification different
   718  	j2.Meta["foo"] = "bar"
   719  	j2.Stable = false
   720  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
   721  
   722  	// Assert that we will get a update allocation call only once. This will
   723  	// verify that the watcher is batching allocation changes
   724  	m1 := matchUpdateAllocDesiredTransitions([]string{d.ID})
   725  	m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once()
   726  
   727  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   728  	c := &matchDeploymentStatusUpdateConfig{
   729  		DeploymentID:      d.ID,
   730  		Status:            structs.DeploymentStatusFailed,
   731  		StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0),
   732  		JobVersion:        helper.Uint64ToPtr(0),
   733  		Eval:              true,
   734  	}
   735  	m2 := matchDeploymentStatusUpdateRequest(c)
   736  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil)
   737  
   738  	w.SetEnabled(true, m.state)
   739  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   740  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   741  
   742  	// Update the allocs health to healthy which should create an evaluation
   743  	for i := 0; i < 5; i++ {
   744  		req := &structs.ApplyDeploymentAllocHealthRequest{
   745  			DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   746  				DeploymentID:         d.ID,
   747  				HealthyAllocationIDs: []string{a.ID},
   748  			},
   749  		}
   750  		assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth")
   751  	}
   752  
   753  	// Wait for there to be one eval
   754  	testutil.WaitForResult(func() (bool, error) {
   755  		ws := memdb.NewWatchSet()
   756  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   757  		if err != nil {
   758  			return false, err
   759  		}
   760  
   761  		if l := len(evals); l != 1 {
   762  			return false, fmt.Errorf("Got %d evals; want 1", l)
   763  		}
   764  
   765  		return true, nil
   766  	}, func(err error) {
   767  		t.Fatal(err)
   768  	})
   769  
   770  	// Update the allocs health to unhealthy which should create a job rollback,
   771  	// status update and eval
   772  	req2 := &structs.ApplyDeploymentAllocHealthRequest{
   773  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   774  			DeploymentID:           d.ID,
   775  			UnhealthyAllocationIDs: []string{a.ID},
   776  		},
   777  	}
   778  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth")
   779  
   780  	// Wait for there to be one eval
   781  	testutil.WaitForResult(func() (bool, error) {
   782  		ws := memdb.NewWatchSet()
   783  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   784  		if err != nil {
   785  			return false, err
   786  		}
   787  
   788  		if l := len(evals); l != 2 {
   789  			return false, fmt.Errorf("Got %d evals; want 1", l)
   790  		}
   791  
   792  		return true, nil
   793  	}, func(err error) {
   794  		t.Fatal(err)
   795  	})
   796  
   797  	m.AssertCalled(t, "UpdateAllocDesiredTransition", mocker.MatchedBy(m1))
   798  
   799  	// After we upsert the job version will go to 2. So use this to assert the
   800  	// original call happened.
   801  	c2 := &matchDeploymentStatusUpdateConfig{
   802  		DeploymentID:      d.ID,
   803  		Status:            structs.DeploymentStatusFailed,
   804  		StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0),
   805  		JobVersion:        helper.Uint64ToPtr(2),
   806  		Eval:              true,
   807  	}
   808  	m3 := matchDeploymentStatusUpdateRequest(c2)
   809  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(m3))
   810  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   811  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   812  }
   813  
   814  func TestDeploymentWatcher_Watch_ProgressDeadline(t *testing.T) {
   815  	t.Parallel()
   816  	assert := assert.New(t)
   817  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
   818  
   819  	// Create a job, alloc, and a deployment
   820  	j := mock.Job()
   821  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   822  	j.TaskGroups[0].Update.MaxParallel = 2
   823  	j.TaskGroups[0].Update.ProgressDeadline = 500 * time.Millisecond
   824  	j.Stable = true
   825  	d := mock.Deployment()
   826  	d.JobID = j.ID
   827  	d.TaskGroups["web"].ProgressDeadline = 500 * time.Millisecond
   828  	a := mock.Alloc()
   829  	now := time.Now()
   830  	a.CreateTime = now.UnixNano()
   831  	a.ModifyTime = now.UnixNano()
   832  	a.DeploymentID = d.ID
   833  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   834  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   835  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   836  
   837  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   838  	c := &matchDeploymentStatusUpdateConfig{
   839  		DeploymentID:      d.ID,
   840  		Status:            structs.DeploymentStatusFailed,
   841  		StatusDescription: structs.DeploymentStatusDescriptionProgressDeadline,
   842  		Eval:              true,
   843  	}
   844  	m2 := matchDeploymentStatusUpdateRequest(c)
   845  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil)
   846  
   847  	w.SetEnabled(true, m.state)
   848  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   849  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   850  
   851  	// Update the alloc to be unhealthy and assert that nothing happens.
   852  	a2 := a.Copy()
   853  	a2.DeploymentStatus = &structs.AllocDeploymentStatus{
   854  		Healthy:   helper.BoolToPtr(false),
   855  		Timestamp: now,
   856  	}
   857  	assert.Nil(m.state.UpdateAllocsFromClient(100, []*structs.Allocation{a2}))
   858  
   859  	// Wait for the deployment to be failed
   860  	testutil.WaitForResult(func() (bool, error) {
   861  		d, err := m.state.DeploymentByID(nil, d.ID)
   862  		if err != nil {
   863  			return false, err
   864  		}
   865  
   866  		return d.Status == structs.DeploymentStatusFailed, fmt.Errorf("bad status %q", d.Status)
   867  	}, func(err error) {
   868  		t.Fatal(err)
   869  	})
   870  
   871  	// Assert there are is only one evaluation
   872  	testutil.WaitForResult(func() (bool, error) {
   873  		ws := memdb.NewWatchSet()
   874  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   875  		if err != nil {
   876  			return false, err
   877  		}
   878  
   879  		if l := len(evals); l != 1 {
   880  			return false, fmt.Errorf("Got %d evals; want 1", l)
   881  		}
   882  
   883  		return true, nil
   884  	}, func(err error) {
   885  		t.Fatal(err)
   886  	})
   887  }
   888  
   889  // Test that we will allow the progress deadline to be reached when the canaries
   890  // are healthy but we haven't promoted
   891  func TestDeploymentWatcher_Watch_ProgressDeadline_Canaries(t *testing.T) {
   892  	t.Parallel()
   893  	require := require.New(t)
   894  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
   895  
   896  	// Create a job, alloc, and a deployment
   897  	j := mock.Job()
   898  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   899  	j.TaskGroups[0].Update.Canary = 1
   900  	j.TaskGroups[0].Update.MaxParallel = 1
   901  	j.TaskGroups[0].Update.ProgressDeadline = 500 * time.Millisecond
   902  	j.Stable = true
   903  	d := mock.Deployment()
   904  	d.StatusDescription = structs.DeploymentStatusDescriptionRunningNeedsPromotion
   905  	d.JobID = j.ID
   906  	d.TaskGroups["web"].ProgressDeadline = 500 * time.Millisecond
   907  	d.TaskGroups["web"].DesiredCanaries = 1
   908  	a := mock.Alloc()
   909  	now := time.Now()
   910  	a.CreateTime = now.UnixNano()
   911  	a.ModifyTime = now.UnixNano()
   912  	a.DeploymentID = d.ID
   913  	require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   914  	require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   915  	require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   916  
   917  	// Assert that we will get a createEvaluation call only once. This will
   918  	// verify that the watcher is batching allocation changes
   919  	m1 := matchUpdateAllocDesiredTransitions([]string{d.ID})
   920  	m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once()
   921  
   922  	w.SetEnabled(true, m.state)
   923  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   924  		func(err error) { require.Equal(1, len(w.watchers), "Should have 1 deployment") })
   925  
   926  	// Update the alloc to be unhealthy and require that nothing happens.
   927  	a2 := a.Copy()
   928  	a2.DeploymentStatus = &structs.AllocDeploymentStatus{
   929  		Healthy:   helper.BoolToPtr(true),
   930  		Timestamp: now,
   931  	}
   932  	require.Nil(m.state.UpdateAllocsFromClient(m.nextIndex(), []*structs.Allocation{a2}))
   933  
   934  	// Wait for the deployment to cross the deadline
   935  	dout, err := m.state.DeploymentByID(nil, d.ID)
   936  	require.NoError(err)
   937  	require.NotNil(dout)
   938  	state := dout.TaskGroups["web"]
   939  	require.NotNil(state)
   940  	time.Sleep(state.RequireProgressBy.Add(time.Second).Sub(now))
   941  
   942  	// Require the deployment is still running
   943  	dout, err = m.state.DeploymentByID(nil, d.ID)
   944  	require.NoError(err)
   945  	require.NotNil(dout)
   946  	require.Equal(structs.DeploymentStatusRunning, dout.Status)
   947  	require.Equal(structs.DeploymentStatusDescriptionRunningNeedsPromotion, dout.StatusDescription)
   948  
   949  	// require there are is only one evaluation
   950  	testutil.WaitForResult(func() (bool, error) {
   951  		ws := memdb.NewWatchSet()
   952  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   953  		if err != nil {
   954  			return false, err
   955  		}
   956  
   957  		if l := len(evals); l != 1 {
   958  			return false, fmt.Errorf("Got %d evals; want 1", l)
   959  		}
   960  
   961  		return true, nil
   962  	}, func(err error) {
   963  		t.Fatal(err)
   964  	})
   965  }
   966  
   967  // Test that a promoted deployment with alloc healthy updates create
   968  // evals to move the deployment forward
   969  func TestDeploymentWatcher_PromotedCanary_UpdatedAllocs(t *testing.T) {
   970  	t.Parallel()
   971  	require := require.New(t)
   972  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
   973  
   974  	// Create a job, alloc, and a deployment
   975  	j := mock.Job()
   976  	j.TaskGroups[0].Count = 2
   977  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   978  	j.TaskGroups[0].Update.Canary = 1
   979  	j.TaskGroups[0].Update.MaxParallel = 1
   980  	j.TaskGroups[0].Update.ProgressDeadline = 50 * time.Millisecond
   981  	j.Stable = true
   982  
   983  	d := mock.Deployment()
   984  	d.TaskGroups["web"].DesiredTotal = 2
   985  	d.TaskGroups["web"].DesiredCanaries = 1
   986  	d.TaskGroups["web"].HealthyAllocs = 1
   987  	d.StatusDescription = structs.DeploymentStatusDescriptionRunning
   988  	d.JobID = j.ID
   989  	d.TaskGroups["web"].ProgressDeadline = 50 * time.Millisecond
   990  	d.TaskGroups["web"].RequireProgressBy = time.Now().Add(50 * time.Millisecond)
   991  
   992  	a := mock.Alloc()
   993  	now := time.Now()
   994  	a.CreateTime = now.UnixNano()
   995  	a.ModifyTime = now.UnixNano()
   996  	a.DeploymentID = d.ID
   997  	a.DeploymentStatus = &structs.AllocDeploymentStatus{
   998  		Healthy:   helper.BoolToPtr(true),
   999  		Timestamp: now,
  1000  	}
  1001  	require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
  1002  	require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
  1003  	require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
  1004  
  1005  	w.SetEnabled(true, m.state)
  1006  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
  1007  		func(err error) { require.Equal(1, len(w.watchers), "Should have 1 deployment") })
  1008  
  1009  	m1 := matchUpdateAllocDesiredTransitions([]string{d.ID})
  1010  	m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Twice()
  1011  
  1012  	// Create another alloc
  1013  	a2 := a.Copy()
  1014  	a2.ID = uuid.Generate()
  1015  	now = time.Now()
  1016  	a2.CreateTime = now.UnixNano()
  1017  	a2.ModifyTime = now.UnixNano()
  1018  	a2.DeploymentStatus = &structs.AllocDeploymentStatus{
  1019  		Healthy:   helper.BoolToPtr(true),
  1020  		Timestamp: now,
  1021  	}
  1022  	d.TaskGroups["web"].RequireProgressBy = time.Now().Add(2 * time.Second)
  1023  	require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
  1024  	// Wait until batch eval period passes before updating another alloc
  1025  	time.Sleep(1 * time.Second)
  1026  	require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a2}), "UpsertAllocs")
  1027  
  1028  	// Wait for the deployment to cross the deadline
  1029  	dout, err := m.state.DeploymentByID(nil, d.ID)
  1030  	require.NoError(err)
  1031  	require.NotNil(dout)
  1032  	state := dout.TaskGroups["web"]
  1033  	require.NotNil(state)
  1034  	time.Sleep(state.RequireProgressBy.Add(time.Second).Sub(now))
  1035  
  1036  	// There should be two evals
  1037  	testutil.WaitForResult(func() (bool, error) {
  1038  		ws := memdb.NewWatchSet()
  1039  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
  1040  		if err != nil {
  1041  			return false, err
  1042  		}
  1043  
  1044  		if l := len(evals); l != 2 {
  1045  			return false, fmt.Errorf("Got %d evals; want 2", l)
  1046  		}
  1047  
  1048  		return true, nil
  1049  	}, func(err error) {
  1050  		t.Fatal(err)
  1051  	})
  1052  }
  1053  
  1054  // Test scenario where deployment initially has no progress deadline
  1055  // After the deployment is updated, a failed alloc's DesiredTransition should be set
  1056  func TestDeploymentWatcher_Watch_StartWithoutProgressDeadline(t *testing.T) {
  1057  	t.Parallel()
  1058  	assert := assert.New(t)
  1059  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
  1060  
  1061  	// Create a job, and a deployment
  1062  	j := mock.Job()
  1063  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
  1064  	j.TaskGroups[0].Update.MaxParallel = 2
  1065  	j.TaskGroups[0].Update.ProgressDeadline = 500 * time.Millisecond
  1066  	j.Stable = true
  1067  	d := mock.Deployment()
  1068  	d.JobID = j.ID
  1069  
  1070  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
  1071  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
  1072  
  1073  	a := mock.Alloc()
  1074  	a.CreateTime = time.Now().UnixNano()
  1075  	a.DeploymentID = d.ID
  1076  
  1077  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
  1078  
  1079  	d.TaskGroups["web"].ProgressDeadline = 500 * time.Millisecond
  1080  	// Update the deployment with a progress deadline
  1081  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
  1082  
  1083  	// Match on DesiredTransition set to Reschedule for the failed alloc
  1084  	m1 := matchUpdateAllocDesiredTransitionReschedule([]string{a.ID})
  1085  	m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once()
  1086  
  1087  	w.SetEnabled(true, m.state)
  1088  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
  1089  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
  1090  
  1091  	// Update the alloc to be unhealthy
  1092  	a2 := a.Copy()
  1093  	a2.DeploymentStatus = &structs.AllocDeploymentStatus{
  1094  		Healthy:   helper.BoolToPtr(false),
  1095  		Timestamp: time.Now(),
  1096  	}
  1097  	assert.Nil(m.state.UpdateAllocsFromClient(m.nextIndex(), []*structs.Allocation{a2}))
  1098  
  1099  	// Wait for the alloc's DesiredState to set reschedule
  1100  	testutil.WaitForResult(func() (bool, error) {
  1101  		a, err := m.state.AllocByID(nil, a.ID)
  1102  		if err != nil {
  1103  			return false, err
  1104  		}
  1105  		dt := a.DesiredTransition
  1106  		shouldReschedule := dt.Reschedule != nil && *dt.Reschedule
  1107  		return shouldReschedule, fmt.Errorf("Desired Transition Reschedule should be set but got %v", shouldReschedule)
  1108  	}, func(err error) {
  1109  		t.Fatal(err)
  1110  	})
  1111  }
  1112  
  1113  // Tests that the watcher fails rollback when the spec hasn't changed
  1114  func TestDeploymentWatcher_RollbackFailed(t *testing.T) {
  1115  	t.Parallel()
  1116  	assert := assert.New(t)
  1117  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
  1118  
  1119  	// Create a job, alloc, and a deployment
  1120  	j := mock.Job()
  1121  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
  1122  	j.TaskGroups[0].Update.MaxParallel = 2
  1123  	j.TaskGroups[0].Update.AutoRevert = true
  1124  	j.TaskGroups[0].Update.ProgressDeadline = 0
  1125  	j.Stable = true
  1126  	d := mock.Deployment()
  1127  	d.JobID = j.ID
  1128  	d.TaskGroups["web"].AutoRevert = true
  1129  	a := mock.Alloc()
  1130  	a.DeploymentID = d.ID
  1131  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
  1132  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
  1133  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
  1134  
  1135  	// Upsert the job again to get a new version
  1136  	j2 := j.Copy()
  1137  	// Modify the job to make its specification different
  1138  	j2.Stable = false
  1139  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
  1140  
  1141  	// Assert that we will get a createEvaluation call only once. This will
  1142  	// verify that the watcher is batching allocation changes
  1143  	m1 := matchUpdateAllocDesiredTransitions([]string{d.ID})
  1144  	m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once()
  1145  
  1146  	// Assert that we get a call to UpsertDeploymentStatusUpdate with roll back failed as the status
  1147  	c := &matchDeploymentStatusUpdateConfig{
  1148  		DeploymentID:      d.ID,
  1149  		Status:            structs.DeploymentStatusFailed,
  1150  		StatusDescription: structs.DeploymentStatusDescriptionRollbackNoop(structs.DeploymentStatusDescriptionFailedAllocations, 0),
  1151  		JobVersion:        nil,
  1152  		Eval:              true,
  1153  	}
  1154  	m2 := matchDeploymentStatusUpdateRequest(c)
  1155  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil)
  1156  
  1157  	w.SetEnabled(true, m.state)
  1158  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
  1159  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
  1160  
  1161  	// Update the allocs health to healthy which should create an evaluation
  1162  	for i := 0; i < 5; i++ {
  1163  		req := &structs.ApplyDeploymentAllocHealthRequest{
  1164  			DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
  1165  				DeploymentID:         d.ID,
  1166  				HealthyAllocationIDs: []string{a.ID},
  1167  			},
  1168  		}
  1169  		assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth")
  1170  	}
  1171  
  1172  	// Wait for there to be one eval
  1173  	testutil.WaitForResult(func() (bool, error) {
  1174  		ws := memdb.NewWatchSet()
  1175  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
  1176  		if err != nil {
  1177  			return false, err
  1178  		}
  1179  
  1180  		if l := len(evals); l != 1 {
  1181  			return false, fmt.Errorf("Got %d evals; want 1", l)
  1182  		}
  1183  
  1184  		return true, nil
  1185  	}, func(err error) {
  1186  		t.Fatal(err)
  1187  	})
  1188  
  1189  	// Update the allocs health to unhealthy which will cause attempting a rollback,
  1190  	// fail in that step, do status update and eval
  1191  	req2 := &structs.ApplyDeploymentAllocHealthRequest{
  1192  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
  1193  			DeploymentID:           d.ID,
  1194  			UnhealthyAllocationIDs: []string{a.ID},
  1195  		},
  1196  	}
  1197  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth")
  1198  
  1199  	// Wait for there to be one eval
  1200  	testutil.WaitForResult(func() (bool, error) {
  1201  		ws := memdb.NewWatchSet()
  1202  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
  1203  		if err != nil {
  1204  			return false, err
  1205  		}
  1206  
  1207  		if l := len(evals); l != 2 {
  1208  			return false, fmt.Errorf("Got %d evals; want 1", l)
  1209  		}
  1210  
  1211  		return true, nil
  1212  	}, func(err error) {
  1213  		t.Fatal(err)
  1214  	})
  1215  
  1216  	m.AssertCalled(t, "UpdateAllocDesiredTransition", mocker.MatchedBy(m1))
  1217  
  1218  	// verify that the job version hasn't changed after upsert
  1219  	m.state.JobByID(nil, structs.DefaultNamespace, j.ID)
  1220  	assert.Equal(uint64(0), j.Version, "Expected job version 0 but got ", j.Version)
  1221  }
  1222  
  1223  // Test allocation updates and evaluation creation is batched between watchers
  1224  func TestWatcher_BatchAllocUpdates(t *testing.T) {
  1225  	t.Parallel()
  1226  	assert := assert.New(t)
  1227  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Second)
  1228  
  1229  	// Create a job, alloc, for two deployments
  1230  	j1 := mock.Job()
  1231  	j1.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
  1232  	j1.TaskGroups[0].Update.ProgressDeadline = 0
  1233  	d1 := mock.Deployment()
  1234  	d1.JobID = j1.ID
  1235  	a1 := mock.Alloc()
  1236  	a1.Job = j1
  1237  	a1.JobID = j1.ID
  1238  	a1.DeploymentID = d1.ID
  1239  
  1240  	j2 := mock.Job()
  1241  	j2.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
  1242  	j2.TaskGroups[0].Update.ProgressDeadline = 0
  1243  	d2 := mock.Deployment()
  1244  	d2.JobID = j2.ID
  1245  	a2 := mock.Alloc()
  1246  	a2.Job = j2
  1247  	a2.JobID = j2.ID
  1248  	a2.DeploymentID = d2.ID
  1249  
  1250  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j1), "UpsertJob")
  1251  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob")
  1252  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d1), "UpsertDeployment")
  1253  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d2), "UpsertDeployment")
  1254  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a1}), "UpsertAllocs")
  1255  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a2}), "UpsertAllocs")
  1256  
  1257  	// Assert that we will get a createEvaluation call only once and it contains
  1258  	// both deployments. This will verify that the watcher is batching
  1259  	// allocation changes
  1260  	m1 := matchUpdateAllocDesiredTransitions([]string{d1.ID, d2.ID})
  1261  	m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once()
  1262  
  1263  	w.SetEnabled(true, m.state)
  1264  	testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil },
  1265  		func(err error) { assert.Equal(2, len(w.watchers), "Should have 2 deployment") })
  1266  
  1267  	// Update the allocs health to healthy which should create an evaluation
  1268  	req := &structs.ApplyDeploymentAllocHealthRequest{
  1269  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
  1270  			DeploymentID:         d1.ID,
  1271  			HealthyAllocationIDs: []string{a1.ID},
  1272  		},
  1273  	}
  1274  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth")
  1275  
  1276  	req2 := &structs.ApplyDeploymentAllocHealthRequest{
  1277  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
  1278  			DeploymentID:         d2.ID,
  1279  			HealthyAllocationIDs: []string{a2.ID},
  1280  		},
  1281  	}
  1282  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth")
  1283  
  1284  	// Wait for there to be one eval for each job
  1285  	testutil.WaitForResult(func() (bool, error) {
  1286  		ws := memdb.NewWatchSet()
  1287  		evals1, err := m.state.EvalsByJob(ws, j1.Namespace, j1.ID)
  1288  		if err != nil {
  1289  			return false, err
  1290  		}
  1291  
  1292  		evals2, err := m.state.EvalsByJob(ws, j2.Namespace, j2.ID)
  1293  		if err != nil {
  1294  			return false, err
  1295  		}
  1296  
  1297  		if l := len(evals1); l != 1 {
  1298  			return false, fmt.Errorf("Got %d evals for job %v; want 1", l, j1.ID)
  1299  		}
  1300  
  1301  		if l := len(evals2); l != 1 {
  1302  			return false, fmt.Errorf("Got %d evals for job 2; want 1", l)
  1303  		}
  1304  
  1305  		return true, nil
  1306  	}, func(err error) {
  1307  		t.Fatal(err)
  1308  	})
  1309  
  1310  	m.AssertCalled(t, "UpdateAllocDesiredTransition", mocker.MatchedBy(m1))
  1311  	testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil },
  1312  		func(err error) { assert.Equal(2, len(w.watchers), "Should have 2 deployment") })
  1313  }