github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/upgradeseries/worker_test.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package upgradeseries_test
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  	"github.com/juju/names/v5"
    12  	jc "github.com/juju/testing/checkers"
    13  	"github.com/juju/worker/v3"
    14  	"github.com/juju/worker/v3/workertest"
    15  	"go.uber.org/mock/gomock"
    16  	gc "gopkg.in/check.v1"
    17  
    18  	"github.com/juju/juju/core/model"
    19  	"github.com/juju/juju/core/watcher"
    20  	"github.com/juju/juju/testing"
    21  	workermocks "github.com/juju/juju/worker/mocks"
    22  	"github.com/juju/juju/worker/upgradeseries"
    23  	. "github.com/juju/juju/worker/upgradeseries/mocks"
    24  )
    25  
    26  type fakeWatcher struct {
    27  	worker.Worker
    28  	ch <-chan struct{}
    29  }
    30  
    31  func (w *fakeWatcher) Changes() watcher.NotifyChannel {
    32  	return w.ch
    33  }
    34  
    35  type workerSuite struct {
    36  	testing.BaseSuite
    37  
    38  	logger        upgradeseries.Logger
    39  	facade        *MockFacade
    40  	unitDiscovery *MockUnitDiscovery
    41  	upgrader      *MockUpgrader
    42  	notifyWorker  *workermocks.MockWorker
    43  
    44  	// The done channel is used by tests to indicate that
    45  	// the worker has accomplished the scenario and can be stopped.
    46  	done chan struct{}
    47  }
    48  
    49  var _ = gc.Suite(&workerSuite{})
    50  
    51  func (s *workerSuite) SetUpTest(c *gc.C) {
    52  	s.BaseSuite.SetUpTest(c)
    53  	s.logger = loggo.GetLogger("test.upgradeseries")
    54  	s.done = make(chan struct{})
    55  }
    56  
    57  // TestFullWorkflow uses the the expectation scenarios from each of the tests
    58  // below to compose a test of the whole upgrade-series scenario, from start
    59  // to finish.
    60  func (s *workerSuite) TestFullWorkflow(c *gc.C) {
    61  	defer s.setupMocks(c).Finish()
    62  
    63  	s.notify(7)
    64  	s.expectUnitDiscovery()
    65  	s.expectMachineValidateUnitsNotPrepareCompleteNoAction()
    66  	s.expectMachinePrepareStartedUnitsNotPrepareCompleteNoAction()
    67  	s.expectMachinePrepareStartedUnitFilesWrittenProgressPrepareComplete()
    68  	s.expectMachineCompleteStartedUnitsPrepareCompleteUnitsStarted()
    69  	s.expectMachineCompleteStartedUnitsCompleteProgressComplete()
    70  	s.expectMachineCompletedFinishUpgradeSeries()
    71  	s.expectLockNotFoundNoAction()
    72  
    73  	w := s.newWorker(c)
    74  
    75  	s.cleanKill(c, w)
    76  	expected := map[string]interface{}{
    77  		"machine status": model.UpgradeSeriesNotStarted,
    78  	}
    79  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
    80  }
    81  
    82  func (s *workerSuite) TestLockNotFoundNoAction(c *gc.C) {
    83  	defer s.setupMocks(c).Finish()
    84  
    85  	s.notify(1)
    86  	s.expectLockNotFoundNoAction()
    87  	w := s.newWorker(c)
    88  
    89  	s.cleanKill(c, w)
    90  	expected := map[string]interface{}{
    91  		"machine status": model.UpgradeSeriesNotStarted,
    92  	}
    93  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
    94  }
    95  
    96  func (s *workerSuite) expectLockNotFoundNoAction() {
    97  	// If the lock is not found, no further processing occurs.
    98  	// This is the only call we expect to see.
    99  	s.facade.EXPECT().MachineStatus().Return(model.UpgradeSeriesStatus(""), errors.NewNotFound(nil, "nope"))
   100  }
   101  
   102  func (s *workerSuite) TestCompleteNoAction(c *gc.C) {
   103  	defer s.setupMocks(c).Finish()
   104  
   105  	// If the workflow is completed, no further processing occurs.
   106  	s.expectUnitDiscovery()
   107  	s.facade.EXPECT().MachineStatus().Return(model.UpgradeSeriesPrepareCompleted, nil)
   108  	s.notify(1)
   109  	w := s.newWorker(c)
   110  
   111  	s.cleanKill(c, w)
   112  	expected := map[string]interface{}{
   113  		"machine status": model.UpgradeSeriesPrepareCompleted,
   114  	}
   115  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
   116  }
   117  
   118  func (s *workerSuite) TestMachinePrepareStartedUnitsNotPrepareCompleteNoAction(c *gc.C) {
   119  	defer s.setupMocks(c).Finish()
   120  
   121  	s.notify(1)
   122  	s.expectUnitDiscovery()
   123  	s.expectMachinePrepareStartedUnitsNotPrepareCompleteNoAction()
   124  
   125  	w := s.newWorker(c)
   126  
   127  	s.cleanKill(c, w)
   128  	expected := map[string]interface{}{
   129  		"machine status": model.UpgradeSeriesPrepareStarted,
   130  		"prepared units": []string{"wordpress/0"},
   131  	}
   132  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
   133  }
   134  
   135  func (s *workerSuite) expectUnitDiscovery() {
   136  	s.unitDiscovery.EXPECT().Units().Return([]names.UnitTag{
   137  		names.NewUnitTag("wordpress/0"),
   138  		names.NewUnitTag("mysql/0"),
   139  	}, nil)
   140  }
   141  
   142  func (s *workerSuite) expectMachineValidateUnitsNotPrepareCompleteNoAction() {
   143  	s.facade.EXPECT().MachineStatus().Return(model.UpgradeSeriesValidate, nil)
   144  
   145  	s.expectSetInstanceStatus(model.UpgradeSeriesValidate, "validating units")
   146  }
   147  
   148  func (s *workerSuite) expectMachinePrepareStartedUnitsNotPrepareCompleteNoAction() {
   149  	s.facade.EXPECT().MachineStatus().Return(model.UpgradeSeriesPrepareStarted, nil)
   150  	s.expectPinLeadership()
   151  
   152  	s.expectSetInstanceStatus(model.UpgradeSeriesPrepareStarted, "preparing units")
   153  
   154  	// Only one of the two units has completed preparation.
   155  	s.expectUnitsPrepared("wordpress/0")
   156  }
   157  
   158  func (s *workerSuite) TestMachinePrepareStartedUnitFilesWrittenProgressPrepareComplete(c *gc.C) {
   159  	defer s.setupMocks(c).Finish()
   160  
   161  	s.notify(1)
   162  	s.expectUnitDiscovery()
   163  	s.expectPinLeadership()
   164  	s.expectMachinePrepareStartedUnitFilesWrittenProgressPrepareComplete()
   165  	w := s.newWorker(c)
   166  
   167  	s.cleanKill(c, w)
   168  	expected := map[string]interface{}{
   169  		"machine status": model.UpgradeSeriesPrepareStarted,
   170  		"prepared units": []string{"wordpress/0", "mysql/0"},
   171  	}
   172  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
   173  }
   174  
   175  func (s *workerSuite) expectMachinePrepareStartedUnitFilesWrittenProgressPrepareComplete() {
   176  	exp := s.facade.EXPECT()
   177  
   178  	exp.MachineStatus().Return(model.UpgradeSeriesPrepareStarted, nil)
   179  	s.expectSetInstanceStatus(model.UpgradeSeriesPrepareStarted, "preparing units")
   180  	s.expectUnitsPrepared("wordpress/0", "mysql/0")
   181  	exp.CurrentSeries().Return("focal", nil)
   182  	exp.TargetSeries().Return("jammy", nil)
   183  
   184  	s.upgrader.EXPECT().PerformUpgrade().Return(nil)
   185  	s.expectSetInstanceStatus(model.UpgradeSeriesPrepareStarted, "completing preparation")
   186  
   187  	exp.SetMachineStatus(model.UpgradeSeriesPrepareCompleted, gomock.Any()).Return(nil)
   188  	s.expectSetInstanceStatus(model.UpgradeSeriesPrepareCompleted, "waiting for completion command")
   189  }
   190  
   191  func (s *workerSuite) TestMachineCompleteStartedUnitsPrepareCompleteUnitsStarted(c *gc.C) {
   192  	defer s.setupMocks(c).Finish()
   193  
   194  	s.notify(1)
   195  	s.expectUnitDiscovery()
   196  	s.expectMachineCompleteStartedUnitsPrepareCompleteUnitsStarted()
   197  	w := s.newWorker(c)
   198  
   199  	s.cleanKill(c, w)
   200  	expected := map[string]interface{}{
   201  		"machine status": model.UpgradeSeriesCompleteStarted,
   202  		"prepared units": []string{"wordpress/0", "mysql/0"},
   203  	}
   204  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
   205  }
   206  
   207  func (s *workerSuite) expectMachineCompleteStartedUnitsPrepareCompleteUnitsStarted() {
   208  	s.facade.EXPECT().MachineStatus().Return(model.UpgradeSeriesCompleteStarted, nil)
   209  	s.expectSetInstanceStatus(model.UpgradeSeriesCompleteStarted, "waiting for units")
   210  	s.expectUnitsPrepared("wordpress/0", "mysql/0")
   211  	s.facade.EXPECT().StartUnitCompletion(gomock.Any()).Return(nil)
   212  }
   213  
   214  func (s *workerSuite) TestMachineCompleteStartedNoUnitsProgressComplete(c *gc.C) {
   215  	defer s.setupMocks(c).Finish()
   216  
   217  	// No units for this test.
   218  	s.unitDiscovery.EXPECT().Units().Return(nil, nil)
   219  
   220  	exp := s.facade.EXPECT()
   221  	exp.MachineStatus().Return(model.UpgradeSeriesCompleteStarted, nil)
   222  	s.expectSetInstanceStatus(model.UpgradeSeriesCompleteStarted, "waiting for units")
   223  
   224  	// Machine with no units - API calls return none, no services discovered.
   225  	exp.UnitsPrepared().Return(nil, nil)
   226  	exp.UnitsCompleted().Return(nil, nil)
   227  
   228  	// Progress directly to completed.
   229  	exp.SetMachineStatus(model.UpgradeSeriesCompleted, gomock.Any()).Return(nil)
   230  
   231  	s.notify(1)
   232  	w := s.newWorker(c)
   233  
   234  	s.cleanKill(c, w)
   235  	expected := map[string]interface{}{
   236  		"machine status": model.UpgradeSeriesCompleteStarted,
   237  	}
   238  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
   239  }
   240  
   241  func (s *workerSuite) TestMachineCompleteStartedUnitsCompleteProgressComplete(c *gc.C) {
   242  	defer s.setupMocks(c).Finish()
   243  	s.notify(1)
   244  	s.expectUnitDiscovery()
   245  	s.expectMachineCompleteStartedUnitsCompleteProgressComplete()
   246  	w := s.newWorker(c)
   247  
   248  	s.cleanKill(c, w)
   249  	expected := map[string]interface{}{
   250  		"machine status":  model.UpgradeSeriesCompleteStarted,
   251  		"completed units": []string{"wordpress/0", "mysql/0"},
   252  	}
   253  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
   254  }
   255  
   256  func (s *workerSuite) expectMachineCompleteStartedUnitsCompleteProgressComplete() {
   257  	exp := s.facade.EXPECT()
   258  
   259  	exp.MachineStatus().Return(model.UpgradeSeriesCompleteStarted, nil)
   260  	s.expectSetInstanceStatus(model.UpgradeSeriesCompleteStarted, "waiting for units")
   261  
   262  	// No units are in the prepare-complete state.
   263  	// They have completed their workflow.
   264  	s.expectUnitsPrepared()
   265  	s.facade.EXPECT().UnitsCompleted().Return([]names.UnitTag{
   266  		names.NewUnitTag("wordpress/0"),
   267  		names.NewUnitTag("mysql/0"),
   268  	}, nil)
   269  	exp.SetMachineStatus(model.UpgradeSeriesCompleted, gomock.Any()).Return(nil)
   270  }
   271  
   272  func (s *workerSuite) TestMachineCompletedFinishUpgradeSeries(c *gc.C) {
   273  	defer s.setupMocks(c).Finish()
   274  
   275  	s.notify(1)
   276  	s.expectUnitDiscovery()
   277  	s.expectMachineCompletedFinishUpgradeSeries()
   278  	w := s.newWorker(c)
   279  
   280  	s.cleanKill(c, w)
   281  	expected := map[string]interface{}{
   282  		"machine status": model.UpgradeSeriesCompleted,
   283  	}
   284  	c.Check(w.(worker.Reporter).Report(), gc.DeepEquals, expected)
   285  }
   286  
   287  func (s *workerSuite) expectMachineCompletedFinishUpgradeSeries() {
   288  	s.patchHost("xenial")
   289  
   290  	exp := s.facade.EXPECT()
   291  	exp.MachineStatus().Return(model.UpgradeSeriesCompleted, nil)
   292  	s.expectSetInstanceStatus(model.UpgradeSeriesCompleted, "finalising upgrade")
   293  	exp.FinishUpgradeSeries("xenial").Return(nil)
   294  
   295  	s.expectSetInstanceStatus(model.UpgradeSeriesCompleted, "success")
   296  	exp.UnpinMachineApplications().Return(map[string]error{
   297  		"mysql":     nil,
   298  		"wordpress": nil,
   299  	}, nil)
   300  }
   301  
   302  func (s *workerSuite) setupMocks(c *gc.C) *gomock.Controller {
   303  	ctrl := gomock.NewController(c)
   304  
   305  	s.facade = NewMockFacade(ctrl)
   306  	s.unitDiscovery = NewMockUnitDiscovery(ctrl)
   307  	s.upgrader = NewMockUpgrader(ctrl)
   308  	s.notifyWorker = workermocks.NewMockWorker(ctrl)
   309  
   310  	return ctrl
   311  }
   312  
   313  // newWorker creates worker config based on the suite's mocks.
   314  // Any supplied behaviour functions are executed,
   315  // then a new worker is started and returned.
   316  func (s *workerSuite) newWorker(c *gc.C) worker.Worker {
   317  	cfg := upgradeseries.Config{
   318  		Logger:          s.logger,
   319  		Facade:          s.facade,
   320  		UnitDiscovery:   s.unitDiscovery,
   321  		UpgraderFactory: func(_, _ string) (upgradeseries.Upgrader, error) { return s.upgrader, nil },
   322  	}
   323  
   324  	w, err := upgradeseries.NewWorker(cfg)
   325  	c.Assert(err, jc.ErrorIsNil)
   326  	return w
   327  }
   328  
   329  // expectUnitsCompleted represents the scenario where the input unit names
   330  // have completed their upgrade-series preparation.
   331  func (s *workerSuite) expectUnitsPrepared(units ...string) {
   332  	tags := make([]names.UnitTag, len(units))
   333  	for i, u := range units {
   334  		tags[i] = names.NewUnitTag(u)
   335  	}
   336  	s.facade.EXPECT().UnitsPrepared().Return(tags, nil)
   337  }
   338  
   339  // For individual tests that use a status of UpgradeSeriesPrepare started,
   340  // this will be called each time, but for the full workflow scenario we
   341  // only expect it once. To accommodate this, calls to this method will
   342  // often be in the Test... method instead of its partner expectation
   343  // method.
   344  func (s *workerSuite) expectPinLeadership() {
   345  	s.facade.EXPECT().PinMachineApplications().Return(map[string]error{
   346  		"mysql":     nil,
   347  		"wordpress": nil,
   348  	}, nil)
   349  }
   350  
   351  func (s *workerSuite) expectSetInstanceStatus(sts model.UpgradeSeriesStatus, msg string) {
   352  	s.facade.EXPECT().SetInstanceStatus(sts, msg).Return(nil)
   353  }
   354  
   355  // cleanKill waits for notifications to be processed, then waits for the input
   356  // worker to be killed cleanly. If either ops time out, the test fails.
   357  func (s *workerSuite) cleanKill(c *gc.C, w worker.Worker) {
   358  	select {
   359  	case <-s.done:
   360  	case <-time.After(testing.LongWait):
   361  		c.Errorf("timed out waiting for notifications to be consumed")
   362  	}
   363  	workertest.CleanKill(c, w)
   364  }
   365  
   366  func (s *workerSuite) patchHost(series string) {
   367  	upgradeseries.PatchHostSeries(s, series)
   368  }
   369  
   370  // notify returns a suite behaviour that will cause the upgrade-series watcher
   371  // to send a number of notifications equal to the supplied argument.
   372  // Once notifications have been consumed, we notify via the suite's channel.
   373  func (s *workerSuite) notify(times int) {
   374  	ch := make(chan struct{})
   375  
   376  	go func() {
   377  		for i := 0; i < times; i++ {
   378  			ch <- struct{}{}
   379  		}
   380  		close(s.done)
   381  	}()
   382  
   383  	s.notifyWorker.EXPECT().Kill().AnyTimes()
   384  	s.notifyWorker.EXPECT().Wait().Return(nil).AnyTimes()
   385  
   386  	s.facade.EXPECT().WatchUpgradeSeriesNotifications().Return(
   387  		&fakeWatcher{
   388  			Worker: s.notifyWorker,
   389  			ch:     ch,
   390  		}, nil)
   391  }