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 }