github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/provisioner/provisioner_task_test.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provisioner_test
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/golang/mock/gomock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/testing"
    16  	jc "github.com/juju/testing/checkers"
    17  	"github.com/juju/version"
    18  	gc "gopkg.in/check.v1"
    19  	"gopkg.in/juju/names.v2"
    20  	"gopkg.in/juju/worker.v1/workertest"
    21  
    22  	"github.com/juju/juju/api"
    23  	apiprovisioner "github.com/juju/juju/api/provisioner"
    24  	apiprovisionermock "github.com/juju/juju/api/provisioner/mocks"
    25  	"github.com/juju/juju/apiserver/params"
    26  	"github.com/juju/juju/controller/authentication"
    27  	"github.com/juju/juju/core/constraints"
    28  	"github.com/juju/juju/core/instance"
    29  	"github.com/juju/juju/core/lxdprofile"
    30  	"github.com/juju/juju/core/status"
    31  	"github.com/juju/juju/core/watcher"
    32  	"github.com/juju/juju/core/watcher/watchertest"
    33  	"github.com/juju/juju/environs"
    34  	"github.com/juju/juju/environs/config"
    35  	"github.com/juju/juju/environs/context"
    36  	"github.com/juju/juju/environs/imagemetadata"
    37  	"github.com/juju/juju/environs/instances"
    38  	jujuversion "github.com/juju/juju/juju/version"
    39  	"github.com/juju/juju/mongo"
    40  	"github.com/juju/juju/provider/common"
    41  	"github.com/juju/juju/provider/common/mocks"
    42  	coretesting "github.com/juju/juju/testing"
    43  	"github.com/juju/juju/worker/provisioner"
    44  	provisionermocks "github.com/juju/juju/worker/provisioner/mocks"
    45  )
    46  
    47  type ProvisionerTaskSuite struct {
    48  	testing.IsolationSuite
    49  
    50  	modelMachinesChanges chan []string
    51  	modelMachinesWatcher watcher.StringsWatcher
    52  
    53  	machineErrorRetryChanges chan struct{}
    54  	machineErrorRetryWatcher watcher.NotifyWatcher
    55  
    56  	modelMachinesProfileChanges chan []string
    57  	modelMachinesProfileWatcher watcher.StringsWatcher
    58  
    59  	machinesResults      []apiprovisioner.MachineResult
    60  	machineStatusResults []apiprovisioner.MachineStatusResult
    61  	machineGetter        *testMachineGetter
    62  
    63  	instances      []instances.Instance
    64  	instanceBroker *testInstanceBroker
    65  
    66  	callCtx           *context.CloudCallContext
    67  	invalidCredential bool
    68  
    69  	auth *testAuthenticationProvider
    70  }
    71  
    72  var _ = gc.Suite(&ProvisionerTaskSuite{})
    73  
    74  func (s *ProvisionerTaskSuite) SetUpTest(c *gc.C) {
    75  	s.IsolationSuite.SetUpTest(c)
    76  
    77  	s.modelMachinesChanges = make(chan []string)
    78  	s.modelMachinesWatcher = watchertest.NewMockStringsWatcher(s.modelMachinesChanges)
    79  
    80  	s.machineErrorRetryChanges = make(chan struct{})
    81  	s.machineErrorRetryWatcher = watchertest.NewMockNotifyWatcher(s.machineErrorRetryChanges)
    82  
    83  	s.modelMachinesProfileChanges = make(chan []string)
    84  	s.modelMachinesProfileWatcher = watchertest.NewMockStringsWatcher(s.modelMachinesProfileChanges)
    85  
    86  	s.machinesResults = []apiprovisioner.MachineResult{}
    87  	s.machineStatusResults = []apiprovisioner.MachineStatusResult{}
    88  	s.machineGetter = &testMachineGetter{
    89  		Stub: &testing.Stub{},
    90  		machinesFunc: func(machines ...names.MachineTag) ([]apiprovisioner.MachineResult, error) {
    91  			return s.machinesResults, nil
    92  		},
    93  		machinesWithTransientErrorsFunc: func() ([]apiprovisioner.MachineStatusResult, error) {
    94  			return s.machineStatusResults, nil
    95  		},
    96  	}
    97  
    98  	s.instances = []instances.Instance{}
    99  	s.instanceBroker = &testInstanceBroker{
   100  		Stub:      &testing.Stub{},
   101  		callsChan: make(chan string, 2),
   102  		allInstancesFunc: func(ctx context.ProviderCallContext) ([]instances.Instance, error) {
   103  			return s.instances, nil
   104  		},
   105  	}
   106  
   107  	s.callCtx = &context.CloudCallContext{
   108  		InvalidateCredentialFunc: func(string) error {
   109  			s.invalidCredential = true
   110  			return nil
   111  		},
   112  	}
   113  	s.auth = &testAuthenticationProvider{&testing.Stub{}}
   114  }
   115  
   116  func (s *ProvisionerTaskSuite) TestStartStop(c *gc.C) {
   117  	task := s.newProvisionerTask(c,
   118  		config.HarvestAll,
   119  		&mockDistributionGroupFinder{},
   120  		mockToolsFinder{},
   121  	)
   122  	workertest.CheckAlive(c, task)
   123  	workertest.CleanKill(c, task)
   124  
   125  	err := workertest.CheckKilled(c, task)
   126  	c.Assert(err, jc.ErrorIsNil)
   127  	err = workertest.CheckKilled(c, s.modelMachinesWatcher)
   128  	c.Assert(err, jc.ErrorIsNil)
   129  	err = workertest.CheckKilled(c, s.machineErrorRetryWatcher)
   130  	c.Assert(err, jc.ErrorIsNil)
   131  	s.machineGetter.CheckNoCalls(c)
   132  	s.instanceBroker.CheckNoCalls(c)
   133  }
   134  
   135  func (s *ProvisionerTaskSuite) TestStopInstancesIgnoresMachinesWithKeep(c *gc.C) {
   136  	task := s.newProvisionerTask(c,
   137  		config.HarvestAll,
   138  		&mockDistributionGroupFinder{},
   139  		mockToolsFinder{},
   140  	)
   141  	defer workertest.CleanKill(c, task)
   142  
   143  	i0 := &testInstance{id: "zero"}
   144  	i1 := &testInstance{id: "one"}
   145  	s.instances = []instances.Instance{
   146  		i0,
   147  		i1,
   148  	}
   149  
   150  	m0 := &testMachine{
   151  		id:       "0",
   152  		life:     params.Dead,
   153  		instance: i0,
   154  	}
   155  	m1 := &testMachine{
   156  		id:           "1",
   157  		life:         params.Dead,
   158  		instance:     i1,
   159  		keepInstance: true,
   160  	}
   161  	c.Assert(m0.markForRemoval, jc.IsFalse)
   162  	c.Assert(m1.markForRemoval, jc.IsFalse)
   163  
   164  	s.machinesResults = []apiprovisioner.MachineResult{
   165  		{Machine: m0},
   166  		{Machine: m1},
   167  	}
   168  
   169  	s.sendModelMachinesChange(c, "0", "1")
   170  
   171  	s.waitForTask(c, []string{"AllInstances", "StopInstances"})
   172  
   173  	workertest.CleanKill(c, task)
   174  	close(s.instanceBroker.callsChan)
   175  	s.machineGetter.CheckCallNames(c, "Machines")
   176  	s.instanceBroker.CheckCalls(c, []testing.StubCall{
   177  		{"AllInstances", []interface{}{s.callCtx}},
   178  		{"StopInstances", []interface{}{s.callCtx, []instance.Id{"zero"}}},
   179  	})
   180  	c.Assert(m0.markForRemoval, jc.IsTrue)
   181  	c.Assert(m1.markForRemoval, jc.IsTrue)
   182  }
   183  
   184  func (s *ProvisionerTaskSuite) TestProvisionerRetries(c *gc.C) {
   185  	s.instanceBroker.SetErrors(
   186  		errors.New("errors 1"),
   187  		errors.New("errors 2"),
   188  	)
   189  
   190  	task := s.newProvisionerTaskWithRetry(c,
   191  		config.HarvestAll,
   192  		&mockDistributionGroupFinder{},
   193  		mockToolsFinder{},
   194  		provisioner.NewRetryStrategy(0*time.Second, 1),
   195  	)
   196  
   197  	m0 := &testMachine{
   198  		id: "0",
   199  	}
   200  	s.machineStatusResults = []apiprovisioner.MachineStatusResult{
   201  		{Machine: m0, Status: params.StatusResult{}},
   202  	}
   203  	s.sendMachineErrorRetryChange(c)
   204  
   205  	s.waitForTask(c, []string{"StartInstance", "StartInstance"})
   206  
   207  	workertest.CleanKill(c, task)
   208  	close(s.instanceBroker.callsChan)
   209  	s.machineGetter.CheckCallNames(c, "MachinesWithTransientErrors")
   210  	s.auth.CheckCallNames(c, "SetupAuthentication")
   211  	s.instanceBroker.CheckCallNames(c, "StartInstance", "StartInstance")
   212  }
   213  
   214  func (s *ProvisionerTaskSuite) TestProcessProfileChanges(c *gc.C) {
   215  	ctrl := gomock.NewController(c)
   216  	defer ctrl.Finish()
   217  
   218  	// Setup mockMachine0 to successfully change from an
   219  	// old profile to a new profile.
   220  	mockMachine0, info0 := setUpSuccessfulMockProfileMachine(ctrl, "0", "juju-default-lxd-profile-0", false)
   221  	mockMachine0.EXPECT().SetCharmProfiles([]string{info0.NewProfileName, "juju-default-different-0"}).Return(nil)
   222  
   223  	// Setup mockMachine1 to successfully change from an
   224  	// old profile to a new profile.
   225  	mockMachine1, info1 := setUpSuccessfulMockProfileMachine(ctrl, "1", "juju-default-lxd-profile-0", true)
   226  	mockMachine1.EXPECT().SetCharmProfiles([]string{info1.NewProfileName, "juju-default-different-0"}).Return(nil)
   227  
   228  	// Setup mockMachine2 to have a failure from CharmProfileChangeInfo()
   229  	mockMachine2 := setUpFailureMockProfileMachine(ctrl, "2")
   230  
   231  	// Setup mockMachine3 to be a new subordinate unit adding
   232  	// an lxd profile.
   233  	mockMachine3, info3 := setUpSuccessfulMockProfileMachine(ctrl, "3", "", true)
   234  	mockMachine3.EXPECT().SetCharmProfiles([]string{"juju-default-different-0", info3.NewProfileName}).Return(nil)
   235  
   236  	s.machinesResults = []apiprovisioner.MachineResult{
   237  		{Machine: mockMachine0, Err: nil},
   238  		{Machine: mockMachine1, Err: nil},
   239  		{Machine: mockMachine2, Err: nil},
   240  		{Machine: mockMachine3, Err: nil},
   241  	}
   242  
   243  	mockBroker := provisionermocks.NewMockLXDProfileInstanceBroker(ctrl)
   244  	lExp := mockBroker.EXPECT()
   245  	machineCharmProfiles := []string{"default", "juju-default", info0.NewProfileName, "juju-default-different-0"}
   246  	lExp.ReplaceOrAddInstanceProfile(
   247  		"0", info0.OldProfileName, info0.NewProfileName, info0.LXDProfile,
   248  	).Return(machineCharmProfiles, nil)
   249  	lExp.ReplaceOrAddInstanceProfile(
   250  		"1", info1.OldProfileName, info1.NewProfileName, info1.LXDProfile,
   251  	).Return(machineCharmProfiles, nil)
   252  	lExp.ReplaceOrAddInstanceProfile(
   253  		"3", info3.OldProfileName, info3.NewProfileName, info3.LXDProfile,
   254  	).Return([]string{"default", "juju-default", "juju-default-different-0", info3.NewProfileName}, nil)
   255  
   256  	task := s.newProvisionerTaskWithBroker(c, mockBroker, nil)
   257  	c.Assert(provisioner.ProcessProfileChanges(task, []string{"0", "1", "2", "3"}), jc.ErrorIsNil)
   258  }
   259  
   260  func setUpSuccessfulMockProfileMachine(ctrl *gomock.Controller, num, old string, sub bool) (*apiprovisionermock.MockMachineProvisioner, apiprovisioner.CharmProfileChangeInfo) {
   261  	mockMachine := apiprovisionermock.NewMockMachineProvisioner(ctrl)
   262  	mExp := mockMachine.EXPECT()
   263  	newProfileName := "juju-default-lxd-profile-1"
   264  	info := apiprovisioner.CharmProfileChangeInfo{
   265  		OldProfileName: old,
   266  		NewProfileName: newProfileName,
   267  		LXDProfile:     nil,
   268  		Subordinate:    sub,
   269  	}
   270  	mExp.CharmProfileChangeInfo().Return(info, nil)
   271  	mExp.Id().Return(num)
   272  	mExp.InstanceId().Return(instance.Id(num), nil)
   273  	if old == "" && sub {
   274  		mExp.RemoveUpgradeCharmProfileData().Return(nil)
   275  	} else {
   276  		mExp.SetInstanceStatus(status.Running, "Running", nil).Return(nil)
   277  		mExp.SetStatus(status.Started, "", nil).Return(nil)
   278  		mExp.SetUpgradeCharmProfileComplete(lxdprofile.SuccessStatus).Return(nil)
   279  	}
   280  
   281  	return mockMachine, info
   282  }
   283  
   284  func setUpFailureMockProfileMachine(ctrl *gomock.Controller, num string) *apiprovisionermock.MockMachineProvisioner {
   285  	mockMachine := apiprovisionermock.NewMockMachineProvisioner(ctrl)
   286  	mExp := mockMachine.EXPECT()
   287  	mExp.CharmProfileChangeInfo().Return(apiprovisioner.CharmProfileChangeInfo{}, errors.New("fail me"))
   288  	mExp.Id().Return(num)
   289  	mExp.SetInstanceStatus(status.Error, gomock.Any(), nil).Return(nil)
   290  	mExp.SetUpgradeCharmProfileComplete(gomock.Any()).Return(nil)
   291  
   292  	return mockMachine
   293  }
   294  
   295  func (s *ProvisionerTaskSuite) TestProcessProfileChangesNoLXDBroker(c *gc.C) {
   296  	ctrl := gomock.NewController(c)
   297  	defer ctrl.Finish()
   298  
   299  	mockMachine := apiprovisionermock.NewMockMachineProvisioner(ctrl)
   300  	mExp := mockMachine.EXPECT()
   301  	mExp.SetUpgradeCharmProfileComplete(lxdprofile.NotSupportedStatus).Return(nil)
   302  
   303  	s.machinesResults = []apiprovisioner.MachineResult{
   304  		{Machine: mockMachine, Err: nil},
   305  	}
   306  
   307  	task := s.newProvisionerTask(c,
   308  		config.HarvestAll,
   309  		&mockDistributionGroupFinder{},
   310  		mockToolsFinder{},
   311  	)
   312  	defer workertest.CleanKill(c, task)
   313  
   314  	c.Assert(provisioner.ProcessProfileChanges(task, []string{"0"}), jc.ErrorIsNil)
   315  }
   316  
   317  func (s *ProvisionerTaskSuite) testProcessOneMachineProfileChangeAddProfile(c *gc.C, sub bool) {
   318  	ctrl := gomock.NewController(c)
   319  	defer ctrl.Finish()
   320  
   321  	mockMachineProvisioner := apiprovisionermock.NewMockMachineProvisioner(ctrl)
   322  	mExp := mockMachineProvisioner.EXPECT()
   323  	newProfileName := "juju-default-lxd-profile-0"
   324  	info := apiprovisioner.CharmProfileChangeInfo{
   325  		OldProfileName: "",
   326  		NewProfileName: newProfileName,
   327  		LXDProfile:     nil,
   328  		Subordinate:    sub,
   329  	}
   330  	mExp.CharmProfileChangeInfo().Return(info, nil)
   331  	mExp.Id().Return("0")
   332  	mExp.InstanceId().Return(instance.Id("0"), nil)
   333  	differentProfileName := "juju-default-different-0"
   334  	mExp.SetCharmProfiles([]string{differentProfileName, newProfileName}).Return(nil)
   335  
   336  	mockLXDProfiler := provisionermocks.NewMockLXDProfileInstanceBroker(ctrl)
   337  	lExp := mockLXDProfiler.EXPECT()
   338  	machineCharmProfiles := []string{"default", "juju-default", differentProfileName, newProfileName}
   339  	lExp.ReplaceOrAddInstanceProfile(
   340  		"0", info.OldProfileName, info.NewProfileName, info.LXDProfile,
   341  	).Return(machineCharmProfiles, nil)
   342  
   343  	remove, err := provisioner.ProcessOneMachineProfileChanges(mockMachineProvisioner, mockLXDProfiler)
   344  	c.Assert(err, jc.ErrorIsNil)
   345  	c.Assert(remove, gc.Equals, sub)
   346  }
   347  
   348  func (s *ProvisionerTaskSuite) TestProcessOneMachineProfileChangeAddProfile(c *gc.C) {
   349  	s.testProcessOneMachineProfileChangeAddProfile(c, false)
   350  }
   351  
   352  func (s *ProvisionerTaskSuite) TestProcessOneMachineProfileChangeAddProfileSubordinate(c *gc.C) {
   353  	s.testProcessOneMachineProfileChangeAddProfile(c, true)
   354  }
   355  
   356  func (s *ProvisionerTaskSuite) TestProcessOneMachineProfileChangeRemoveProfileSubordinate(c *gc.C) {
   357  	info := apiprovisioner.CharmProfileChangeInfo{
   358  		OldProfileName: "juju-default-lxd-profile-0",
   359  		NewProfileName: "",
   360  		LXDProfile:     nil,
   361  		Subordinate:    true,
   362  	}
   363  
   364  	ctrl, mockMachineProvisioner, mockLXDProfiler := setUpMocksProcessOneMachineProfileChange(c, info)
   365  	defer ctrl.Finish()
   366  
   367  	remove, err := provisioner.ProcessOneMachineProfileChanges(mockMachineProvisioner, mockLXDProfiler)
   368  	c.Assert(err, jc.ErrorIsNil)
   369  	c.Assert(remove, gc.Equals, false)
   370  }
   371  
   372  func (s *ProvisionerTaskSuite) TestProcessOneMachineProfileChangeChangeProfile(c *gc.C) {
   373  	info := apiprovisioner.CharmProfileChangeInfo{
   374  		OldProfileName: "juju-default-lxd-profile-0",
   375  		NewProfileName: "juju-default-lxd-profile-1",
   376  		LXDProfile:     nil,
   377  		Subordinate:    true,
   378  	}
   379  
   380  	ctrl, mockMachineProvisioner, mockLXDProfiler := setUpMocksProcessOneMachineProfileChange(c, info)
   381  	defer ctrl.Finish()
   382  
   383  	remove, err := provisioner.ProcessOneMachineProfileChanges(mockMachineProvisioner, mockLXDProfiler)
   384  	c.Assert(err, jc.ErrorIsNil)
   385  	c.Assert(remove, gc.Equals, false)
   386  }
   387  
   388  func setUpMocksProcessOneMachineProfileChange(c *gc.C, info apiprovisioner.CharmProfileChangeInfo) (*gomock.Controller, *apiprovisionermock.MockMachineProvisioner, *provisionermocks.MockLXDProfileInstanceBroker) {
   389  	ctrl := gomock.NewController(c)
   390  
   391  	mockMachineProvisioner := apiprovisionermock.NewMockMachineProvisioner(ctrl)
   392  	mExp := mockMachineProvisioner.EXPECT()
   393  	mExp.CharmProfileChangeInfo().Return(info, nil)
   394  	mExp.Id().Return("0")
   395  	mExp.InstanceId().Return(instance.Id("0"), nil)
   396  
   397  	differentProfileName := "juju-default-different-0"
   398  	machineCharmProfiles := []string{"default", "juju-default"}
   399  	if info.NewProfileName != "" {
   400  		mExp.SetCharmProfiles([]string{info.NewProfileName, differentProfileName}).Return(nil)
   401  		machineCharmProfiles = append(machineCharmProfiles, info.NewProfileName)
   402  	} else {
   403  		mExp.SetCharmProfiles([]string{differentProfileName}).Return(nil)
   404  	}
   405  	machineCharmProfiles = append(machineCharmProfiles, differentProfileName)
   406  
   407  	mockLXDProfiler := provisionermocks.NewMockLXDProfileInstanceBroker(ctrl)
   408  	mockLXDProfiler.EXPECT().ReplaceOrAddInstanceProfile(
   409  		"0", info.OldProfileName, info.NewProfileName, info.LXDProfile,
   410  	).Return(machineCharmProfiles, nil)
   411  
   412  	return ctrl, mockMachineProvisioner, mockLXDProfiler
   413  }
   414  
   415  func (s *ProvisionerTaskSuite) TestZoneConstraintsNoZoneAvailable(c *gc.C) {
   416  	ctrl := gomock.NewController(c)
   417  	defer ctrl.Finish()
   418  
   419  	broker := s.setUpZonedEnviron(ctrl)
   420  
   421  	// Constraint for availability zone az9 can not be satisfied;
   422  	// this broker only knows of az1, az2, az3.
   423  	azConstraints := newAZConstraintStartInstanceParamsMatcher("az9")
   424  	broker.EXPECT().DeriveAvailabilityZones(s.callCtx, azConstraints).Return([]string{}, nil)
   425  
   426  	task := s.newProvisionerTaskWithBroker(c, broker, nil)
   427  
   428  	m0 := &testMachine{
   429  		id:          "0",
   430  		constraints: "zones=az9",
   431  	}
   432  	s.machineStatusResults = []apiprovisioner.MachineStatusResult{{Machine: m0, Status: params.StatusResult{}}}
   433  	s.sendMachineErrorRetryChange(c)
   434  
   435  	// Wait for instance status to be set.
   436  	timeout := time.After(coretesting.LongWait)
   437  	for msg := ""; msg == ""; {
   438  		select {
   439  		case <-time.After(coretesting.ShortWait):
   440  			_, msg, _ = m0.InstanceStatus()
   441  		case <-timeout:
   442  			c.Fatalf("machine InstanceStatus was not set")
   443  		}
   444  	}
   445  
   446  	_, msg, err := m0.InstanceStatus()
   447  	c.Assert(err, jc.ErrorIsNil)
   448  	c.Check(msg, gc.Equals, "suitable availability zone for machine 0 not found")
   449  
   450  	workertest.CleanKill(c, task)
   451  }
   452  
   453  func (s *ProvisionerTaskSuite) TestZoneConstraintsNoDistributionGroup(c *gc.C) {
   454  	ctrl := gomock.NewController(c)
   455  	defer ctrl.Finish()
   456  
   457  	broker := s.setUpZonedEnviron(ctrl)
   458  	azConstraints := newAZConstraintStartInstanceParamsMatcher("az1")
   459  	broker.EXPECT().DeriveAvailabilityZones(s.callCtx, azConstraints).Return([]string{}, nil)
   460  
   461  	// For the call to start instance, we expect the same zone constraint to
   462  	// be present, but we also expect that the zone in start instance params
   463  	// matches the constraint, based on being available in this environ.
   464  	azConstraintsAndDerivedZone := newAZConstraintStartInstanceParamsMatcher("az1")
   465  	azConstraintsAndDerivedZone.addMatch("availability zone: az1", func(p environs.StartInstanceParams) bool {
   466  		return p.AvailabilityZone == "az1"
   467  	})
   468  
   469  	// Use satisfaction of this call as the synchronisation point.
   470  	started := make(chan struct{})
   471  	broker.EXPECT().StartInstance(s.callCtx, azConstraints).Return(&environs.StartInstanceResult{
   472  		Instance: &testInstance{id: "instance-1"},
   473  	}, nil).Do(func(_ ...interface{}) {
   474  		go func() { started <- struct{}{} }()
   475  	})
   476  
   477  	task := s.newProvisionerTaskWithBroker(c, broker, nil)
   478  
   479  	m0 := &testMachine{
   480  		id:          "0",
   481  		constraints: "zones=az1",
   482  	}
   483  	s.machineStatusResults = []apiprovisioner.MachineStatusResult{{Machine: m0, Status: params.StatusResult{}}}
   484  	s.sendMachineErrorRetryChange(c)
   485  
   486  	select {
   487  	case <-started:
   488  	case <-time.After(coretesting.LongWait):
   489  		c.Fatalf("no matching call to StartInstance")
   490  	}
   491  
   492  	workertest.CleanKill(c, task)
   493  }
   494  
   495  func (s *ProvisionerTaskSuite) TestZoneConstraintsWithDistributionGroup(c *gc.C) {
   496  	ctrl := gomock.NewController(c)
   497  	defer ctrl.Finish()
   498  
   499  	broker := s.setUpZonedEnviron(ctrl)
   500  	azConstraints := newAZConstraintStartInstanceParamsMatcher("az1", "az2")
   501  	broker.EXPECT().DeriveAvailabilityZones(s.callCtx, azConstraints).Return([]string{}, nil)
   502  
   503  	// For the call to start instance, we expect the same zone constraints to
   504  	// be present, but we also expect that the zone in start instance params
   505  	// was selected from the constraints, based on a machine from the same
   506  	// distribution group already being in one of the zones.
   507  	azConstraintsAndDerivedZone := newAZConstraintStartInstanceParamsMatcher("az1", "az2")
   508  	azConstraintsAndDerivedZone.addMatch("availability zone: az2", func(p environs.StartInstanceParams) bool {
   509  		return p.AvailabilityZone == "az2"
   510  	})
   511  
   512  	// Use satisfaction of this call as the synchronisation point.
   513  	started := make(chan struct{})
   514  	broker.EXPECT().StartInstance(s.callCtx, azConstraints).Return(&environs.StartInstanceResult{
   515  		Instance: &testInstance{id: "instance-1"},
   516  	}, nil).Do(func(_ ...interface{}) {
   517  		go func() { started <- struct{}{} }()
   518  	})
   519  
   520  	// Another machine from the same distribution group is already in az1,
   521  	// so we expect the machine to be created in az2.
   522  	task := s.newProvisionerTaskWithBroker(c, broker, map[names.MachineTag][]string{
   523  		names.NewMachineTag("0"): {"az1"},
   524  	})
   525  
   526  	m0 := &testMachine{
   527  		id:          "0",
   528  		constraints: "zones=az1,az2",
   529  	}
   530  	s.machineStatusResults = []apiprovisioner.MachineStatusResult{{Machine: m0, Status: params.StatusResult{}}}
   531  	s.sendMachineErrorRetryChange(c)
   532  
   533  	select {
   534  	case <-started:
   535  	case <-time.After(coretesting.LongWait):
   536  		c.Fatalf("no matching call to StartInstance")
   537  	}
   538  
   539  	workertest.CleanKill(c, task)
   540  }
   541  
   542  // setUpZonedEnviron creates a mock environ with instances based on those set
   543  // on the test suite, and 3 availability zones.
   544  func (s *ProvisionerTaskSuite) setUpZonedEnviron(ctrl *gomock.Controller) *mocks.MockZonedEnviron {
   545  	instanceIds := make([]instance.Id, len(s.instances))
   546  	for i, inst := range s.instances {
   547  		instanceIds[i] = inst.Id()
   548  	}
   549  
   550  	// Environ has 3 availability zones: az1, az2, az3.
   551  	zones := make([]common.AvailabilityZone, 3)
   552  	for i := 0; i < 3; i++ {
   553  		az := mocks.NewMockAvailabilityZone(ctrl)
   554  		az.EXPECT().Name().Return(fmt.Sprintf("az%d", i+1))
   555  		az.EXPECT().Available().Return(true)
   556  		zones[i] = az
   557  	}
   558  
   559  	broker := mocks.NewMockZonedEnviron(ctrl)
   560  	exp := broker.EXPECT()
   561  	exp.AllInstances(s.callCtx).Return(s.instances, nil)
   562  	exp.InstanceAvailabilityZoneNames(s.callCtx, instanceIds).Return([]string{}, nil)
   563  	exp.AvailabilityZones(s.callCtx).Return(zones, nil)
   564  	return broker
   565  }
   566  
   567  func (s *ProvisionerTaskSuite) waitForTask(c *gc.C, expectedCalls []string) {
   568  	var calls []string
   569  	for {
   570  		select {
   571  		case call := <-s.instanceBroker.callsChan:
   572  			calls = append(calls, call)
   573  		case <-time.After(coretesting.LongWait):
   574  			c.Fatalf("stopping worker chan didn't stop")
   575  		}
   576  		if reflect.DeepEqual(expectedCalls, calls) {
   577  			// we are done
   578  			break
   579  		}
   580  	}
   581  }
   582  
   583  func (s *ProvisionerTaskSuite) sendModelMachinesChange(c *gc.C, ids ...string) {
   584  	select {
   585  	case s.modelMachinesChanges <- ids:
   586  	case <-time.After(coretesting.LongWait):
   587  		c.Fatal("timed out sending model machines change")
   588  	}
   589  }
   590  
   591  func (s *ProvisionerTaskSuite) sendMachineErrorRetryChange(c *gc.C) {
   592  	select {
   593  	case s.machineErrorRetryChanges <- struct{}{}:
   594  	case <-time.After(coretesting.LongWait):
   595  		c.Fatal("timed out sending machine error retry change")
   596  	}
   597  }
   598  
   599  func (s *ProvisionerTaskSuite) newProvisionerTask(
   600  	c *gc.C,
   601  	harvestingMethod config.HarvestMode,
   602  	distributionGroupFinder provisioner.DistributionGroupFinder,
   603  	toolsFinder provisioner.ToolsFinder,
   604  ) provisioner.ProvisionerTask {
   605  	return s.newProvisionerTaskWithRetry(c,
   606  		harvestingMethod,
   607  		distributionGroupFinder,
   608  		toolsFinder,
   609  		provisioner.NewRetryStrategy(0*time.Second, 0),
   610  	)
   611  }
   612  
   613  func (s *ProvisionerTaskSuite) newProvisionerTaskWithRetry(
   614  	c *gc.C,
   615  	harvestingMethod config.HarvestMode,
   616  	distributionGroupFinder provisioner.DistributionGroupFinder,
   617  	toolsFinder provisioner.ToolsFinder,
   618  	retryStrategy provisioner.RetryStrategy,
   619  ) provisioner.ProvisionerTask {
   620  	w, err := provisioner.NewProvisionerTask(
   621  		coretesting.ControllerTag.Id(),
   622  		names.NewMachineTag("0"),
   623  		harvestingMethod,
   624  		s.machineGetter,
   625  		distributionGroupFinder,
   626  		toolsFinder,
   627  		s.modelMachinesWatcher,
   628  		s.machineErrorRetryWatcher,
   629  		s.modelMachinesProfileWatcher,
   630  		s.instanceBroker,
   631  		s.auth,
   632  		imagemetadata.ReleasedStream,
   633  		retryStrategy,
   634  		s.callCtx,
   635  	)
   636  	c.Assert(err, jc.ErrorIsNil)
   637  	return w
   638  }
   639  
   640  func (s *ProvisionerTaskSuite) newProvisionerTaskWithBroker(
   641  	c *gc.C, broker environs.InstanceBroker, distributionGroups map[names.MachineTag][]string,
   642  ) provisioner.ProvisionerTask {
   643  	task, err := provisioner.NewProvisionerTask(
   644  		coretesting.ControllerTag.Id(),
   645  		names.NewMachineTag("0"),
   646  		config.HarvestAll,
   647  		s.machineGetter,
   648  		&mockDistributionGroupFinder{groups: distributionGroups},
   649  		mockToolsFinder{},
   650  		s.modelMachinesWatcher,
   651  		s.machineErrorRetryWatcher,
   652  		s.modelMachinesProfileWatcher,
   653  		broker,
   654  		s.auth,
   655  		imagemetadata.ReleasedStream,
   656  		provisioner.NewRetryStrategy(0*time.Second, 0),
   657  		s.callCtx,
   658  	)
   659  	c.Assert(err, jc.ErrorIsNil)
   660  	return task
   661  }
   662  
   663  type testMachineGetter struct {
   664  	*testing.Stub
   665  
   666  	machinesFunc                    func(machines ...names.MachineTag) ([]apiprovisioner.MachineResult, error)
   667  	machinesWithTransientErrorsFunc func() ([]apiprovisioner.MachineStatusResult, error)
   668  }
   669  
   670  func (m *testMachineGetter) Machines(machines ...names.MachineTag) ([]apiprovisioner.MachineResult, error) {
   671  	m.AddCall("Machines", machines)
   672  	return m.machinesFunc(machines...)
   673  }
   674  
   675  func (m *testMachineGetter) MachinesWithTransientErrors() ([]apiprovisioner.MachineStatusResult, error) {
   676  	m.AddCall("MachinesWithTransientErrors")
   677  	return m.machinesWithTransientErrorsFunc()
   678  }
   679  
   680  type testInstanceBroker struct {
   681  	*testing.Stub
   682  
   683  	callsChan chan string
   684  
   685  	allInstancesFunc func(ctx context.ProviderCallContext) ([]instances.Instance, error)
   686  }
   687  
   688  func (t *testInstanceBroker) StartInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
   689  	t.AddCall("StartInstance", ctx, args)
   690  	t.callsChan <- "StartInstance"
   691  	return nil, t.NextErr()
   692  }
   693  
   694  func (t *testInstanceBroker) StopInstances(ctx context.ProviderCallContext, ids ...instance.Id) error {
   695  	t.AddCall("StopInstances", ctx, ids)
   696  	t.callsChan <- "StopInstances"
   697  	return t.NextErr()
   698  }
   699  
   700  func (t *testInstanceBroker) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) {
   701  	t.AddCall("AllInstances", ctx)
   702  	t.callsChan <- "AllInstances"
   703  	return t.allInstancesFunc(ctx)
   704  }
   705  
   706  func (t *testInstanceBroker) MaintainInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) error {
   707  	t.AddCall("MaintainInstance", ctx, args)
   708  	t.callsChan <- "MaintainInstance"
   709  	return nil
   710  }
   711  
   712  type testInstance struct {
   713  	instances.Instance
   714  	id string
   715  }
   716  
   717  func (i *testInstance) Id() instance.Id {
   718  	return instance.Id(i.id)
   719  }
   720  
   721  type testMachine struct {
   722  	mu sync.Mutex
   723  
   724  	*apiprovisioner.Machine
   725  	id   string
   726  	life params.Life
   727  
   728  	instance     *testInstance
   729  	keepInstance bool
   730  
   731  	markForRemoval bool
   732  	constraints    string
   733  
   734  	instStatusMsg string
   735  }
   736  
   737  func (m *testMachine) Id() string {
   738  	return m.id
   739  }
   740  
   741  func (m *testMachine) String() string {
   742  	return m.Id()
   743  }
   744  
   745  func (m *testMachine) Life() params.Life {
   746  	return m.life
   747  }
   748  
   749  func (m *testMachine) InstanceId() (instance.Id, error) {
   750  	return m.instance.Id(), nil
   751  }
   752  
   753  func (m *testMachine) InstanceNames() (instance.Id, string, error) {
   754  	instId, err := m.InstanceId()
   755  	return instId, "", err
   756  }
   757  
   758  func (m *testMachine) KeepInstance() (bool, error) {
   759  	return m.keepInstance, nil
   760  }
   761  
   762  func (m *testMachine) MarkForRemoval() error {
   763  	m.markForRemoval = true
   764  	return nil
   765  }
   766  
   767  func (m *testMachine) Tag() names.Tag {
   768  	return m.MachineTag()
   769  }
   770  
   771  func (m *testMachine) MachineTag() names.MachineTag {
   772  	return names.NewMachineTag(m.id)
   773  }
   774  
   775  func (m *testMachine) SetInstanceStatus(_ status.Status, message string, _ map[string]interface{}) error {
   776  	m.mu.Lock()
   777  	m.instStatusMsg = message
   778  	m.mu.Unlock()
   779  	return nil
   780  }
   781  
   782  func (m *testMachine) InstanceStatus() (status.Status, string, error) {
   783  	m.mu.Lock()
   784  	defer m.mu.Unlock()
   785  	return status.Status(""), m.instStatusMsg, nil
   786  }
   787  
   788  func (m *testMachine) SetStatus(status status.Status, info string, data map[string]interface{}) error {
   789  	return nil
   790  }
   791  
   792  func (m *testMachine) Status() (status.Status, string, error) {
   793  	return status.Status(""), "", nil
   794  }
   795  
   796  func (m *testMachine) ModelAgentVersion() (*version.Number, error) {
   797  	return &coretesting.FakeVersionNumber, nil
   798  }
   799  
   800  func (m *testMachine) SetInstanceInfo(
   801  	id instance.Id, displayName string, nonce string, characteristics *instance.HardwareCharacteristics,
   802  	networkConfig []params.NetworkConfig, volumes []params.Volume,
   803  	volumeAttachments map[string]params.VolumeAttachmentInfo, charmProfiles []string,
   804  ) error {
   805  	return nil
   806  }
   807  
   808  func (m *testMachine) ProvisioningInfo() (*params.ProvisioningInfo, error) {
   809  	return &params.ProvisioningInfo{
   810  		ControllerConfig: coretesting.FakeControllerConfig(),
   811  		Series:           jujuversion.SupportedLTS(),
   812  		Constraints:      constraints.MustParse(m.constraints),
   813  	}, nil
   814  }
   815  
   816  type testAuthenticationProvider struct {
   817  	*testing.Stub
   818  }
   819  
   820  func (m *testAuthenticationProvider) SetupAuthentication(
   821  	machine authentication.TaggedPasswordChanger,
   822  ) (*mongo.MongoInfo, *api.Info, error) {
   823  	m.AddCall("SetupAuthentication", machine)
   824  	return nil, nil, nil
   825  }
   826  
   827  // startInstanceParamsMatcher is a GoMock matcher that applies a collection of
   828  // conditions to an environs.StartInstanceParams.
   829  // All conditions must be true in order for a positive match.
   830  type startInstanceParamsMatcher struct {
   831  	matchers map[string]func(environs.StartInstanceParams) bool
   832  	failMsg  string
   833  }
   834  
   835  func (m *startInstanceParamsMatcher) Matches(params interface{}) bool {
   836  	siParams := params.(environs.StartInstanceParams)
   837  	for msg, match := range m.matchers {
   838  		if !match(siParams) {
   839  			m.failMsg = msg
   840  			return false
   841  		}
   842  	}
   843  	return true
   844  }
   845  
   846  func (m *startInstanceParamsMatcher) String() string {
   847  	return m.failMsg
   848  }
   849  
   850  func (m *startInstanceParamsMatcher) addMatch(msg string, match func(environs.StartInstanceParams) bool) {
   851  	m.matchers[msg] = match
   852  }
   853  
   854  // newAZConstraintStartInstanceParamsMatcher returns a matcher that tests
   855  // whether the candidate environs.StartInstanceParams has a constraints value
   856  // that includes exactly the input zones.
   857  func newAZConstraintStartInstanceParamsMatcher(zones ...string) *startInstanceParamsMatcher {
   858  	match := func(p environs.StartInstanceParams) bool {
   859  		if !p.Constraints.HasZones() {
   860  			return false
   861  		}
   862  		cZones := *p.Constraints.Zones
   863  		if len(cZones) != len(zones) {
   864  			return false
   865  		}
   866  		for _, z := range zones {
   867  			found := false
   868  			for _, cz := range cZones {
   869  				if z == cz {
   870  					found = true
   871  					break
   872  				}
   873  			}
   874  			if !found {
   875  				return false
   876  			}
   877  		}
   878  		return true
   879  	}
   880  	return newStartInstanceParamsMatcher(map[string]func(environs.StartInstanceParams) bool{
   881  		fmt.Sprint("AZ constraints:", strings.Join(zones, ", ")): match,
   882  	})
   883  }
   884  
   885  func newStartInstanceParamsMatcher(
   886  	matchers map[string]func(environs.StartInstanceParams) bool,
   887  ) *startInstanceParamsMatcher {
   888  	if matchers == nil {
   889  		matchers = make(map[string]func(environs.StartInstanceParams) bool)
   890  	}
   891  	return &startInstanceParamsMatcher{matchers: matchers}
   892  }