github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/undertaker/undertaker_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package undertaker_test 5 6 import ( 7 "context" 8 "time" 9 10 "github.com/juju/clock/testclock" 11 "github.com/juju/errors" 12 "github.com/juju/loggo" 13 "github.com/juju/testing" 14 jc "github.com/juju/testing/checkers" 15 "github.com/juju/worker/v3" 16 "github.com/juju/worker/v3/workertest" 17 "go.uber.org/mock/gomock" 18 gc "gopkg.in/check.v1" 19 20 "github.com/juju/juju/core/life" 21 "github.com/juju/juju/core/status" 22 watcher "github.com/juju/juju/core/watcher" 23 "github.com/juju/juju/core/watcher/watchertest" 24 "github.com/juju/juju/environs" 25 cloudspec "github.com/juju/juju/environs/cloudspec" 26 environscontext "github.com/juju/juju/environs/context" 27 "github.com/juju/juju/rpc/params" 28 "github.com/juju/juju/worker/undertaker" 29 ) 30 31 // OldUndertakerSuite is *not* complete. But it's a lot more so 32 // than it was before, and should be much easier to extend. 33 type OldUndertakerSuite struct { 34 testing.IsolationSuite 35 fix fixture 36 } 37 38 var _ = gc.Suite(&OldUndertakerSuite{}) 39 40 func (s *OldUndertakerSuite) SetUpTest(c *gc.C) { 41 s.IsolationSuite.SetUpTest(c) 42 minute := time.Minute 43 s.fix = fixture{ 44 clock: testclock.NewDilatedWallClock(10 * time.Millisecond), 45 info: params.UndertakerModelInfoResult{ 46 Result: params.UndertakerModelInfo{ 47 Life: "dying", 48 DestroyTimeout: &minute, 49 }, 50 }, 51 } 52 } 53 54 func (s *OldUndertakerSuite) TestAliveError(c *gc.C) { 55 s.fix.info.Result.Life = "alive" 56 s.fix.dirty = true 57 stub := s.fix.run(c, func(w worker.Worker) { 58 err := workertest.CheckKilled(c, w) 59 c.Check(err, gc.ErrorMatches, "model still alive") 60 }) 61 stub.CheckCallNames(c, "WatchModel", "ModelInfo") 62 } 63 64 func (s *OldUndertakerSuite) TestAlreadyDeadRemoves(c *gc.C) { 65 s.fix.info.Result.Life = "dead" 66 stub := s.fix.run(c, func(w worker.Worker) { 67 workertest.CheckKilled(c, w) 68 }) 69 stub.CheckCallNames(c, "WatchModel", "ModelInfo", "SetStatus", "ModelConfig", "CloudSpec", "Destroy", "RemoveModel") 70 } 71 72 func (s *OldUndertakerSuite) TestDyingDeadRemoved(c *gc.C) { 73 stub := s.fix.run(c, func(w worker.Worker) { 74 workertest.CheckKilled(c, w) 75 }) 76 stub.CheckCallNames(c, 77 "WatchModel", 78 "ModelInfo", 79 "SetStatus", 80 "WatchModelResources", 81 "ProcessDyingModel", 82 "SetStatus", 83 "ModelConfig", 84 "CloudSpec", 85 "Destroy", 86 "RemoveModel", 87 ) 88 } 89 90 func (s *OldUndertakerSuite) TestSetStatusDestroying(c *gc.C) { 91 stub := s.fix.run(c, func(w worker.Worker) { 92 workertest.CheckKilled(c, w) 93 }) 94 stub.CheckCallNames(c, 95 "WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel", 96 "SetStatus", "ModelConfig", "CloudSpec", "Destroy", "RemoveModel") 97 stub.CheckCall( 98 c, 2, "SetStatus", status.Destroying, 99 "cleaning up cloud resources", map[string]interface{}(nil), 100 ) 101 stub.CheckCall( 102 c, 5, "SetStatus", status.Destroying, 103 "tearing down cloud environment", map[string]interface{}(nil), 104 ) 105 } 106 107 func (s *OldUndertakerSuite) TestControllerStopsWhenModelDead(c *gc.C) { 108 s.fix.info.Result.IsSystem = true 109 stub := s.fix.run(c, func(w worker.Worker) { 110 workertest.CheckKilled(c, w) 111 }) 112 stub.CheckCallNames(c, 113 "WatchModel", 114 "ModelInfo", 115 "SetStatus", 116 "WatchModelResources", 117 "ProcessDyingModel", 118 ) 119 } 120 121 func (s *OldUndertakerSuite) TestModelInfoErrorFatal(c *gc.C) { 122 s.fix.errors = []error{nil, errors.New("pow")} 123 s.fix.dirty = true 124 stub := s.fix.run(c, func(w worker.Worker) { 125 err := workertest.CheckKilled(c, w) 126 c.Check(err, gc.ErrorMatches, "pow") 127 }) 128 stub.CheckCallNames(c, "WatchModel", "ModelInfo") 129 } 130 131 func (s *OldUndertakerSuite) TestWatchModelResourcesErrorFatal(c *gc.C) { 132 s.fix.errors = []error{nil, nil, nil, errors.New("pow")} 133 s.fix.dirty = true 134 stub := s.fix.run(c, func(w worker.Worker) { 135 err := workertest.CheckKilled(c, w) 136 c.Check(err, gc.ErrorMatches, "proccesing model death: pow") 137 }) 138 stub.CheckCallNames(c, "WatchModel", "ModelInfo", "SetStatus", "WatchModelResources") 139 } 140 141 func (s *OldUndertakerSuite) TestProcessDyingModelErrorRetried(c *gc.C) { 142 s.fix.errors = []error{ 143 nil, // WatchModel 144 nil, // ModelInfo 145 nil, // SetStatus 146 nil, // WatchModelResources, 147 ¶ms.Error{Code: params.CodeHasHostedModels}, 148 nil, // SetStatus 149 ¶ms.Error{Code: params.CodeModelNotEmpty}, 150 nil, // SetStatus 151 nil, // ProcessDyingModel, 152 nil, // SetStatus 153 nil, // ModelConfig 154 nil, // CloudSpec 155 nil, // Destroy, 156 nil, // RemoveModel 157 } 158 stub := s.fix.run(c, func(w worker.Worker) { 159 workertest.CheckKilled(c, w) 160 }) 161 stub.CheckCallNames(c, 162 "WatchModel", 163 "ModelInfo", 164 "SetStatus", 165 "WatchModelResources", 166 "ProcessDyingModel", 167 "SetStatus", 168 "ProcessDyingModel", 169 "SetStatus", 170 "ProcessDyingModel", 171 "SetStatus", 172 "ModelConfig", 173 "CloudSpec", 174 "Destroy", 175 "RemoveModel", 176 ) 177 } 178 179 func (s *OldUndertakerSuite) TestProcessDyingModelErrorFatal(c *gc.C) { 180 s.fix.errors = []error{ 181 nil, // WatchModel 182 nil, // ModelInfo 183 nil, // SetStatus 184 nil, // WatchModelResources, 185 errors.New("nope"), 186 } 187 s.fix.dirty = true 188 stub := s.fix.run(c, func(w worker.Worker) { 189 err := workertest.CheckKilled(c, w) 190 c.Check(err, gc.ErrorMatches, "proccesing model death: nope") 191 }) 192 stub.CheckCallNames(c, 193 "WatchModel", 194 "ModelInfo", 195 "SetStatus", 196 "WatchModelResources", 197 "ProcessDyingModel", 198 ) 199 } 200 201 func (s *OldUndertakerSuite) TestDestroyErrorFatal(c *gc.C) { 202 s.fix.errors = []error{nil, nil, nil, nil, nil, errors.New("pow")} 203 s.fix.info.Result.Life = "dead" 204 s.fix.dirty = true 205 stub := s.fix.run(c, func(w worker.Worker) { 206 err := workertest.CheckKilled(c, w) 207 c.Check(err, gc.ErrorMatches, "cannot destroy cloud resources: process destroy environ: pow") 208 }) 209 stub.CheckCallNames(c, "WatchModel", "ModelInfo", "SetStatus", "ModelConfig", "CloudSpec", "Destroy") 210 } 211 212 func (s *OldUndertakerSuite) TestDestroyErrorForced(c *gc.C) { 213 s.fix.errors = []error{nil, nil, nil, nil, nil, errors.New("pow")} 214 s.fix.info.Result.Life = "dead" 215 s.fix.info.Result.ForceDestroyed = true 216 destroyTimeout := 500 * time.Millisecond 217 s.fix.info.Result.DestroyTimeout = &destroyTimeout 218 stub := s.fix.run(c, func(w worker.Worker) { 219 err := workertest.CheckKilled(c, w) 220 c.Assert(err, jc.ErrorIsNil) 221 }) 222 // Removal continues despite the error calling destroy. 223 mainCalls, destroyCloudCalls := s.sortCalls(c, stub) 224 c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "RemoveModel"}) 225 c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"}) 226 // Logged the failed destroy call. 227 s.fix.logger.stub.CheckCallNames(c, "Errorf") 228 } 229 230 func (s *OldUndertakerSuite) TestRemoveModelErrorFatal(c *gc.C) { 231 s.fix.errors = []error{nil, nil, nil, nil, nil, nil, errors.New("pow")} 232 s.fix.info.Result.Life = "dead" 233 s.fix.dirty = true 234 stub := s.fix.run(c, func(w worker.Worker) { 235 err := workertest.CheckKilled(c, w) 236 c.Check(err, gc.ErrorMatches, "cannot remove model: pow") 237 }) 238 mainCalls, destroyCloudCalls := s.sortCalls(c, stub) 239 c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "RemoveModel"}) 240 c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"}) 241 } 242 243 func (s *OldUndertakerSuite) TestDestroyTimeout(c *gc.C) { 244 notEmptyErr := ¶ms.Error{Code: params.CodeModelNotEmpty} 245 s.fix.errors = []error{nil, nil, nil, nil, notEmptyErr, notEmptyErr, notEmptyErr, notEmptyErr, errors.Timeoutf("error")} 246 s.fix.dirty = true 247 s.fix.advance = 2 * time.Minute 248 stub := s.fix.run(c, func(w worker.Worker) { 249 err := workertest.CheckKilled(c, w) 250 c.Assert(err, gc.ErrorMatches, ".* timeout") 251 }) 252 // Depending on timing there can be 1 or more ProcessDyingModel calls. 253 calls := stub.Calls() 254 var callNames []string 255 for i, call := range calls { 256 if call.FuncName == "ProcessDyingModel" { 257 continue 258 } 259 if i > 4 && call.FuncName == "SetStatus" { 260 continue 261 } 262 callNames = append(callNames, call.FuncName) 263 } 264 c.Assert(callNames, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources"}) 265 } 266 267 func (s *OldUndertakerSuite) TestDestroyTimeoutForce(c *gc.C) { 268 s.fix.info.Result.ForceDestroyed = true 269 s.fix.advance = 2 * time.Minute 270 stub := s.fix.run(c, func(w worker.Worker) { 271 err := workertest.CheckKilled(c, w) 272 c.Assert(err, jc.ErrorIsNil) 273 }) 274 mainCalls, destroyCloudCalls := s.sortCalls(c, stub) 275 c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel", "SetStatus", "RemoveModel"}) 276 c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"}) 277 s.fix.logger.stub.CheckNoCalls(c) 278 } 279 280 func (s *OldUndertakerSuite) TestEnvironDestroyTimeout(c *gc.C) { 281 timeout := time.Millisecond 282 s.fix.info.Result.DestroyTimeout = &timeout 283 s.fix.dirty = true 284 stub := s.fix.run(c, func(w worker.Worker) { 285 err := workertest.CheckKilled(c, w) 286 c.Assert(err, jc.ErrorIsNil) 287 }) 288 mainCalls, destroyCloudCalls := s.sortCalls(c, stub) 289 c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel", "SetStatus", "RemoveModel"}) 290 c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"}) 291 s.fix.logger.stub.CheckCall(c, 0, "Warningf", "timeout ignored for graceful model destroy", []interface{}(nil)) 292 } 293 294 func (s *OldUndertakerSuite) TestEnvironDestroyTimeoutForce(c *gc.C) { 295 timeout := time.Second 296 s.fix.info.Result.DestroyTimeout = &timeout 297 s.fix.info.Result.ForceDestroyed = true 298 s.fix.dirty = true 299 stub := s.fix.run(c, func(w worker.Worker) { 300 err := workertest.CheckKilled(c, w) 301 c.Assert(err, jc.ErrorIsNil) 302 }) 303 mainCalls, destroyCloudCalls := s.sortCalls(c, stub) 304 c.Assert(mainCalls, jc.DeepEquals, []string{"WatchModel", "ModelInfo", "SetStatus", "WatchModelResources", "ProcessDyingModel", "SetStatus", "RemoveModel"}) 305 c.Assert(destroyCloudCalls, jc.DeepEquals, []string{"ModelConfig", "CloudSpec", "Destroy"}) 306 } 307 308 func (s *OldUndertakerSuite) sortCalls(c *gc.C, stub *testing.Stub) (mainCalls []string, destroyCloudCalls []string) { 309 calls := stub.Calls() 310 for _, call := range calls { 311 switch call.FuncName { 312 case "ModelConfig", "CloudSpec", "Destroy": 313 destroyCloudCalls = append(destroyCloudCalls, call.FuncName) 314 default: 315 mainCalls = append(mainCalls, call.FuncName) 316 } 317 } 318 return 319 } 320 321 func (s *OldUndertakerSuite) TestEnvironDestroyForceTimeoutZero(c *gc.C) { 322 zero := time.Second * 0 323 s.fix.info.Result.DestroyTimeout = &zero 324 s.fix.info.Result.ForceDestroyed = true 325 s.fix.dirty = true 326 stub := s.fix.run(c, func(w worker.Worker) { 327 err := workertest.CheckKilled(c, w) 328 c.Assert(err, jc.ErrorIsNil) 329 }) 330 stub.CheckCallNames(c, "WatchModel", "ModelInfo", "RemoveModel") 331 s.fix.logger.stub.CheckNoCalls(c) 332 } 333 334 type UndertakerSuite struct{} 335 336 var _ = gc.Suite(&UndertakerSuite{}) 337 338 func (s *UndertakerSuite) TestExitOnModelChanged(c *gc.C) { 339 ctrl := gomock.NewController(c) 340 defer ctrl.Finish() 341 342 facade := NewMockFacade(ctrl) 343 facade.EXPECT().SetStatus(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() 344 345 modelChanged := make(chan struct{}, 1) 346 modelChanged <- struct{}{} 347 modelResources := make(chan struct{}, 1) 348 modelResources <- struct{}{} 349 facade.EXPECT().WatchModel().DoAndReturn(func() (watcher.NotifyWatcher, error) { 350 return watchertest.NewMockNotifyWatcher(modelChanged), nil 351 }) 352 facade.EXPECT().WatchModelResources().DoAndReturn(func() (watcher.NotifyWatcher, error) { 353 return watchertest.NewMockNotifyWatcher(modelResources), nil 354 }) 355 facade.EXPECT().ModelConfig().Return(nil, nil) 356 357 gomock.InOrder( 358 facade.EXPECT().ModelInfo().Return(params.UndertakerModelInfoResult{ 359 Result: params.UndertakerModelInfo{ 360 Life: life.Dying, 361 ForceDestroyed: false, 362 }, 363 }, nil), 364 facade.EXPECT().ProcessDyingModel().Return(nil), 365 facade.EXPECT().CloudSpec().DoAndReturn(func() (cloudspec.CloudSpec, error) { 366 modelChanged <- struct{}{} 367 return cloudspec.CloudSpec{}, nil 368 }), 369 facade.EXPECT().ModelInfo().Return(params.UndertakerModelInfoResult{ 370 Result: params.UndertakerModelInfo{ 371 Life: life.Dying, 372 ForceDestroyed: true, // changed from false to true to cause worker to exit. 373 }, 374 }, nil), 375 ) 376 377 credentialAPI := NewMockCredentialAPI(ctrl) 378 379 w, err := undertaker.NewUndertaker(undertaker.Config{ 380 Facade: facade, 381 CredentialAPI: credentialAPI, 382 Logger: loggo.GetLogger("test"), 383 Clock: testclock.NewDilatedWallClock(testing.ShortWait), 384 NewCloudDestroyerFunc: func(ctx context.Context, op environs.OpenParams) (environs.CloudDestroyer, error) { 385 return &waitDestroyer{}, nil 386 }, 387 }) 388 c.Assert(err, jc.ErrorIsNil) 389 390 workertest.CheckKilled(c, w) 391 392 err = w.Wait() 393 c.Assert(err, gc.ErrorMatches, "model destroy parameters changed") 394 } 395 396 type waitDestroyer struct { 397 environs.Environ 398 } 399 400 func (w *waitDestroyer) Destroy(ctx environscontext.ProviderCallContext) error { 401 <-ctx.Done() 402 return ctx.Err() 403 }