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:          &params.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  }