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 }