github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/provisioner/containerworker_test.go (about) 1 // Copyright 2022 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package provisioner_test 5 6 import ( 7 "sync" 8 "time" 9 10 "github.com/juju/names/v5" 11 jc "github.com/juju/testing/checkers" 12 "github.com/juju/utils/v3" 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/agent" 19 apiprovisioner "github.com/juju/juju/api/agent/provisioner" 20 provisionermocks "github.com/juju/juju/api/agent/provisioner/mocks" 21 apimocks "github.com/juju/juju/api/base/mocks" 22 "github.com/juju/juju/container" 23 "github.com/juju/juju/container/factory" 24 "github.com/juju/juju/container/testing" 25 "github.com/juju/juju/core/instance" 26 "github.com/juju/juju/core/life" 27 "github.com/juju/juju/core/machinelock" 28 "github.com/juju/juju/core/network" 29 "github.com/juju/juju/core/watcher" 30 "github.com/juju/juju/rpc/params" 31 coretesting "github.com/juju/juju/testing" 32 jujuversion "github.com/juju/juju/version" 33 "github.com/juju/juju/worker/provisioner" 34 "github.com/juju/juju/worker/provisioner/mocks" 35 ) 36 37 type containerWorkerSuite struct { 38 coretesting.BaseSuite 39 40 modelUUID utils.UUID 41 controllerUUID utils.UUID 42 43 initialiser *testing.MockInitialiser 44 facadeCaller *apimocks.MockFacadeCaller 45 machine *provisionermocks.MockMachineProvisioner 46 manager *testing.MockManager 47 stringsWatcher *mocks.MockStringsWatcher 48 49 machineLock *fakeMachineLock 50 51 // The done channel is used by tests to indicate that 52 // the worker has accomplished the scenario and can be stopped. 53 done chan struct{} 54 } 55 56 func (s *containerWorkerSuite) SetUpTest(c *gc.C) { 57 s.BaseSuite.SetUpTest(c) 58 59 s.modelUUID = utils.MustNewUUID() 60 s.controllerUUID = utils.MustNewUUID() 61 62 s.machineLock = &fakeMachineLock{} 63 s.done = make(chan struct{}) 64 } 65 66 var _ = gc.Suite(&containerWorkerSuite{}) 67 68 func (s *containerWorkerSuite) TestContainerSetupAndProvisioner(c *gc.C) { 69 defer s.patch(c).Finish() 70 71 // Adding one new container machine. 72 s.notify([]string{"0/lxd/0"}) 73 74 s.expectContainerManagerConfig("lxd") 75 s.initialiser.EXPECT().Initialise().Return(nil) 76 77 s.PatchValue( 78 &factory.NewContainerManager, 79 func(forType instance.ContainerType, conf container.ManagerConfig) (container.Manager, error) { 80 return s.manager, nil 81 }) 82 83 w := s.setUpContainerWorker(c) 84 work, ok := w.(*provisioner.ContainerSetupAndProvisioner) 85 c.Assert(ok, jc.IsTrue) 86 87 // Watch the worker report. We are waiting for the lxd-provisioner 88 // to be started. 89 workers := make(chan struct{}, 1) 90 defer close(workers) 91 go func() { 92 for { 93 rep := work.Report() 94 if _, ok := rep["lxd-provisioner"].(string); ok { 95 workers <- struct{}{} 96 return 97 } 98 time.Sleep(time.Millisecond) 99 } 100 }() 101 102 // Check that the provisioner is there. 103 select { 104 case _, ok := <-workers: 105 c.Check(ok, jc.IsTrue) 106 case <-time.After(coretesting.LongWait): 107 c.Errorf("timed out waiting for runner to start all workers") 108 } 109 110 s.cleanKill(c, w) 111 } 112 113 func (s *containerWorkerSuite) TestContainerSetupAndProvisionerErrWatcherClose(c *gc.C) { 114 ctrl := gomock.NewController(c) 115 defer ctrl.Finish() 116 117 s.initialiser = testing.NewMockInitialiser(ctrl) 118 s.facadeCaller = apimocks.NewMockFacadeCaller(ctrl) 119 s.stringsWatcher = mocks.NewMockStringsWatcher(ctrl) 120 s.machine = provisionermocks.NewMockMachineProvisioner(ctrl) 121 s.manager = testing.NewMockManager(ctrl) 122 s.machine.EXPECT().MachineTag().Return(names.NewMachineTag("0")).AnyTimes() 123 124 s.stringsWatcher.EXPECT().Kill().AnyTimes() 125 s.stringsWatcher.EXPECT().Wait().AnyTimes() 126 s.stringsWatcher.EXPECT().Changes().DoAndReturn( 127 func() <-chan []string { 128 // Kill the worker while waiting for 129 // a container to be provisioned. 130 close(s.done) 131 return nil 132 }).AnyTimes() 133 134 s.PatchValue( 135 &factory.NewContainerManager, 136 func(forType instance.ContainerType, conf container.ManagerConfig) (container.Manager, error) { 137 return s.manager, nil 138 }) 139 140 w := s.setUpContainerWorker(c) 141 142 s.cleanKill(c, w) 143 } 144 145 func (s *containerWorkerSuite) setUpContainerWorker(c *gc.C) worker.Worker { 146 pState := apiprovisioner.NewStateFromFacade(s.facadeCaller) 147 148 cfg, err := agent.NewAgentConfig( 149 agent.AgentConfigParams{ 150 Paths: agent.DefaultPaths, 151 Tag: s.machine.MachineTag(), 152 UpgradedToVersion: jujuversion.Current, 153 Password: "password", 154 Nonce: "nonce", 155 APIAddresses: []string{"0.0.0.0:12345"}, 156 CACert: coretesting.CACert, 157 Controller: names.NewControllerTag(s.controllerUUID.String()), 158 Model: names.NewModelTag(s.modelUUID.String()), 159 }) 160 c.Assert(err, jc.ErrorIsNil) 161 162 args := provisioner.ContainerSetupParams{ 163 Logger: noOpLogger{}, 164 ContainerType: instance.LXD, 165 MachineZone: s.machine, 166 MTag: s.machine.MachineTag(), 167 Provisioner: pState, 168 Config: cfg, 169 MachineLock: s.machineLock, 170 CredentialAPI: &credentialAPIForTest{}, 171 GetNetConfig: func(_ network.ConfigSource) (network.InterfaceInfos, error) { 172 return nil, nil 173 }, 174 } 175 cs := provisioner.NewContainerSetup(args) 176 177 // Stub out network config getter. 178 watcherFunc := func() (watcher.StringsWatcher, error) { 179 return s.stringsWatcher, nil 180 } 181 w, err := provisioner.NewContainerSetupAndProvisioner(cs, watcherFunc) 182 c.Assert(err, jc.ErrorIsNil) 183 184 return w 185 } 186 187 func (s *containerWorkerSuite) patch(c *gc.C) *gomock.Controller { 188 ctrl := gomock.NewController(c) 189 190 s.initialiser = testing.NewMockInitialiser(ctrl) 191 s.facadeCaller = apimocks.NewMockFacadeCaller(ctrl) 192 s.stringsWatcher = mocks.NewMockStringsWatcher(ctrl) 193 s.machine = provisionermocks.NewMockMachineProvisioner(ctrl) 194 s.manager = testing.NewMockManager(ctrl) 195 196 s.stubOutProvisioner(ctrl) 197 198 s.machine.EXPECT().Id().Return("0").AnyTimes() 199 s.machine.EXPECT().MachineTag().Return(names.NewMachineTag("0")).AnyTimes() 200 201 s.PatchValue(provisioner.GetContainerInitialiser, func(instance.ContainerType, map[string]string, string) container.Initialiser { 202 return s.initialiser 203 }) 204 205 s.manager.EXPECT().ListContainers().Return(nil, nil).AnyTimes() 206 207 return ctrl 208 } 209 210 // stubOutProvisioner is used to effectively ignore provisioner calls that we 211 // do not care about for testing container provisioning. 212 // The bulk of the calls mocked here are called in 213 // authentication.NewAPIAuthenticator, which is passed the provisioner's 214 // client-side state by the provisioner worker. 215 func (s *containerWorkerSuite) stubOutProvisioner(ctrl *gomock.Controller) { 216 // We could have mocked only the base caller and not the FacadeCaller, 217 // but expectations would be verbose to the point of obfuscation. 218 // So we only mock the base caller for calls that use it directly, 219 // such as watcher acquisition. 220 caller := apimocks.NewMockAPICaller(ctrl) 221 cExp := caller.EXPECT() 222 cExp.BestFacadeVersion(gomock.Any()).Return(0).AnyTimes() 223 cExp.APICall("NotifyWatcher", 0, gomock.Any(), gomock.Any(), nil, gomock.Any()).Return(nil).AnyTimes() 224 cExp.APICall("StringsWatcher", 0, gomock.Any(), gomock.Any(), nil, gomock.Any()).Return(nil).AnyTimes() 225 226 fExp := s.facadeCaller.EXPECT() 227 fExp.RawAPICaller().Return(caller).AnyTimes() 228 229 notifySource := params.NotifyWatchResult{NotifyWatcherId: "who-cares"} 230 fExp.FacadeCall("WatchForModelConfigChanges", nil, gomock.Any()).SetArg(2, notifySource).Return(nil).AnyTimes() 231 232 modelCfgSource := params.ModelConfigResult{ 233 Config: map[string]interface{}{ 234 "uuid": s.modelUUID.String(), 235 "type": "maas", 236 "name": "container-init-test-model", 237 "secret-backend": "auto", 238 }, 239 } 240 fExp.FacadeCall("ModelConfig", nil, gomock.Any()).SetArg(2, modelCfgSource).Return(nil).AnyTimes() 241 242 addrSource := params.StringsResult{Result: []string{"0.0.0.0"}} 243 fExp.FacadeCall("StateAddresses", nil, gomock.Any()).SetArg(2, addrSource).Return(nil).AnyTimes() 244 fExp.FacadeCall("APIAddresses", nil, gomock.Any()).SetArg(2, addrSource).Return(nil).AnyTimes() 245 246 certSource := params.BytesResult{Result: []byte(coretesting.CACert)} 247 fExp.FacadeCall("CACert", nil, gomock.Any()).SetArg(2, certSource).Return(nil).AnyTimes() 248 249 uuidSource := params.StringResult{Result: s.modelUUID.String()} 250 fExp.FacadeCall("ModelUUID", nil, gomock.Any()).SetArg(2, uuidSource).Return(nil).AnyTimes() 251 252 lifeSource := params.LifeResults{Results: []params.LifeResult{{Life: life.Alive}}} 253 fExp.FacadeCall("Life", gomock.Any(), gomock.Any()).SetArg(2, lifeSource).Return(nil).AnyTimes() 254 255 watchSource := params.StringsWatchResults{Results: []params.StringsWatchResult{{ 256 StringsWatcherId: "whatever", 257 Changes: []string{}, 258 }}} 259 fExp.FacadeCall("WatchContainers", gomock.Any(), gomock.Any()).SetArg(2, watchSource).Return(nil).AnyTimes() 260 fExp.FacadeCall("WatchContainersCharmProfiles", gomock.Any(), gomock.Any()).SetArg(2, watchSource).Return(nil).AnyTimes() 261 262 controllerCfgSource := params.ControllerConfigResult{ 263 Config: map[string]interface{}{"controller-uuid": s.controllerUUID.String()}, 264 } 265 fExp.FacadeCall("ControllerConfig", nil, gomock.Any()).SetArg(2, controllerCfgSource).Return(nil).AnyTimes() 266 } 267 268 // notify returns a suite behaviour that will cause the container watcher 269 // to send a number of notifications equal to the supplied argument. 270 // Once notifications have been consumed, we notify via the suite's channel. 271 func (s *containerWorkerSuite) notify(messages ...[]string) { 272 ch := make(chan []string) 273 274 go func() { 275 for _, m := range messages { 276 ch <- m 277 } 278 close(s.done) 279 }() 280 281 s.stringsWatcher.EXPECT().Kill().AnyTimes() 282 s.stringsWatcher.EXPECT().Wait().Return(nil).AnyTimes() 283 s.stringsWatcher.EXPECT().Changes().Return(ch) 284 } 285 286 // expectContainerManagerConfig sets up expectations associated with 287 // acquisition and decoration of container manager configuration. 288 func (s *containerWorkerSuite) expectContainerManagerConfig(cType instance.ContainerType) { 289 resultSource := params.ContainerManagerConfig{ 290 ManagerConfig: map[string]string{"model-uuid": s.modelUUID.String()}, 291 } 292 s.facadeCaller.EXPECT().FacadeCall( 293 "ContainerManagerConfig", params.ContainerManagerConfigParams{Type: cType}, gomock.Any(), 294 ).SetArg(2, resultSource).MinTimes(1) 295 296 s.machine.EXPECT().AvailabilityZone().Return("az1", nil) 297 } 298 299 // cleanKill waits for notifications to be processed, then waits for the input 300 // worker to be killed cleanly. If either ops time out, the test fails. 301 func (s *containerWorkerSuite) cleanKill(c *gc.C, w worker.Worker) { 302 select { 303 case <-s.done: 304 case <-time.After(coretesting.LongWait): 305 c.Errorf("timed out waiting for notifications to be consumed") 306 } 307 workertest.CleanKill(c, w) 308 } 309 310 type fakeMachineLock struct { 311 mu sync.Mutex 312 } 313 314 func (f *fakeMachineLock) Acquire(spec machinelock.Spec) (func(), error) { 315 f.mu.Lock() 316 return func() { 317 f.mu.Unlock() 318 }, nil 319 } 320 321 func (f *fakeMachineLock) Report(opts ...machinelock.ReportOption) (string, error) { 322 return "", nil 323 } 324 325 type noOpLogger struct{} 326 327 func (noOpLogger) Errorf(format string, values ...interface{}) {} 328 func (noOpLogger) Warningf(format string, values ...interface{}) {} 329 func (noOpLogger) Infof(format string, values ...interface{}) {} 330 func (noOpLogger) Debugf(format string, values ...interface{}) {} 331 func (noOpLogger) Tracef(format string, values ...interface{}) {} 332 func (noOpLogger) IsTraceEnabled() bool { return false }