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