github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/caasmodel_test.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state_test 5 6 import ( 7 "fmt" 8 9 "github.com/juju/errors" 10 "github.com/juju/names/v5" 11 jc "github.com/juju/testing/checkers" 12 gc "gopkg.in/check.v1" 13 14 "github.com/juju/juju/caas" 15 k8sprovider "github.com/juju/juju/caas/kubernetes/provider" 16 k8stesting "github.com/juju/juju/caas/kubernetes/provider/testing" 17 "github.com/juju/juju/cloud" 18 "github.com/juju/juju/core/status" 19 "github.com/juju/juju/environs/config" 20 "github.com/juju/juju/state" 21 stateerrors "github.com/juju/juju/state/errors" 22 "github.com/juju/juju/state/stateenvirons" 23 "github.com/juju/juju/state/testing" 24 "github.com/juju/juju/storage" 25 "github.com/juju/juju/storage/provider" 26 "github.com/juju/juju/testing/factory" 27 ) 28 29 type CAASFixture struct { 30 ConnSuite 31 } 32 33 func (s *CAASFixture) SetUpTest(c *gc.C) { 34 s.ConnSuite.SetUpTest(c) 35 s.PatchValue(&k8sprovider.NewK8sClients, k8stesting.NoopFakeK8sClients) 36 } 37 38 // createTestModelConfig returns a new model config and its UUID for testing. 39 func (s *CAASFixture) createTestModelConfig(c *gc.C) (*config.Config, string) { 40 return createTestModelConfig(c, s.modelTag.Id()) 41 } 42 43 func (s *CAASFixture) newCAASModel(c *gc.C) (*state.CAASModel, *state.State) { 44 st := s.Factory.MakeCAASModel(c, nil) 45 s.AddCleanup(func(*gc.C) { st.Close() }) 46 model, err := st.Model() 47 c.Assert(err, jc.ErrorIsNil) 48 caasModel, err := model.CAASModel() 49 c.Assert(err, jc.ErrorIsNil) 50 return caasModel, st 51 } 52 53 type CAASModelSuite struct { 54 CAASFixture 55 } 56 57 var _ = gc.Suite(&CAASModelSuite{}) 58 59 func (s *CAASModelSuite) TestNewModel(c *gc.C) { 60 owner := s.Factory.MakeUser(c, nil) 61 err := s.State.AddCloud(cloud.Cloud{ 62 Name: "caas-cloud", 63 Type: "kubernetes", 64 AuthTypes: []cloud.AuthType{cloud.UserPassAuthType}, 65 }, owner.Name()) 66 c.Assert(err, jc.ErrorIsNil) 67 cfg, uuid := s.createTestModelConfig(c) 68 modelTag := names.NewModelTag(uuid) 69 cred := cloud.NewCredential(cloud.UserPassAuthType, nil) 70 credTag := names.NewCloudCredentialTag( 71 fmt.Sprintf("caas-cloud/%s/dummy-credential", owner.Name())) 72 err = s.State.UpdateCloudCredential(credTag, cred) 73 c.Assert(err, jc.ErrorIsNil) 74 model, st, err := s.Controller.NewModel(state.ModelArgs{ 75 Type: state.ModelTypeCAAS, 76 CloudName: "caas-cloud", 77 Config: cfg, 78 Owner: owner.UserTag(), 79 CloudCredential: credTag, 80 StorageProviderRegistry: provider.CommonStorageProviders(), 81 }) 82 c.Assert(err, jc.ErrorIsNil) 83 defer st.Close() 84 85 c.Assert(model.Type(), gc.Equals, state.ModelTypeCAAS) 86 c.Assert(model.UUID(), gc.Equals, modelTag.Id()) 87 c.Assert(model.Tag(), gc.Equals, modelTag) 88 c.Assert(model.ControllerTag(), gc.Equals, s.State.ControllerTag()) 89 c.Assert(model.Owner().Name(), gc.Equals, owner.Name()) 90 c.Assert(model.Name(), gc.Equals, "testing") 91 c.Assert(model.Life(), gc.Equals, state.Alive) 92 c.Assert(model.CloudRegion(), gc.Equals, "") 93 } 94 95 func (s *CAASModelSuite) TestDestroyEmptyModel(c *gc.C) { 96 model, st := s.newCAASModel(c) 97 c.Assert(model.Destroy(state.DestroyModelParams{}), jc.ErrorIsNil) 98 c.Assert(model.Refresh(), jc.ErrorIsNil) 99 c.Assert(model.Life(), gc.Equals, state.Dying) 100 c.Assert(st.RemoveDyingModel(), jc.ErrorIsNil) 101 c.Assert(model.Refresh(), jc.Satisfies, errors.IsNotFound) 102 } 103 104 func (s *CAASModelSuite) TestDestroyModel(c *gc.C) { 105 model, st := s.newCAASModel(c) 106 107 f := factory.NewFactory(st, s.StatePool) 108 ch := f.MakeCharm(c, &factory.CharmParams{Name: "gitlab", Series: "kubernetes"}) 109 app := f.MakeApplication(c, &factory.ApplicationParams{Charm: ch}) 110 unit, err := app.AddUnit(state.AddUnitParams{}) 111 c.Assert(err, jc.ErrorIsNil) 112 113 err = model.Destroy(state.DestroyModelParams{}) 114 c.Assert(err, jc.ErrorIsNil) 115 err = model.Refresh() 116 c.Assert(err, jc.ErrorIsNil) 117 c.Assert(model.Life(), gc.Equals, state.Dying) 118 119 assertCleanupCount(c, st, 3) 120 121 // App removal requires cluster resources to be cleared. 122 err = app.Refresh() 123 c.Assert(err, jc.ErrorIsNil) 124 err = app.ClearResources() 125 c.Assert(err, jc.ErrorIsNil) 126 assertCleanupCount(c, st, 2) 127 128 err = app.Refresh() 129 c.Assert(err, jc.Satisfies, errors.IsNotFound) 130 err = unit.Refresh() 131 c.Assert(err, jc.Satisfies, errors.IsNotFound) 132 assertDoesNotNeedCleanup(c, st) 133 } 134 135 func (s *CAASModelSuite) TestDestroyModelDestroyStorage(c *gc.C) { 136 model, st := s.newCAASModel(c) 137 broker, err := stateenvirons.GetNewCAASBrokerFunc(caas.New)(model) 138 c.Assert(err, jc.ErrorIsNil) 139 registry := stateenvirons.NewStorageProviderRegistry(broker) 140 s.policy = testing.MockPolicy{ 141 GetStorageProviderRegistry: func() (storage.ProviderRegistry, error) { 142 return registry, nil 143 }, 144 } 145 146 sb, err := state.NewStorageBackend(st) 147 c.Assert(err, jc.ErrorIsNil) 148 149 f := factory.NewFactory(st, s.StatePool) 150 app := f.MakeApplication(c, &factory.ApplicationParams{ 151 Charm: state.AddTestingCharmForSeries(c, st, "kubernetes", "storage-filesystem"), 152 Storage: map[string]state.StorageConstraints{ 153 "data": {Count: 1, Size: 1024}, 154 }, 155 }) 156 unit := f.MakeUnit(c, &factory.UnitParams{ 157 Application: app, 158 }) 159 160 si, err := sb.AllStorageInstances() 161 c.Assert(err, jc.ErrorIsNil) 162 c.Assert(si, gc.HasLen, 1) 163 fs, err := sb.AllFilesystems() 164 c.Assert(err, jc.ErrorIsNil) 165 c.Assert(fs, gc.HasLen, 1) 166 167 destroyStorage := true 168 err = model.Destroy(state.DestroyModelParams{DestroyStorage: &destroyStorage}) 169 c.Assert(err, jc.ErrorIsNil) 170 c.Assert(model.Refresh(), jc.ErrorIsNil) 171 c.Assert(model.Life(), gc.Equals, state.Dying) 172 173 assertNeedsCleanup(c, st) 174 assertCleanupCount(c, st, 4) 175 176 c.Assert(app.Refresh(), jc.ErrorIsNil) 177 c.Assert(app.Life(), gc.Equals, state.Dying) 178 c.Assert(unit.Refresh(), jc.ErrorIsNil) 179 c.Assert(unit.Life(), gc.Equals, state.Dying) 180 181 // The uniter would call this when it sees it is dying. 182 err = unit.EnsureDead() 183 c.Assert(err, jc.ErrorIsNil) 184 // The deployer or the caasapplicationprovisioner would call this once the unit is Dead. 185 err = unit.Remove() 186 c.Assert(err, jc.ErrorIsNil) 187 188 assertNeedsCleanup(c, st) 189 assertCleanupCount(c, st, 2) 190 191 // The caasapplicationprovisioner would call this when the app is gone from the cloud. 192 err = app.ClearResources() 193 c.Assert(err, jc.ErrorIsNil) 194 195 assertNeedsCleanup(c, st) 196 assertCleanupCount(c, st, 2) 197 198 si, err = sb.AllStorageInstances() 199 c.Assert(err, jc.ErrorIsNil) 200 c.Assert(si, gc.HasLen, 0) 201 fs, err = sb.AllFilesystems() 202 c.Assert(err, jc.ErrorIsNil) 203 c.Assert(fs, gc.HasLen, 0) 204 205 vols, err := sb.AllVolumes() 206 c.Assert(err, jc.ErrorIsNil) 207 c.Assert(vols, gc.HasLen, 1) 208 c.Assert(vols[0].Life(), gc.Equals, state.Dying) 209 // A storage provisioner would call this. 210 err = sb.RemoveVolumeAttachment(unit.UnitTag(), vols[0].VolumeTag(), false) 211 c.Assert(err, jc.ErrorIsNil) 212 err = sb.RemoveVolume(vols[0].VolumeTag()) 213 c.Assert(err, jc.ErrorIsNil) 214 215 // Undertaker would call this. 216 err = st.ProcessDyingModel() 217 c.Assert(err, jc.ErrorIsNil) 218 err = st.RemoveDyingModel() 219 c.Assert(err, jc.ErrorIsNil) 220 } 221 222 func (s *CAASModelSuite) TestCAASModelWrongCloudRegion(c *gc.C) { 223 cfg, _ := s.createTestModelConfig(c) 224 _, _, err := s.Controller.NewModel(state.ModelArgs{ 225 Type: state.ModelTypeCAAS, 226 CloudName: "dummy", 227 CloudRegion: "fork", 228 Config: cfg, 229 Owner: names.NewUserTag("test@remote"), 230 StorageProviderRegistry: provider.CommonStorageProviders(), 231 }) 232 c.Assert(err, gc.ErrorMatches, `region "fork" not found \(expected one of \["dotty.region" "dummy-region" "nether-region" "unused-region"\]\)`) 233 } 234 235 func (s *CAASModelSuite) TestDestroyControllerAndHostedCAASModels(c *gc.C) { 236 st2 := s.Factory.MakeCAASModel(c, nil) 237 defer st2.Close() 238 239 f := factory.NewFactory(st2, s.StatePool) 240 ch := f.MakeCharm(c, &factory.CharmParams{Name: "gitlab", Series: "kubernetes"}) 241 app := f.MakeApplication(c, &factory.ApplicationParams{Charm: ch}) 242 243 controllerModel, err := s.State.Model() 244 c.Assert(err, jc.ErrorIsNil) 245 destroyStorage := true 246 c.Assert(controllerModel.Destroy(state.DestroyModelParams{ 247 DestroyHostedModels: true, 248 DestroyStorage: &destroyStorage, 249 }), jc.ErrorIsNil) 250 251 model, err := s.State.Model() 252 c.Assert(err, jc.ErrorIsNil) 253 c.Assert(model.Life(), gc.Equals, state.Dying) 254 255 assertNeedsCleanup(c, s.State) 256 assertCleanupRuns(c, s.State) 257 258 // Cleanups for hosted model enqueued by controller model cleanups. 259 assertNeedsCleanup(c, st2) 260 assertCleanupRuns(c, st2) 261 262 model2, err := st2.Model() 263 c.Assert(err, jc.ErrorIsNil) 264 c.Assert(model2.Life(), gc.Equals, state.Dying) 265 266 // App removal requires cluster resources to be cleared. 267 err = app.Refresh() 268 c.Assert(err, jc.ErrorIsNil) 269 err = app.ClearResources() 270 c.Assert(err, jc.ErrorIsNil) 271 assertCleanupCount(c, st2, 2) 272 273 c.Assert(st2.ProcessDyingModel(), jc.ErrorIsNil) 274 c.Assert(st2.RemoveDyingModel(), jc.ErrorIsNil) 275 276 c.Assert(model2.Refresh(), jc.Satisfies, errors.IsNotFound) 277 278 c.Assert(s.State.ProcessDyingModel(), jc.ErrorIsNil) 279 c.Assert(s.State.RemoveDyingModel(), jc.ErrorIsNil) 280 c.Assert(model.Refresh(), jc.Satisfies, errors.IsNotFound) 281 } 282 283 func (s *CAASModelSuite) TestDestroyControllerAndHostedCAASModelsWithResources(c *gc.C) { 284 otherSt := s.Factory.MakeCAASModel(c, nil) 285 defer otherSt.Close() 286 287 assertModel := func(model *state.Model, st *state.State, life state.Life, expectedApps int) { 288 c.Assert(model.Refresh(), jc.ErrorIsNil) 289 c.Assert(model.Life(), gc.Equals, life) 290 291 apps, err := st.AllApplications() 292 c.Assert(err, jc.ErrorIsNil) 293 c.Assert(apps, gc.HasLen, expectedApps) 294 } 295 296 // add some applications 297 otherModel, err := otherSt.Model() 298 c.Assert(err, jc.ErrorIsNil) 299 application := s.Factory.MakeApplication(c, &factory.ApplicationParams{Name: "gitlab"}) 300 c.Assert(err, jc.ErrorIsNil) 301 302 f := factory.NewFactory(otherSt, s.StatePool) 303 ch := f.MakeCharm(c, &factory.CharmParams{Name: "gitlab", Series: "kubernetes"}) 304 args := state.AddApplicationArgs{ 305 Name: application.Name(), 306 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 307 OS: "ubuntu", 308 Channel: "20.04/stable", 309 }}, 310 Charm: ch, 311 } 312 application2, err := otherSt.AddApplication(args) 313 c.Assert(err, jc.ErrorIsNil) 314 315 controllerModel, err := s.State.Model() 316 c.Assert(err, jc.ErrorIsNil) 317 destroyStorage := true 318 c.Assert(controllerModel.Destroy(state.DestroyModelParams{ 319 DestroyHostedModels: true, 320 DestroyStorage: &destroyStorage, 321 }), jc.ErrorIsNil) 322 323 assertCleanupCount(c, s.State, 2) 324 assertAllMachinesDeadAndRemove(c, s.State) 325 assertModel(controllerModel, s.State, state.Dying, 0) 326 327 err = s.State.ProcessDyingModel() 328 c.Assert(errors.Is(err, stateerrors.HasHostedModelsError), jc.IsTrue) 329 c.Assert(err, gc.ErrorMatches, `hosting 1 other model`) 330 331 assertCleanupCount(c, otherSt, 2) 332 333 // App removal requires cluster resources to be cleared. 334 err = application2.Refresh() 335 c.Assert(err, jc.ErrorIsNil) 336 err = application2.ClearResources() 337 c.Assert(err, jc.ErrorIsNil) 338 assertCleanupCount(c, otherSt, 2) 339 340 assertModel(otherModel, otherSt, state.Dying, 0) 341 c.Assert(otherSt.ProcessDyingModel(), jc.ErrorIsNil) 342 c.Assert(otherSt.RemoveDyingModel(), jc.ErrorIsNil) 343 344 c.Assert(otherModel.Refresh(), jc.Satisfies, errors.IsNotFound) 345 346 c.Assert(s.State.ProcessDyingModel(), jc.ErrorIsNil) 347 c.Assert(s.State.RemoveDyingModel(), jc.ErrorIsNil) 348 c.Assert(controllerModel.Refresh(), jc.Satisfies, errors.IsNotFound) 349 } 350 351 func (s *CAASModelSuite) TestContainers(c *gc.C) { 352 m, st := s.newCAASModel(c) 353 f := factory.NewFactory(st, s.StatePool) 354 ch := f.MakeCharm(c, &factory.CharmParams{ 355 Name: "gitlab", 356 Series: "kubernetes", 357 }) 358 app := f.MakeApplication(c, &factory.ApplicationParams{Charm: ch}) 359 360 _, err := app.AddUnit(state.AddUnitParams{ProviderId: strPtr("provider-id1")}) 361 c.Assert(err, jc.ErrorIsNil) 362 _, err = app.AddUnit(state.AddUnitParams{ProviderId: strPtr("provider-id2")}) 363 c.Assert(err, jc.ErrorIsNil) 364 365 containers, err := m.Containers("provider-id1", "provider-id2") 366 c.Assert(err, jc.ErrorIsNil) 367 c.Assert(containers, gc.HasLen, 2) 368 var unitNames []string 369 for _, c := range containers { 370 unitNames = append(unitNames, c.Unit()) 371 } 372 c.Assert(unitNames, jc.SameContents, []string{app.Name() + "/0", app.Name() + "/1"}) 373 } 374 375 func (s *CAASModelSuite) TestUnitStatusNoPodSpec(c *gc.C) { 376 m, st := s.newCAASModel(c) 377 f := factory.NewFactory(st, s.StatePool) 378 unit := f.MakeUnit(c, &factory.UnitParams{ 379 Status: &status.StatusInfo{ 380 Status: status.Waiting, 381 Message: status.MessageInitializingAgent, 382 }, 383 }) 384 385 msWorkload := unitWorkloadStatus(c, m, unit.Name(), false) 386 c.Check(msWorkload.Message, gc.Equals, "agent initialising") 387 c.Check(msWorkload.Status, gc.Equals, status.Waiting) 388 389 err := unit.SetStatus(status.StatusInfo{Status: status.Active, Message: "running"}) 390 c.Assert(err, jc.ErrorIsNil) 391 msWorkload = unitWorkloadStatus(c, m, unit.Name(), false) 392 c.Check(msWorkload.Message, gc.Equals, "running") 393 c.Check(msWorkload.Status, gc.Equals, status.Active) 394 } 395 396 func (s *CAASModelSuite) TestCloudContainerStatus(c *gc.C) { 397 m, st := s.newCAASModel(c) 398 f := factory.NewFactory(st, s.StatePool) 399 unit := f.MakeUnit(c, &factory.UnitParams{ 400 Status: &status.StatusInfo{ 401 Status: status.Active, 402 Message: "Unit Active", 403 }, 404 }) 405 406 // Cloud container overrides Allocating unit 407 setCloudContainerStatus(c, unit, status.Allocating, "k8s allocating") 408 msWorkload := unitWorkloadStatus(c, m, unit.Name(), true) 409 c.Check(msWorkload.Message, gc.Equals, "k8s allocating") 410 c.Check(msWorkload.Status, gc.Equals, status.Allocating) 411 412 // Cloud container error overrides unit status 413 setCloudContainerStatus(c, unit, status.Error, "k8s charm error") 414 msWorkload = unitWorkloadStatus(c, m, unit.Name(), true) 415 c.Check(msWorkload.Message, gc.Equals, "k8s charm error") 416 c.Check(msWorkload.Status, gc.Equals, status.Error) 417 418 // Unit status must be used. 419 setCloudContainerStatus(c, unit, status.Running, "k8s idle") 420 msWorkload = unitWorkloadStatus(c, m, unit.Name(), true) 421 c.Check(msWorkload.Message, gc.Equals, "Unit Active") 422 c.Check(msWorkload.Status, gc.Equals, status.Active) 423 424 // Cloud container overrides 425 setCloudContainerStatus(c, unit, status.Blocked, "POD storage issue") 426 msWorkload = unitWorkloadStatus(c, m, unit.Name(), true) 427 c.Check(msWorkload.Message, gc.Equals, "POD storage issue") 428 c.Check(msWorkload.Status, gc.Equals, status.Blocked) 429 430 // Cloud container overrides 431 setCloudContainerStatus(c, unit, status.Waiting, "Building the bits") 432 msWorkload = unitWorkloadStatus(c, m, unit.Name(), true) 433 c.Check(msWorkload.Message, gc.Equals, "Building the bits") 434 c.Check(msWorkload.Status, gc.Equals, status.Waiting) 435 436 // Cloud container overrides 437 setCloudContainerStatus(c, unit, status.Running, "Bits have been built") 438 msWorkload = unitWorkloadStatus(c, m, unit.Name(), true) 439 c.Check(msWorkload.Message, gc.Equals, "Unit Active") 440 c.Check(msWorkload.Status, gc.Equals, status.Active) 441 } 442 443 func (s *CAASModelSuite) TestCloudContainerHistoryOverwrite(c *gc.C) { 444 m, st := s.newCAASModel(c) 445 f := factory.NewFactory(st, s.StatePool) 446 unit := f.MakeUnit(c, &factory.UnitParams{}) 447 448 workloadStatus := unitWorkloadStatus(c, m, unit.Name(), true) 449 c.Assert(workloadStatus.Message, gc.Equals, status.MessageWaitForContainer) 450 c.Assert(workloadStatus.Status, gc.Equals, status.Waiting) 451 statusHistory, err := unit.StatusHistory(status.StatusHistoryFilter{Size: 10}) 452 c.Assert(err, jc.ErrorIsNil) 453 c.Assert(statusHistory, gc.HasLen, 1) 454 c.Assert(statusHistory[0].Message, gc.Equals, status.MessageInstallingAgent) 455 c.Assert(statusHistory[0].Status, gc.Equals, status.Waiting) 456 457 err = unit.SetStatus(status.StatusInfo{ 458 Status: status.Active, 459 Message: "Unit Active", 460 }) 461 c.Assert(err, jc.ErrorIsNil) 462 unitStatus, err := unit.Status() 463 c.Assert(err, jc.ErrorIsNil) 464 c.Assert(unitStatus.Message, gc.Equals, "Unit Active") 465 c.Assert(unitStatus.Status, gc.Equals, status.Active) 466 467 // Now that status is stored as Active, but displayed (and in history) 468 // as waiting for container, once we set cloud container status as active 469 // it must show active from the unit (incl. history) 470 setCloudContainerStatus(c, unit, status.Running, "Container Active") 471 workloadStatus = unitWorkloadStatus(c, m, unit.Name(), true) 472 c.Assert(workloadStatus.Message, gc.Equals, "Unit Active") 473 c.Assert(workloadStatus.Status, gc.Equals, status.Active) 474 statusHistory, err = unit.StatusHistory(status.StatusHistoryFilter{Size: 10}) 475 c.Assert(err, jc.ErrorIsNil) 476 c.Assert(statusHistory, gc.HasLen, 2) 477 c.Assert(statusHistory[0].Message, gc.Equals, "Unit Active") 478 c.Assert(statusHistory[0].Status, gc.Equals, status.Active) 479 c.Assert(statusHistory[1].Message, gc.Equals, status.MessageInstallingAgent) 480 c.Assert(statusHistory[1].Status, gc.Equals, status.Waiting) 481 482 err = unit.SetStatus(status.StatusInfo{ 483 Status: status.Waiting, 484 Message: "This is a different message", 485 }) 486 c.Assert(err, jc.ErrorIsNil) 487 workloadStatus = unitWorkloadStatus(c, m, unit.Name(), true) 488 c.Assert(workloadStatus.Message, gc.Equals, "This is a different message") 489 c.Assert(workloadStatus.Status, gc.Equals, status.Waiting) 490 statusHistory, err = unit.StatusHistory(status.StatusHistoryFilter{Size: 10}) 491 c.Assert(err, jc.ErrorIsNil) 492 c.Assert(statusHistory, gc.HasLen, 3) 493 c.Assert(statusHistory[0].Message, gc.Equals, "This is a different message") 494 c.Assert(statusHistory[0].Status, gc.Equals, status.Waiting) 495 c.Assert(statusHistory[1].Message, gc.Equals, "Unit Active") 496 c.Assert(statusHistory[1].Status, gc.Equals, status.Active) 497 c.Assert(statusHistory[2].Message, gc.Equals, status.MessageInstallingAgent) 498 c.Assert(statusHistory[2].Status, gc.Equals, status.Waiting) 499 } 500 501 func unitWorkloadStatus(c *gc.C, model *state.CAASModel, unitName string, expectWorkload bool) status.StatusInfo { 502 ms, err := model.LoadModelStatus() 503 c.Assert(err, jc.ErrorIsNil) 504 msWorkload, err := ms.UnitWorkload(unitName, expectWorkload) 505 c.Assert(err, jc.ErrorIsNil) 506 return msWorkload 507 } 508 509 func setCloudContainerStatus(c *gc.C, unit *state.Unit, statusCode status.Status, message string) { 510 var updateUnits state.UpdateUnitsOperation 511 updateUnits.Updates = []*state.UpdateUnitOperation{ 512 unit.UpdateOperation(state.UnitUpdateProperties{ 513 CloudContainerStatus: &status.StatusInfo{Status: statusCode, Message: message}, 514 })} 515 app, err := unit.Application() 516 c.Assert(err, jc.ErrorIsNil) 517 err = app.UpdateUnits(&updateUnits) 518 c.Assert(err, jc.ErrorIsNil) 519 }