github.com/emate/nomad@v0.8.2-wo-binpacking/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  )
    17  
    18  func testDeploymentWatcher(t *testing.T, qps float64, batchDur time.Duration) (*Watcher, *mockBackend) {
    19  	m := newMockBackend(t)
    20  	w := NewDeploymentsWatcher(testLogger(), m, qps, batchDur)
    21  	return w, m
    22  }
    23  
    24  func defaultTestDeploymentWatcher(t *testing.T) (*Watcher, *mockBackend) {
    25  	return testDeploymentWatcher(t, LimitStateQueriesPerSecond, CrossDeploymentEvalBatchDuration)
    26  }
    27  
    28  // Tests that the watcher properly watches for deployments and reconciles them
    29  func TestWatcher_WatchDeployments(t *testing.T) {
    30  	t.Parallel()
    31  	assert := assert.New(t)
    32  	w, m := defaultTestDeploymentWatcher(t)
    33  
    34  	// Create three jobs
    35  	j1, j2, j3 := mock.Job(), mock.Job(), mock.Job()
    36  	assert.Nil(m.state.UpsertJob(100, j1))
    37  	assert.Nil(m.state.UpsertJob(101, j2))
    38  	assert.Nil(m.state.UpsertJob(102, j3))
    39  
    40  	// Create three deployments all running
    41  	d1, d2, d3 := mock.Deployment(), mock.Deployment(), mock.Deployment()
    42  	d1.JobID = j1.ID
    43  	d2.JobID = j2.ID
    44  	d3.JobID = j3.ID
    45  
    46  	// Upsert the first deployment
    47  	assert.Nil(m.state.UpsertDeployment(103, d1))
    48  
    49  	// Next list 3
    50  	block1 := make(chan time.Time)
    51  	go func() {
    52  		<-block1
    53  		assert.Nil(m.state.UpsertDeployment(104, d2))
    54  		assert.Nil(m.state.UpsertDeployment(105, d3))
    55  	}()
    56  
    57  	//// Next list 3 but have one be terminal
    58  	block2 := make(chan time.Time)
    59  	d3terminal := d3.Copy()
    60  	d3terminal.Status = structs.DeploymentStatusFailed
    61  	go func() {
    62  		<-block2
    63  		assert.Nil(m.state.UpsertDeployment(106, d3terminal))
    64  	}()
    65  
    66  	w.SetEnabled(true, m.state)
    67  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
    68  		func(err error) { assert.Equal(1, len(w.watchers), "1 deployment returned") })
    69  
    70  	close(block1)
    71  	testutil.WaitForResult(func() (bool, error) { return 3 == len(w.watchers), nil },
    72  		func(err error) { assert.Equal(3, len(w.watchers), "3 deployment returned") })
    73  
    74  	close(block2)
    75  	testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil },
    76  		func(err error) { assert.Equal(3, len(w.watchers), "3 deployment returned - 1 terminal") })
    77  }
    78  
    79  // Tests that calls against an unknown deployment fail
    80  func TestWatcher_UnknownDeployment(t *testing.T) {
    81  	t.Parallel()
    82  	assert := assert.New(t)
    83  	w, m := defaultTestDeploymentWatcher(t)
    84  	w.SetEnabled(true, m.state)
    85  
    86  	// The expected error is that it should be an unknown deployment
    87  	dID := uuid.Generate()
    88  	expected := fmt.Sprintf("unknown deployment %q", dID)
    89  
    90  	// Request setting the health against an unknown deployment
    91  	req := &structs.DeploymentAllocHealthRequest{
    92  		DeploymentID:         dID,
    93  		HealthyAllocationIDs: []string{uuid.Generate()},
    94  	}
    95  	var resp structs.DeploymentUpdateResponse
    96  	err := w.SetAllocHealth(req, &resp)
    97  	if assert.NotNil(err, "should have error for unknown deployment") {
    98  		assert.Contains(err.Error(), expected)
    99  	}
   100  
   101  	// Request promoting against an unknown deployment
   102  	req2 := &structs.DeploymentPromoteRequest{
   103  		DeploymentID: dID,
   104  		All:          true,
   105  	}
   106  	err = w.PromoteDeployment(req2, &resp)
   107  	if assert.NotNil(err, "should have error for unknown deployment") {
   108  		assert.Contains(err.Error(), expected)
   109  	}
   110  
   111  	// Request pausing against an unknown deployment
   112  	req3 := &structs.DeploymentPauseRequest{
   113  		DeploymentID: dID,
   114  		Pause:        true,
   115  	}
   116  	err = w.PauseDeployment(req3, &resp)
   117  	if assert.NotNil(err, "should have error for unknown deployment") {
   118  		assert.Contains(err.Error(), expected)
   119  	}
   120  
   121  	// Request failing against an unknown deployment
   122  	req4 := &structs.DeploymentFailRequest{
   123  		DeploymentID: dID,
   124  	}
   125  	err = w.FailDeployment(req4, &resp)
   126  	if assert.NotNil(err, "should have error for unknown deployment") {
   127  		assert.Contains(err.Error(), expected)
   128  	}
   129  }
   130  
   131  // Test setting an unknown allocation's health
   132  func TestWatcher_SetAllocHealth_Unknown(t *testing.T) {
   133  	t.Parallel()
   134  	assert := assert.New(t)
   135  	w, m := defaultTestDeploymentWatcher(t)
   136  
   137  	// Create a job, and a deployment
   138  	j := mock.Job()
   139  	d := mock.Deployment()
   140  	d.JobID = j.ID
   141  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   142  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   143  
   144  	w.SetEnabled(true, m.state)
   145  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   146  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   147  
   148  	// Assert that we get a call to UpsertDeploymentAllocHealth
   149  	a := mock.Alloc()
   150  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   151  		DeploymentID: d.ID,
   152  		Healthy:      []string{a.ID},
   153  		Eval:         true,
   154  	}
   155  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   156  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   157  
   158  	// Call SetAllocHealth
   159  	req := &structs.DeploymentAllocHealthRequest{
   160  		DeploymentID:         d.ID,
   161  		HealthyAllocationIDs: []string{a.ID},
   162  	}
   163  	var resp structs.DeploymentUpdateResponse
   164  	err := w.SetAllocHealth(req, &resp)
   165  	if assert.NotNil(err, "Set health of unknown allocation") {
   166  		assert.Contains(err.Error(), "unknown")
   167  	}
   168  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   169  }
   170  
   171  // Test setting allocation health
   172  func TestWatcher_SetAllocHealth_Healthy(t *testing.T) {
   173  	t.Parallel()
   174  	assert := assert.New(t)
   175  	w, m := defaultTestDeploymentWatcher(t)
   176  
   177  	// Create a job, alloc, and a deployment
   178  	j := mock.Job()
   179  	d := mock.Deployment()
   180  	d.JobID = j.ID
   181  	a := mock.Alloc()
   182  	a.DeploymentID = d.ID
   183  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   184  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   185  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   186  
   187  	w.SetEnabled(true, m.state)
   188  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   189  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   190  
   191  	// Assert that we get a call to UpsertDeploymentAllocHealth
   192  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   193  		DeploymentID: d.ID,
   194  		Healthy:      []string{a.ID},
   195  		Eval:         true,
   196  	}
   197  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   198  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   199  
   200  	// Call SetAllocHealth
   201  	req := &structs.DeploymentAllocHealthRequest{
   202  		DeploymentID:         d.ID,
   203  		HealthyAllocationIDs: []string{a.ID},
   204  	}
   205  	var resp structs.DeploymentUpdateResponse
   206  	err := w.SetAllocHealth(req, &resp)
   207  	assert.Nil(err, "SetAllocHealth")
   208  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   209  	m.AssertCalled(t, "UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher))
   210  }
   211  
   212  // Test setting allocation unhealthy
   213  func TestWatcher_SetAllocHealth_Unhealthy(t *testing.T) {
   214  	t.Parallel()
   215  	assert := assert.New(t)
   216  	w, m := defaultTestDeploymentWatcher(t)
   217  
   218  	// Create a job, alloc, and a deployment
   219  	j := mock.Job()
   220  	d := mock.Deployment()
   221  	d.JobID = j.ID
   222  	a := mock.Alloc()
   223  	a.DeploymentID = d.ID
   224  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   225  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   226  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   227  
   228  	w.SetEnabled(true, m.state)
   229  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   230  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   231  
   232  	// Assert that we get a call to UpsertDeploymentAllocHealth
   233  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   234  		DeploymentID: d.ID,
   235  		Unhealthy:    []string{a.ID},
   236  		Eval:         true,
   237  		DeploymentUpdate: &structs.DeploymentStatusUpdate{
   238  			DeploymentID:      d.ID,
   239  			Status:            structs.DeploymentStatusFailed,
   240  			StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations,
   241  		},
   242  	}
   243  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   244  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   245  
   246  	// Call SetAllocHealth
   247  	req := &structs.DeploymentAllocHealthRequest{
   248  		DeploymentID:           d.ID,
   249  		UnhealthyAllocationIDs: []string{a.ID},
   250  	}
   251  	var resp structs.DeploymentUpdateResponse
   252  	err := w.SetAllocHealth(req, &resp)
   253  	assert.Nil(err, "SetAllocHealth")
   254  
   255  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   256  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   257  	m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1)
   258  }
   259  
   260  // Test setting allocation unhealthy and that there should be a rollback
   261  func TestWatcher_SetAllocHealth_Unhealthy_Rollback(t *testing.T) {
   262  	t.Parallel()
   263  	assert := assert.New(t)
   264  	w, m := defaultTestDeploymentWatcher(t)
   265  
   266  	// Create a job, alloc, and a deployment
   267  	j := mock.Job()
   268  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   269  	j.TaskGroups[0].Update.MaxParallel = 2
   270  	j.TaskGroups[0].Update.AutoRevert = true
   271  	j.Stable = true
   272  	d := mock.Deployment()
   273  	d.JobID = j.ID
   274  	d.TaskGroups["web"].AutoRevert = true
   275  	a := mock.Alloc()
   276  	a.DeploymentID = d.ID
   277  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   278  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   279  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   280  
   281  	// Upsert the job again to get a new version
   282  	j2 := j.Copy()
   283  	j2.Stable = false
   284  	// Modify the job to make its specification different
   285  	j2.Meta["foo"] = "bar"
   286  
   287  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
   288  
   289  	w.SetEnabled(true, m.state)
   290  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   291  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   292  
   293  	// Assert that we get a call to UpsertDeploymentAllocHealth
   294  	matchConfig := &matchDeploymentAllocHealthRequestConfig{
   295  		DeploymentID: d.ID,
   296  		Unhealthy:    []string{a.ID},
   297  		Eval:         true,
   298  		DeploymentUpdate: &structs.DeploymentStatusUpdate{
   299  			DeploymentID:      d.ID,
   300  			Status:            structs.DeploymentStatusFailed,
   301  			StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations,
   302  		},
   303  		JobVersion: helper.Uint64ToPtr(0),
   304  	}
   305  	matcher := matchDeploymentAllocHealthRequest(matchConfig)
   306  	m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil)
   307  
   308  	// Call SetAllocHealth
   309  	req := &structs.DeploymentAllocHealthRequest{
   310  		DeploymentID:           d.ID,
   311  		UnhealthyAllocationIDs: []string{a.ID},
   312  	}
   313  	var resp structs.DeploymentUpdateResponse
   314  	err := w.SetAllocHealth(req, &resp)
   315  	assert.Nil(err, "SetAllocHealth")
   316  
   317  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   318  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   319  	m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1)
   320  }
   321  
   322  // Test setting allocation unhealthy on job with identical spec and there should be no rollback
   323  func TestWatcher_SetAllocHealth_Unhealthy_NoRollback(t *testing.T) {
   324  	t.Parallel()
   325  	assert := assert.New(t)
   326  	w, m := defaultTestDeploymentWatcher(t)
   327  
   328  	// Create a job, alloc, and a deployment
   329  	j := mock.Job()
   330  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   331  	j.TaskGroups[0].Update.MaxParallel = 2
   332  	j.TaskGroups[0].Update.AutoRevert = true
   333  	j.Stable = true
   334  	d := mock.Deployment()
   335  	d.JobID = j.ID
   336  	d.TaskGroups["web"].AutoRevert = true
   337  	a := mock.Alloc()
   338  	a.DeploymentID = d.ID
   339  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   340  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   341  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   342  
   343  	// Upsert the job again to get a new version
   344  	j2 := j.Copy()
   345  	j2.Stable = false
   346  
   347  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
   348  
   349  	w.SetEnabled(true, m.state)
   350  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   351  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   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  	// Call SetAllocHealth
   369  	req := &structs.DeploymentAllocHealthRequest{
   370  		DeploymentID:           d.ID,
   371  		UnhealthyAllocationIDs: []string{a.ID},
   372  	}
   373  	var resp structs.DeploymentUpdateResponse
   374  	err := w.SetAllocHealth(req, &resp)
   375  	assert.Nil(err, "SetAllocHealth")
   376  
   377  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   378  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   379  	m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1)
   380  }
   381  
   382  // Test promoting a deployment
   383  func TestWatcher_PromoteDeployment_HealthyCanaries(t *testing.T) {
   384  	t.Parallel()
   385  	assert := assert.New(t)
   386  	w, m := defaultTestDeploymentWatcher(t)
   387  
   388  	// Create a job, canary alloc, and a deployment
   389  	j := mock.Job()
   390  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   391  	j.TaskGroups[0].Update.MaxParallel = 2
   392  	j.TaskGroups[0].Update.Canary = 2
   393  	d := mock.Deployment()
   394  	d.JobID = j.ID
   395  	a := mock.Alloc()
   396  	d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID}
   397  	a.DeploymentStatus = &structs.AllocDeploymentStatus{
   398  		Healthy: helper.BoolToPtr(true),
   399  	}
   400  	a.DeploymentID = d.ID
   401  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   402  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   403  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   404  
   405  	w.SetEnabled(true, m.state)
   406  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   407  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   408  
   409  	// Assert that we get a call to UpsertDeploymentPromotion
   410  	matchConfig := &matchDeploymentPromoteRequestConfig{
   411  		Promotion: &structs.DeploymentPromoteRequest{
   412  			DeploymentID: d.ID,
   413  			All:          true,
   414  		},
   415  		Eval: true,
   416  	}
   417  	matcher := matchDeploymentPromoteRequest(matchConfig)
   418  	m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil)
   419  
   420  	// Call PromoteDeployment
   421  	req := &structs.DeploymentPromoteRequest{
   422  		DeploymentID: d.ID,
   423  		All:          true,
   424  	}
   425  	var resp structs.DeploymentUpdateResponse
   426  	err := w.PromoteDeployment(req, &resp)
   427  	assert.Nil(err, "PromoteDeployment")
   428  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   429  	m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher))
   430  }
   431  
   432  // Test promoting a deployment with unhealthy canaries
   433  func TestWatcher_PromoteDeployment_UnhealthyCanaries(t *testing.T) {
   434  	t.Parallel()
   435  	assert := assert.New(t)
   436  	w, m := defaultTestDeploymentWatcher(t)
   437  
   438  	// Create a job, canary alloc, and a deployment
   439  	j := mock.Job()
   440  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   441  	j.TaskGroups[0].Update.MaxParallel = 2
   442  	j.TaskGroups[0].Update.Canary = 2
   443  	d := mock.Deployment()
   444  	d.JobID = j.ID
   445  	a := mock.Alloc()
   446  	d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID}
   447  	a.DeploymentID = d.ID
   448  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   449  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   450  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   451  
   452  	w.SetEnabled(true, m.state)
   453  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   454  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   455  
   456  	// Assert that we get a call to UpsertDeploymentPromotion
   457  	matchConfig := &matchDeploymentPromoteRequestConfig{
   458  		Promotion: &structs.DeploymentPromoteRequest{
   459  			DeploymentID: d.ID,
   460  			All:          true,
   461  		},
   462  		Eval: true,
   463  	}
   464  	matcher := matchDeploymentPromoteRequest(matchConfig)
   465  	m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil)
   466  
   467  	// Call SetAllocHealth
   468  	req := &structs.DeploymentPromoteRequest{
   469  		DeploymentID: d.ID,
   470  		All:          true,
   471  	}
   472  	var resp structs.DeploymentUpdateResponse
   473  	err := w.PromoteDeployment(req, &resp)
   474  	if assert.NotNil(err, "PromoteDeployment") {
   475  		assert.Contains(err.Error(), "is not healthy", "Should error because canary isn't marked healthy")
   476  	}
   477  
   478  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   479  	m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher))
   480  }
   481  
   482  // Test pausing a deployment that is running
   483  func TestWatcher_PauseDeployment_Pause_Running(t *testing.T) {
   484  	t.Parallel()
   485  	assert := assert.New(t)
   486  	w, m := defaultTestDeploymentWatcher(t)
   487  
   488  	// Create a job and a deployment
   489  	j := mock.Job()
   490  	d := mock.Deployment()
   491  	d.JobID = j.ID
   492  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   493  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   494  
   495  	w.SetEnabled(true, m.state)
   496  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   497  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   498  
   499  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   500  	matchConfig := &matchDeploymentStatusUpdateConfig{
   501  		DeploymentID:      d.ID,
   502  		Status:            structs.DeploymentStatusPaused,
   503  		StatusDescription: structs.DeploymentStatusDescriptionPaused,
   504  	}
   505  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   506  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   507  
   508  	// Call PauseDeployment
   509  	req := &structs.DeploymentPauseRequest{
   510  		DeploymentID: d.ID,
   511  		Pause:        true,
   512  	}
   513  	var resp structs.DeploymentUpdateResponse
   514  	err := w.PauseDeployment(req, &resp)
   515  	assert.Nil(err, "PauseDeployment")
   516  
   517  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   518  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   519  }
   520  
   521  // Test pausing a deployment that is paused
   522  func TestWatcher_PauseDeployment_Pause_Paused(t *testing.T) {
   523  	t.Parallel()
   524  	assert := assert.New(t)
   525  	w, m := defaultTestDeploymentWatcher(t)
   526  
   527  	// Create a job and a deployment
   528  	j := mock.Job()
   529  	d := mock.Deployment()
   530  	d.JobID = j.ID
   531  	d.Status = structs.DeploymentStatusPaused
   532  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   533  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   534  
   535  	w.SetEnabled(true, m.state)
   536  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   537  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   538  
   539  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   540  	matchConfig := &matchDeploymentStatusUpdateConfig{
   541  		DeploymentID:      d.ID,
   542  		Status:            structs.DeploymentStatusPaused,
   543  		StatusDescription: structs.DeploymentStatusDescriptionPaused,
   544  	}
   545  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   546  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   547  
   548  	// Call PauseDeployment
   549  	req := &structs.DeploymentPauseRequest{
   550  		DeploymentID: d.ID,
   551  		Pause:        true,
   552  	}
   553  	var resp structs.DeploymentUpdateResponse
   554  	err := w.PauseDeployment(req, &resp)
   555  	assert.Nil(err, "PauseDeployment")
   556  
   557  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   558  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   559  }
   560  
   561  // Test unpausing a deployment that is paused
   562  func TestWatcher_PauseDeployment_Unpause_Paused(t *testing.T) {
   563  	t.Parallel()
   564  	assert := assert.New(t)
   565  	w, m := defaultTestDeploymentWatcher(t)
   566  
   567  	// Create a job and a deployment
   568  	j := mock.Job()
   569  	d := mock.Deployment()
   570  	d.JobID = j.ID
   571  	d.Status = structs.DeploymentStatusPaused
   572  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   573  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   574  
   575  	w.SetEnabled(true, m.state)
   576  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   577  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   578  
   579  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   580  	matchConfig := &matchDeploymentStatusUpdateConfig{
   581  		DeploymentID:      d.ID,
   582  		Status:            structs.DeploymentStatusRunning,
   583  		StatusDescription: structs.DeploymentStatusDescriptionRunning,
   584  		Eval:              true,
   585  	}
   586  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   587  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   588  
   589  	// Call PauseDeployment
   590  	req := &structs.DeploymentPauseRequest{
   591  		DeploymentID: d.ID,
   592  		Pause:        false,
   593  	}
   594  	var resp structs.DeploymentUpdateResponse
   595  	err := w.PauseDeployment(req, &resp)
   596  	assert.Nil(err, "PauseDeployment")
   597  
   598  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   599  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   600  }
   601  
   602  // Test unpausing a deployment that is running
   603  func TestWatcher_PauseDeployment_Unpause_Running(t *testing.T) {
   604  	t.Parallel()
   605  	assert := assert.New(t)
   606  	w, m := defaultTestDeploymentWatcher(t)
   607  
   608  	// Create a job and a deployment
   609  	j := mock.Job()
   610  	d := mock.Deployment()
   611  	d.JobID = j.ID
   612  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   613  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   614  
   615  	w.SetEnabled(true, m.state)
   616  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   617  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   618  
   619  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   620  	matchConfig := &matchDeploymentStatusUpdateConfig{
   621  		DeploymentID:      d.ID,
   622  		Status:            structs.DeploymentStatusRunning,
   623  		StatusDescription: structs.DeploymentStatusDescriptionRunning,
   624  		Eval:              true,
   625  	}
   626  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   627  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   628  
   629  	// Call PauseDeployment
   630  	req := &structs.DeploymentPauseRequest{
   631  		DeploymentID: d.ID,
   632  		Pause:        false,
   633  	}
   634  	var resp structs.DeploymentUpdateResponse
   635  	err := w.PauseDeployment(req, &resp)
   636  	assert.Nil(err, "PauseDeployment")
   637  
   638  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   639  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   640  }
   641  
   642  // Test failing a deployment that is running
   643  func TestWatcher_FailDeployment_Running(t *testing.T) {
   644  	t.Parallel()
   645  	assert := assert.New(t)
   646  	w, m := defaultTestDeploymentWatcher(t)
   647  
   648  	// Create a job and a deployment
   649  	j := mock.Job()
   650  	d := mock.Deployment()
   651  	d.JobID = j.ID
   652  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   653  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   654  
   655  	w.SetEnabled(true, m.state)
   656  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   657  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   658  
   659  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   660  	matchConfig := &matchDeploymentStatusUpdateConfig{
   661  		DeploymentID:      d.ID,
   662  		Status:            structs.DeploymentStatusFailed,
   663  		StatusDescription: structs.DeploymentStatusDescriptionFailedByUser,
   664  		Eval:              true,
   665  	}
   666  	matcher := matchDeploymentStatusUpdateRequest(matchConfig)
   667  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil)
   668  
   669  	// Call PauseDeployment
   670  	req := &structs.DeploymentFailRequest{
   671  		DeploymentID: d.ID,
   672  	}
   673  	var resp structs.DeploymentUpdateResponse
   674  	err := w.FailDeployment(req, &resp)
   675  	assert.Nil(err, "FailDeployment")
   676  
   677  	assert.Equal(1, len(w.watchers), "Deployment should still be active")
   678  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher))
   679  }
   680  
   681  // Tests that the watcher properly watches for allocation changes and takes the
   682  // proper actions
   683  func TestDeploymentWatcher_Watch(t *testing.T) {
   684  	t.Parallel()
   685  	assert := assert.New(t)
   686  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
   687  
   688  	// Create a job, alloc, and a deployment
   689  	j := mock.Job()
   690  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   691  	j.TaskGroups[0].Update.MaxParallel = 2
   692  	j.TaskGroups[0].Update.AutoRevert = true
   693  	j.Stable = true
   694  	d := mock.Deployment()
   695  	d.JobID = j.ID
   696  	d.TaskGroups["web"].AutoRevert = true
   697  	a := mock.Alloc()
   698  	a.DeploymentID = d.ID
   699  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   700  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   701  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   702  
   703  	// Upsert the job again to get a new version
   704  	j2 := j.Copy()
   705  	// Modify the job to make its specification different
   706  	j2.Meta["foo"] = "bar"
   707  	j2.Stable = false
   708  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
   709  
   710  	w.SetEnabled(true, m.state)
   711  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   712  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   713  
   714  	// Assert that we will get a createEvaluation call only once. This will
   715  	// verify that the watcher is batching allocation changes
   716  	m1 := matchUpsertEvals([]string{d.ID})
   717  	m.On("UpsertEvals", mocker.MatchedBy(m1)).Return(nil).Once()
   718  
   719  	// Update the allocs health to healthy which should create an evaluation
   720  	for i := 0; i < 5; i++ {
   721  		req := &structs.ApplyDeploymentAllocHealthRequest{
   722  			DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   723  				DeploymentID:         d.ID,
   724  				HealthyAllocationIDs: []string{a.ID},
   725  			},
   726  		}
   727  		assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth")
   728  	}
   729  
   730  	// Wait for there to be one eval
   731  	testutil.WaitForResult(func() (bool, error) {
   732  		ws := memdb.NewWatchSet()
   733  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   734  		if err != nil {
   735  			return false, err
   736  		}
   737  
   738  		if l := len(evals); l != 1 {
   739  			return false, fmt.Errorf("Got %d evals; want 1", l)
   740  		}
   741  
   742  		return true, nil
   743  	}, func(err error) {
   744  		t.Fatal(err)
   745  	})
   746  
   747  	// Assert that we get a call to UpsertDeploymentStatusUpdate
   748  	c := &matchDeploymentStatusUpdateConfig{
   749  		DeploymentID:      d.ID,
   750  		Status:            structs.DeploymentStatusFailed,
   751  		StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0),
   752  		JobVersion:        helper.Uint64ToPtr(0),
   753  		Eval:              true,
   754  	}
   755  	m2 := matchDeploymentStatusUpdateRequest(c)
   756  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil)
   757  
   758  	// Update the allocs health to unhealthy which should create a job rollback,
   759  	// status update and eval
   760  	req2 := &structs.ApplyDeploymentAllocHealthRequest{
   761  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   762  			DeploymentID:           d.ID,
   763  			UnhealthyAllocationIDs: []string{a.ID},
   764  		},
   765  	}
   766  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth")
   767  
   768  	// Wait for there to be one eval
   769  	testutil.WaitForResult(func() (bool, error) {
   770  		ws := memdb.NewWatchSet()
   771  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   772  		if err != nil {
   773  			return false, err
   774  		}
   775  
   776  		if l := len(evals); l != 2 {
   777  			return false, fmt.Errorf("Got %d evals; want 1", l)
   778  		}
   779  
   780  		return true, nil
   781  	}, func(err error) {
   782  		t.Fatal(err)
   783  	})
   784  
   785  	m.AssertCalled(t, "UpsertEvals", mocker.MatchedBy(m1))
   786  
   787  	// After we upsert the job version will go to 2. So use this to assert the
   788  	// original call happened.
   789  	c2 := &matchDeploymentStatusUpdateConfig{
   790  		DeploymentID:      d.ID,
   791  		Status:            structs.DeploymentStatusFailed,
   792  		StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0),
   793  		JobVersion:        helper.Uint64ToPtr(2),
   794  		Eval:              true,
   795  	}
   796  	m3 := matchDeploymentStatusUpdateRequest(c2)
   797  	m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(m3))
   798  	testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil },
   799  		func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") })
   800  }
   801  
   802  // Tests that the watcher fails rollback when the spec hasn't changed
   803  func TestDeploymentWatcher_RollbackFailed(t *testing.T) {
   804  	t.Parallel()
   805  	assert := assert.New(t)
   806  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond)
   807  
   808  	// Create a job, alloc, and a deployment
   809  	j := mock.Job()
   810  	j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy()
   811  	j.TaskGroups[0].Update.MaxParallel = 2
   812  	j.TaskGroups[0].Update.AutoRevert = true
   813  	j.Stable = true
   814  	d := mock.Deployment()
   815  	d.JobID = j.ID
   816  	d.TaskGroups["web"].AutoRevert = true
   817  	a := mock.Alloc()
   818  	a.DeploymentID = d.ID
   819  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob")
   820  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment")
   821  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs")
   822  
   823  	// Upsert the job again to get a new version
   824  	j2 := j.Copy()
   825  	// Modify the job to make its specification different
   826  	j2.Stable = false
   827  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2")
   828  
   829  	w.SetEnabled(true, m.state)
   830  	testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil },
   831  		func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") })
   832  
   833  	// Assert that we will get a createEvaluation call only once. This will
   834  	// verify that the watcher is batching allocation changes
   835  	m1 := matchUpsertEvals([]string{d.ID})
   836  	m.On("UpsertEvals", mocker.MatchedBy(m1)).Return(nil).Once()
   837  
   838  	// Update the allocs health to healthy which should create an evaluation
   839  	for i := 0; i < 5; i++ {
   840  		req := &structs.ApplyDeploymentAllocHealthRequest{
   841  			DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   842  				DeploymentID:         d.ID,
   843  				HealthyAllocationIDs: []string{a.ID},
   844  			},
   845  		}
   846  		assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth")
   847  	}
   848  
   849  	// Wait for there to be one eval
   850  	testutil.WaitForResult(func() (bool, error) {
   851  		ws := memdb.NewWatchSet()
   852  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   853  		if err != nil {
   854  			return false, err
   855  		}
   856  
   857  		if l := len(evals); l != 1 {
   858  			return false, fmt.Errorf("Got %d evals; want 1", l)
   859  		}
   860  
   861  		return true, nil
   862  	}, func(err error) {
   863  		t.Fatal(err)
   864  	})
   865  
   866  	// Assert that we get a call to UpsertDeploymentStatusUpdate with roll back failed as the status
   867  	c := &matchDeploymentStatusUpdateConfig{
   868  		DeploymentID:      d.ID,
   869  		Status:            structs.DeploymentStatusFailed,
   870  		StatusDescription: structs.DeploymentStatusDescriptionRollbackNoop(structs.DeploymentStatusDescriptionFailedAllocations, 0),
   871  		JobVersion:        nil,
   872  		Eval:              true,
   873  	}
   874  	m2 := matchDeploymentStatusUpdateRequest(c)
   875  	m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil)
   876  
   877  	// Update the allocs health to unhealthy which will cause attempting a rollback,
   878  	// fail in that step, do status update and eval
   879  	req2 := &structs.ApplyDeploymentAllocHealthRequest{
   880  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   881  			DeploymentID:           d.ID,
   882  			UnhealthyAllocationIDs: []string{a.ID},
   883  		},
   884  	}
   885  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth")
   886  
   887  	// Wait for there to be one eval
   888  	testutil.WaitForResult(func() (bool, error) {
   889  		ws := memdb.NewWatchSet()
   890  		evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID)
   891  		if err != nil {
   892  			return false, err
   893  		}
   894  
   895  		if l := len(evals); l != 2 {
   896  			return false, fmt.Errorf("Got %d evals; want 1", l)
   897  		}
   898  
   899  		return true, nil
   900  	}, func(err error) {
   901  		t.Fatal(err)
   902  	})
   903  
   904  	m.AssertCalled(t, "UpsertEvals", mocker.MatchedBy(m1))
   905  
   906  	// verify that the job version hasn't changed after upsert
   907  	m.state.JobByID(nil, structs.DefaultNamespace, j.ID)
   908  	assert.Equal(uint64(0), j.Version, "Expected job version 0 but got ", j.Version)
   909  }
   910  
   911  // Test evaluations are batched between watchers
   912  func TestWatcher_BatchEvals(t *testing.T) {
   913  	t.Parallel()
   914  	assert := assert.New(t)
   915  	w, m := testDeploymentWatcher(t, 1000.0, 1*time.Second)
   916  
   917  	// Create a job, alloc, for two deployments
   918  	j1 := mock.Job()
   919  	d1 := mock.Deployment()
   920  	d1.JobID = j1.ID
   921  	a1 := mock.Alloc()
   922  	a1.DeploymentID = d1.ID
   923  
   924  	j2 := mock.Job()
   925  	d2 := mock.Deployment()
   926  	d2.JobID = j2.ID
   927  	a2 := mock.Alloc()
   928  	a2.DeploymentID = d2.ID
   929  
   930  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j1), "UpsertJob")
   931  	assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob")
   932  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d1), "UpsertDeployment")
   933  	assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d2), "UpsertDeployment")
   934  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a1}), "UpsertAllocs")
   935  	assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a2}), "UpsertAllocs")
   936  
   937  	w.SetEnabled(true, m.state)
   938  	testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil },
   939  		func(err error) { assert.Equal(2, len(w.watchers), "Should have 2 deployment") })
   940  
   941  	// Assert that we will get a createEvaluation call only once and it contains
   942  	// both deployments. This will verify that the watcher is batching
   943  	// allocation changes
   944  	m1 := matchUpsertEvals([]string{d1.ID, d2.ID})
   945  	m.On("UpsertEvals", mocker.MatchedBy(m1)).Return(nil).Once()
   946  
   947  	// Update the allocs health to healthy which should create an evaluation
   948  	req := &structs.ApplyDeploymentAllocHealthRequest{
   949  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   950  			DeploymentID:         d1.ID,
   951  			HealthyAllocationIDs: []string{a1.ID},
   952  		},
   953  	}
   954  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth")
   955  
   956  	req2 := &structs.ApplyDeploymentAllocHealthRequest{
   957  		DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
   958  			DeploymentID:         d2.ID,
   959  			HealthyAllocationIDs: []string{a2.ID},
   960  		},
   961  	}
   962  	assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth")
   963  
   964  	// Wait for there to be one eval for each job
   965  	testutil.WaitForResult(func() (bool, error) {
   966  		ws := memdb.NewWatchSet()
   967  		evals1, err := m.state.EvalsByJob(ws, j1.Namespace, j1.ID)
   968  		if err != nil {
   969  			return false, err
   970  		}
   971  
   972  		evals2, err := m.state.EvalsByJob(ws, j2.Namespace, j2.ID)
   973  		if err != nil {
   974  			return false, err
   975  		}
   976  
   977  		if l := len(evals1); l != 1 {
   978  			return false, fmt.Errorf("Got %d evals; want 1", l)
   979  		}
   980  
   981  		if l := len(evals2); l != 1 {
   982  			return false, fmt.Errorf("Got %d evals; want 1", l)
   983  		}
   984  
   985  		return true, nil
   986  	}, func(err error) {
   987  		t.Fatal(err)
   988  	})
   989  
   990  	m.AssertCalled(t, "UpsertEvals", mocker.MatchedBy(m1))
   991  	testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil },
   992  		func(err error) { assert.Equal(2, len(w.watchers), "Should have 2 deployment") })
   993  }