github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/container/broker/broker_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package broker_test 5 6 import ( 7 stdcontext "context" 8 "net" 9 "os" 10 "path/filepath" 11 12 "github.com/juju/loggo" 13 "github.com/juju/names/v5" 14 gitjujutesting "github.com/juju/testing" 15 jc "github.com/juju/testing/checkers" 16 "github.com/juju/version/v2" 17 gc "gopkg.in/check.v1" 18 19 apiprovisioner "github.com/juju/juju/api/agent/provisioner" 20 "github.com/juju/juju/cloudconfig" 21 "github.com/juju/juju/cloudconfig/instancecfg" 22 "github.com/juju/juju/container" 23 "github.com/juju/juju/container/broker" 24 "github.com/juju/juju/core/arch" 25 corebase "github.com/juju/juju/core/base" 26 "github.com/juju/juju/core/constraints" 27 "github.com/juju/juju/core/instance" 28 "github.com/juju/juju/core/lxdprofile" 29 corenetwork "github.com/juju/juju/core/network" 30 "github.com/juju/juju/core/status" 31 "github.com/juju/juju/environs" 32 "github.com/juju/juju/environs/context" 33 "github.com/juju/juju/environs/instances" 34 "github.com/juju/juju/environs/instances/instancetest" 35 jujutesting "github.com/juju/juju/juju/testing" 36 "github.com/juju/juju/network" 37 "github.com/juju/juju/rpc/params" 38 coretesting "github.com/juju/juju/testing" 39 coretools "github.com/juju/juju/tools" 40 ) 41 42 type brokerSuite struct { 43 coretesting.BaseSuite 44 } 45 46 var _ = gc.Suite(&brokerSuite{}) 47 48 func (s *brokerSuite) SetUpSuite(c *gc.C) { 49 s.BaseSuite.SetUpSuite(c) 50 broker.PatchNewMachineInitReader(s, newFakeMachineInitReader) 51 } 52 53 func (s *brokerSuite) TestCombinedCloudInitDataNoCloudInitUserData(c *gc.C) { 54 obtained, err := broker.CombinedCloudInitData(nil, "ca-certs,apt-primary", 55 corebase.MakeDefaultBase("ubuntu", "16.04"), loggo.Logger{}) 56 c.Assert(err, jc.ErrorIsNil) 57 58 assertCloudInitUserData(obtained, map[string]interface{}{ 59 "apt": map[string]interface{}{ 60 "primary": []interface{}{ 61 map[interface{}]interface{}{ 62 "arches": []interface{}{"default"}, 63 "uri": "http://archive.ubuntu.com/ubuntu", 64 }, 65 }, 66 }, 67 "ca-certs": map[interface{}]interface{}{ 68 "remove-defaults": true, 69 "trusted": []interface{}{"-----BEGIN CERTIFICATE-----\nYOUR-ORGS-TRUSTED-CA-CERT-HERE\n-----END CERTIFICATE-----\n"}, 70 }, 71 }, c) 72 } 73 74 func (s *brokerSuite) TestCombinedCloudInitDataNoContainerInheritProperties(c *gc.C) { 75 containerConfig := fakeContainerConfig() 76 obtained, err := broker.CombinedCloudInitData(containerConfig.CloudInitUserData, "", 77 corebase.MakeDefaultBase("ubuntu", "16.04"), loggo.Logger{}) 78 c.Assert(err, jc.ErrorIsNil) 79 assertCloudInitUserData(obtained, containerConfig.CloudInitUserData, c) 80 } 81 82 type fakeAddr struct{ value string } 83 84 func (f *fakeAddr) Network() string { return "net" } 85 func (f *fakeAddr) String() string { 86 if f.value != "" { 87 return f.value 88 } 89 return "fakeAddr" 90 } 91 92 var _ net.Addr = (*fakeAddr)(nil) 93 94 type fakeAPI struct { 95 *gitjujutesting.Stub 96 97 fakeContainerConfig params.ContainerConfig 98 fakeInterfaceInfo corenetwork.InterfaceInfo 99 fakeDeviceToBridge network.DeviceToBridge 100 fakePreparer broker.PrepareHostFunc 101 } 102 103 var _ broker.APICalls = (*fakeAPI)(nil) 104 105 var fakeInterfaceInfo = corenetwork.InterfaceInfo{ 106 DeviceIndex: 0, 107 MACAddress: "aa:bb:cc:dd:ee:ff", 108 InterfaceName: "dummy0", 109 Addresses: corenetwork.ProviderAddresses{ 110 corenetwork.NewMachineAddress("0.1.2.3", corenetwork.WithCIDR("0.1.2.0/24")).AsProviderAddress(), 111 }, 112 GatewayAddress: corenetwork.NewMachineAddress("0.1.2.1").AsProviderAddress(), 113 // Explicitly set only DNSServers, but not DNSSearchDomains to test this is 114 // detected and the latter populated by parsing the fake resolv.conf created 115 // by patchResolvConf(). See LP bug http://pad.lv/1575940 for more info. 116 DNSServers: corenetwork.NewMachineAddresses([]string{"ns1.dummy"}).AsProviderAddresses(), 117 DNSSearchDomains: nil, 118 } 119 120 func fakeContainerConfig() params.ContainerConfig { 121 return params.ContainerConfig{ 122 UpdateBehavior: ¶ms.UpdateBehavior{true, true}, 123 ProviderType: "fake", 124 AuthorizedKeys: coretesting.FakeAuthKeys, 125 SSLHostnameVerification: true, 126 CloudInitUserData: map[string]interface{}{ 127 "packages": []interface{}{"python-keystoneclient", "python-glanceclient"}, 128 "preruncmd": []interface{}{"mkdir /tmp/preruncmd", "mkdir /tmp/preruncmd2"}, 129 "postruncmd": []interface{}{"mkdir /tmp/postruncmd", "mkdir /tmp/postruncmd2"}, 130 "package_upgrade": false, 131 }, 132 } 133 } 134 135 func NewFakeAPI() *fakeAPI { 136 return &fakeAPI{ 137 Stub: &gitjujutesting.Stub{}, 138 fakeContainerConfig: fakeContainerConfig(), 139 fakeInterfaceInfo: fakeInterfaceInfo, 140 } 141 } 142 143 func (f *fakeAPI) ContainerConfig() (params.ContainerConfig, error) { 144 f.MethodCall(f, "ContainerConfig") 145 if err := f.NextErr(); err != nil { 146 return params.ContainerConfig{}, err 147 } 148 return f.fakeContainerConfig, nil 149 } 150 151 func (f *fakeAPI) PrepareContainerInterfaceInfo(tag names.MachineTag) (corenetwork.InterfaceInfos, error) { 152 f.MethodCall(f, "PrepareContainerInterfaceInfo", tag) 153 if err := f.NextErr(); err != nil { 154 return nil, err 155 } 156 return corenetwork.InterfaceInfos{f.fakeInterfaceInfo}, nil 157 } 158 159 func (f *fakeAPI) GetContainerInterfaceInfo(tag names.MachineTag) (corenetwork.InterfaceInfos, error) { 160 f.MethodCall(f, "GetContainerInterfaceInfo", tag) 161 if err := f.NextErr(); err != nil { 162 return nil, err 163 } 164 return corenetwork.InterfaceInfos{f.fakeInterfaceInfo}, nil 165 } 166 167 func (f *fakeAPI) ReleaseContainerAddresses(tag names.MachineTag) error { 168 f.MethodCall(f, "ReleaseContainerAddresses", tag) 169 if err := f.NextErr(); err != nil { 170 return err 171 } 172 return nil 173 } 174 175 func (f *fakeAPI) SetHostMachineNetworkConfig(hostMachineTag names.MachineTag, netConfig []params.NetworkConfig) error { 176 f.MethodCall(f, "SetHostMachineNetworkConfig", hostMachineTag.String(), netConfig) 177 if err := f.NextErr(); err != nil { 178 return err 179 } 180 return nil 181 } 182 183 func (f *fakeAPI) HostChangesForContainer(machineTag names.MachineTag) ([]network.DeviceToBridge, int, error) { 184 f.MethodCall(f, "HostChangesForContainer", machineTag) 185 if err := f.NextErr(); err != nil { 186 return nil, 0, err 187 } 188 return []network.DeviceToBridge{f.fakeDeviceToBridge}, 0, nil 189 } 190 191 func (f *fakeAPI) PrepareHost(containerTag names.MachineTag, log loggo.Logger, abort <-chan struct{}) error { 192 // This is not actually part of the API, however it is something that the 193 // Brokers should be calling, and putting it here means we get a wholistic 194 // view of when what function is getting called. 195 f.MethodCall(f, "PrepareHost", containerTag) 196 if err := f.NextErr(); err != nil { 197 return err 198 } 199 if f.fakePreparer != nil { 200 return f.fakePreparer(containerTag, log, abort) 201 } 202 return nil 203 } 204 205 func (f *fakeAPI) GetContainerProfileInfo(containerTag names.MachineTag) ([]*apiprovisioner.LXDProfileResult, error) { 206 f.MethodCall(f, "GetContainerProfileInfo", containerTag) 207 if err := f.NextErr(); err != nil { 208 return nil, err 209 } 210 return []*apiprovisioner.LXDProfileResult{}, nil 211 } 212 213 type fakeContainerManager struct { 214 gitjujutesting.Stub 215 } 216 217 func (m *fakeContainerManager) CreateContainer(_ stdcontext.Context, 218 instanceConfig *instancecfg.InstanceConfig, 219 cons constraints.Value, 220 base corebase.Base, 221 network *container.NetworkConfig, 222 storage *container.StorageConfig, 223 callback environs.StatusCallbackFunc, 224 ) (instances.Instance, *instance.HardwareCharacteristics, error) { 225 m.MethodCall(m, "CreateContainer", instanceConfig, cons, base, network, storage, callback) 226 inst := mockInstance{id: "testinst"} 227 arch := "testarch" 228 hw := instance.HardwareCharacteristics{Arch: &arch} 229 return &inst, &hw, m.NextErr() 230 } 231 232 func (m *fakeContainerManager) DestroyContainer(id instance.Id) error { 233 m.MethodCall(m, "DestroyContainer", id) 234 return m.NextErr() 235 } 236 237 func (m *fakeContainerManager) ListContainers() ([]instances.Instance, error) { 238 m.MethodCall(m, "ListContainers") 239 return nil, m.NextErr() 240 } 241 242 func (m *fakeContainerManager) Namespace() instance.Namespace { 243 ns, _ := instance.NewNamespace(coretesting.ModelTag.Id()) 244 return ns 245 } 246 247 func (m *fakeContainerManager) IsInitialized() bool { 248 m.MethodCall(m, "IsInitialized") 249 m.PopNoErr() 250 return true 251 } 252 253 func (m *fakeContainerManager) MaybeWriteLXDProfile(pName string, put lxdprofile.Profile) error { 254 m.MethodCall(m, "MaybeWriteLXDProfile") 255 return m.NextErr() 256 } 257 258 type mockInstance struct { 259 id string 260 } 261 262 var _ instances.Instance = (*mockInstance)(nil) 263 264 // Id implements instances.instance.Id. 265 func (m *mockInstance) Id() instance.Id { 266 return instance.Id(m.id) 267 } 268 269 // Status implements instances.Instance.Status. 270 func (m *mockInstance) Status(context.ProviderCallContext) instance.Status { 271 return instance.Status{} 272 } 273 274 // Addresses implements instances.Instance.Addresses. 275 func (m *mockInstance) Addresses(context.ProviderCallContext) (corenetwork.ProviderAddresses, error) { 276 return nil, nil 277 } 278 279 type patcher interface { 280 PatchValue(destination, source interface{}) 281 } 282 283 func patchResolvConf(s patcher, c *gc.C) { 284 const fakeConf = ` 285 nameserver ns1.dummy 286 search dummy invalid 287 nameserver ns2.dummy 288 ` 289 290 fakeResolvConf := filepath.Join(c.MkDir(), "fakeresolv.conf") 291 err := os.WriteFile(fakeResolvConf, []byte(fakeConf), 0644) 292 c.Assert(err, jc.ErrorIsNil) 293 s.PatchValue(broker.ResolvConfFiles, []string{fakeResolvConf}) 294 } 295 296 func instancesFromResults(results ...*environs.StartInstanceResult) []instances.Instance { 297 instances := make([]instances.Instance, len(results)) 298 for i := range results { 299 instances[i] = results[i].Instance 300 } 301 return instances 302 } 303 304 func assertInstancesStarted(c *gc.C, broker environs.InstanceBroker, results ...*environs.StartInstanceResult) { 305 allInstances, err := broker.AllRunningInstances(context.NewEmptyCloudCallContext()) 306 c.Assert(err, jc.ErrorIsNil) 307 instancetest.MatchInstances(c, allInstances, instancesFromResults(results...)...) 308 } 309 310 func makeInstanceConfig(c *gc.C, s patcher, machineId string) *instancecfg.InstanceConfig { 311 machineNonce := "fake-nonce" 312 // To isolate the tests from the host's architecture, we override it here. 313 s.PatchValue(&arch.HostArch, func() string { return arch.AMD64 }) 314 apiInfo := jujutesting.FakeAPIInfo(machineId) 315 instanceConfig, err := instancecfg.NewInstanceConfig(coretesting.ControllerTag, machineId, machineNonce, 316 "released", corebase.MakeDefaultBase("ubuntu", "22.04"), apiInfo) 317 c.Assert(err, jc.ErrorIsNil) 318 return instanceConfig 319 } 320 321 func makePossibleTools() coretools.List { 322 return coretools.List{&coretools.Tools{ 323 Version: version.MustParseBinary("2.3.4-ubuntu-amd64"), 324 URL: "http://tools.testing.invalid/2.3.4-ubuntu-amd64.tgz", 325 }, { 326 // non-host-arch tools should be filtered out by StartInstance 327 Version: version.MustParseBinary("2.3.4-ubuntu-arm64"), 328 URL: "http://tools.testing.invalid/2.3.4-ubuntu-arm64.tgz", 329 }} 330 } 331 332 func makeNoOpStatusCallback() func(settableStatus status.Status, info string, data map[string]interface{}) error { 333 return func(_ status.Status, _ string, _ map[string]interface{}) error { 334 return nil 335 } 336 } 337 338 func callStartInstance(c *gc.C, s patcher, broker environs.InstanceBroker, machineId string) (*environs.StartInstanceResult, error) { 339 return broker.StartInstance(context.NewEmptyCloudCallContext(), environs.StartInstanceParams{ 340 Constraints: constraints.Value{}, 341 Tools: makePossibleTools(), 342 InstanceConfig: makeInstanceConfig(c, s, machineId), 343 StatusCallback: makeNoOpStatusCallback(), 344 }) 345 } 346 347 func assertCloudInitUserData(obtained, expected map[string]interface{}, c *gc.C) { 348 c.Assert(obtained, gc.HasLen, len(expected)) 349 for obtainedK, obtainedV := range obtained { 350 expectedV, ok := expected[obtainedK] 351 c.Assert(ok, jc.IsTrue) 352 switch obtainedK { 353 case "package_upgrade": 354 c.Assert(obtainedV, gc.Equals, expectedV) 355 case "apt", "ca-certs": 356 c.Assert(obtainedV, jc.DeepEquals, expectedV) 357 default: 358 c.Assert(obtainedV, jc.SameContents, expectedV) 359 } 360 } 361 } 362 363 type fakeMachineInitReader struct { 364 cloudconfig.InitReader 365 } 366 367 func (r *fakeMachineInitReader) GetInitConfig() (map[string]interface{}, error) { 368 return map[string]interface{}{ 369 "packages": []interface{}{"python-novaclient"}, 370 "fake-entry": []interface{}{"testing-garbage"}, 371 "apt": map[interface{}]interface{}{ 372 "primary": []interface{}{ 373 map[interface{}]interface{}{ 374 "arches": []interface{}{"default"}, 375 "uri": "http://archive.ubuntu.com/ubuntu", 376 }, 377 }, 378 "security": []interface{}{ 379 map[interface{}]interface{}{ 380 "arches": []interface{}{"default"}, 381 "uri": "http://archive.ubuntu.com/ubuntu", 382 }, 383 }, 384 }, 385 "ca-certs": map[interface{}]interface{}{ 386 "remove-defaults": true, 387 "trusted": []interface{}{"-----BEGIN CERTIFICATE-----\nYOUR-ORGS-TRUSTED-CA-CERT-HERE\n-----END CERTIFICATE-----\n"}, 388 }, 389 }, nil 390 } 391 392 var newFakeMachineInitReader = func(base corebase.Base) (cloudconfig.InitReader, error) { 393 r, err := cloudconfig.NewMachineInitReader(base) 394 return &fakeMachineInitReader{r}, err 395 }