github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/prechecker_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state_test
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/names/v5"
    11  	jc "github.com/juju/testing/checkers"
    12  	gc "gopkg.in/check.v1"
    13  
    14  	"github.com/juju/juju/agent"
    15  	corebase "github.com/juju/juju/core/base"
    16  	"github.com/juju/juju/core/constraints"
    17  	"github.com/juju/juju/core/instance"
    18  	"github.com/juju/juju/environs"
    19  	"github.com/juju/juju/environs/context"
    20  	"github.com/juju/juju/state"
    21  	"github.com/juju/juju/storage"
    22  )
    23  
    24  type PrecheckerSuite struct {
    25  	ConnSuite
    26  	prechecker mockPrechecker
    27  }
    28  
    29  var _ = gc.Suite(&PrecheckerSuite{})
    30  
    31  type mockPrechecker struct {
    32  	precheckInstanceError error
    33  	precheckInstanceArgs  environs.PrecheckInstanceParams
    34  }
    35  
    36  func (p *mockPrechecker) PrecheckInstance(ctx context.ProviderCallContext, args environs.PrecheckInstanceParams) error {
    37  	p.precheckInstanceArgs = args
    38  	return p.precheckInstanceError
    39  }
    40  
    41  func (s *PrecheckerSuite) SetUpTest(c *gc.C) {
    42  	s.ConnSuite.SetUpTest(c)
    43  	s.prechecker = mockPrechecker{}
    44  	s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) {
    45  		return &s.prechecker, nil
    46  	}
    47  }
    48  
    49  func (s *PrecheckerSuite) TestPrecheckInstance(c *gc.C) {
    50  	// PrecheckInstance should be called with the specified
    51  	// series and no placement, and the specified constraints
    52  	// merged with the model constraints, when attempting
    53  	// to create an instance.
    54  	modelCons := constraints.MustParse("mem=4G")
    55  	placement := ""
    56  	template, err := s.addOneMachine(c, modelCons, placement)
    57  	c.Assert(err, jc.ErrorIsNil)
    58  	c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, template.Base.String())
    59  	c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, placement)
    60  	validator := constraints.NewValidator()
    61  	cons, err := validator.Merge(modelCons, template.Constraints)
    62  	c.Assert(err, jc.ErrorIsNil)
    63  	c.Assert(s.prechecker.precheckInstanceArgs.Constraints, gc.DeepEquals, cons)
    64  }
    65  
    66  func (s *PrecheckerSuite) TestPrecheckInstanceWithPlacement(c *gc.C) {
    67  	// PrecheckInstance should be called with the specified
    68  	// series and placement. If placement is provided all
    69  	// model constraints should be ignored, otherwise they
    70  	// should be merged with provided constraints, when
    71  	// attempting to create an instance
    72  	modelCons := constraints.MustParse("mem=4G")
    73  	placement := "abc123"
    74  	template, err := s.addOneMachine(c, modelCons, placement)
    75  	c.Assert(err, jc.ErrorIsNil)
    76  	c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, template.Base.String())
    77  	c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, placement)
    78  	c.Assert(s.prechecker.precheckInstanceArgs.Constraints, gc.DeepEquals, template.Constraints)
    79  }
    80  
    81  func (s *PrecheckerSuite) TestPrecheckErrors(c *gc.C) {
    82  	// Ensure that AddOneMachine fails when PrecheckInstance returns an error.
    83  	s.prechecker.precheckInstanceError = fmt.Errorf("no instance for you")
    84  	_, err := s.addOneMachine(c, constraints.Value{}, "placement")
    85  	c.Assert(err, gc.ErrorMatches, ".*no instance for you")
    86  
    87  	// If the policy's Prechecker method fails, that will be returned first.
    88  	s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) {
    89  		return nil, fmt.Errorf("no prechecker for you")
    90  	}
    91  	_, err = s.addOneMachine(c, constraints.Value{}, "placement")
    92  	c.Assert(err, gc.ErrorMatches, ".*no prechecker for you")
    93  }
    94  
    95  func (s *PrecheckerSuite) TestPrecheckPrecheckerUnimplemented(c *gc.C) {
    96  	var precheckerErr error
    97  	s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) {
    98  		return nil, precheckerErr
    99  	}
   100  	_, err := s.addOneMachine(c, constraints.Value{}, "placement")
   101  	c.Assert(err, gc.ErrorMatches, "cannot add a new machine: policy returned nil prechecker without an error")
   102  	precheckerErr = errors.NotImplementedf("Prechecker")
   103  	_, err = s.addOneMachine(c, constraints.Value{}, "placement")
   104  	c.Assert(err, jc.ErrorIsNil)
   105  }
   106  
   107  func (s *PrecheckerSuite) TestPrecheckNoPolicy(c *gc.C) {
   108  	s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) {
   109  		c.Errorf("should not have been invoked")
   110  		return nil, nil
   111  	}
   112  	state.SetPolicy(s.State, nil)
   113  	_, err := s.addOneMachine(c, constraints.Value{}, "placement")
   114  	c.Assert(err, jc.ErrorIsNil)
   115  }
   116  
   117  func (s *PrecheckerSuite) addOneMachine(c *gc.C, modelCons constraints.Value, placement string) (state.MachineTemplate, error) {
   118  	_, template, err := s.addMachine(c, modelCons, placement)
   119  	return template, err
   120  }
   121  
   122  func (s *PrecheckerSuite) addMachine(c *gc.C, modelCons constraints.Value, placement string) (*state.Machine, state.MachineTemplate, error) {
   123  	err := s.State.SetModelConstraints(modelCons)
   124  	c.Assert(err, jc.ErrorIsNil)
   125  	oneJob := []state.MachineJob{state.JobHostUnits}
   126  	extraCons := constraints.MustParse("cores=4")
   127  	template := state.MachineTemplate{
   128  		Base:        state.UbuntuBase("20.04"),
   129  		Constraints: extraCons,
   130  		Jobs:        oneJob,
   131  		Placement:   placement,
   132  	}
   133  	machine, err := s.State.AddOneMachine(template)
   134  	return machine, template, err
   135  }
   136  
   137  func (s *PrecheckerSuite) TestPrecheckInstanceInjectMachine(c *gc.C) {
   138  	template := state.MachineTemplate{
   139  		InstanceId: instance.Id("bootstrap"),
   140  		Base:       state.UbuntuBase("22.04"),
   141  		Nonce:      agent.BootstrapNonce,
   142  		Jobs:       []state.MachineJob{state.JobManageModel},
   143  		Placement:  "anyoldthing",
   144  	}
   145  	_, err := s.State.AddOneMachine(template)
   146  	c.Assert(err, jc.ErrorIsNil)
   147  	// PrecheckInstance should not have been called, as we've
   148  	// injected a machine with an existing instance.
   149  	c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, "")
   150  	c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, "")
   151  }
   152  
   153  func (s *PrecheckerSuite) TestPrecheckContainerNewMachine(c *gc.C) {
   154  	// Attempting to add a container to a new machine should cause
   155  	// PrecheckInstance to be called.
   156  	template := state.MachineTemplate{
   157  		Base:      state.UbuntuBase("22.04"),
   158  		Jobs:      []state.MachineJob{state.JobHostUnits},
   159  		Placement: "intertubes",
   160  	}
   161  	_, err := s.State.AddMachineInsideNewMachine(template, template, instance.LXD)
   162  	c.Assert(err, jc.ErrorIsNil)
   163  	c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, template.Base.String())
   164  	c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, template.Placement)
   165  }
   166  
   167  func (s *PrecheckerSuite) TestPrecheckAddApplication(c *gc.C) {
   168  	// Deploy an application for the purpose of creating a
   169  	// storage instance. We'll then destroy the unit and detach
   170  	// the storage, so that it can be attached to a new
   171  	// application unit.
   172  	ch := s.AddTestingCharm(c, "storage-block")
   173  	app, err := s.State.AddApplication(state.AddApplicationArgs{
   174  		Name:  "storage-block",
   175  		Charm: ch,
   176  		CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{
   177  			OS:      "ubuntu",
   178  			Channel: "20.04/stable",
   179  		}},
   180  		NumUnits: 1,
   181  		Storage: map[string]state.StorageConstraints{
   182  			"data":    {Count: 1, Pool: "modelscoped"},
   183  			"allecto": {Count: 1, Pool: "modelscoped"},
   184  		},
   185  	})
   186  	c.Assert(err, jc.ErrorIsNil)
   187  
   188  	unit, err := app.AddUnit(state.AddUnitParams{})
   189  	c.Assert(err, jc.ErrorIsNil)
   190  	err = unit.AssignToNewMachine()
   191  	c.Assert(err, jc.ErrorIsNil)
   192  	machineId, err := unit.AssignedMachineId()
   193  	c.Assert(err, jc.ErrorIsNil)
   194  	machineTag := names.NewMachineTag(machineId)
   195  
   196  	sb, err := state.NewStorageBackend(s.State)
   197  	c.Assert(err, jc.ErrorIsNil)
   198  	storageAttachments, err := sb.UnitStorageAttachments(unit.UnitTag())
   199  	c.Assert(err, jc.ErrorIsNil)
   200  	c.Assert(storageAttachments, gc.HasLen, 2)
   201  	storageTags := []names.StorageTag{
   202  		storageAttachments[0].StorageInstance(),
   203  		storageAttachments[1].StorageInstance(),
   204  	}
   205  
   206  	volumeTags := make([]names.VolumeTag, len(storageTags))
   207  	for i, storageTag := range storageTags {
   208  		volume, err := sb.StorageInstanceVolume(storageTag)
   209  		c.Assert(err, jc.ErrorIsNil)
   210  		volumeTags[i] = volume.VolumeTag()
   211  	}
   212  	// Provision only the first volume.
   213  	err = sb.SetVolumeInfo(volumeTags[0], state.VolumeInfo{
   214  		VolumeId: "foo",
   215  		Pool:     "modelscoped",
   216  	})
   217  	c.Assert(err, jc.ErrorIsNil)
   218  
   219  	err = unit.Destroy()
   220  	c.Assert(err, jc.ErrorIsNil)
   221  	for _, storageTag := range storageTags {
   222  		err = sb.DetachStorage(storageTag, unit.UnitTag(), false, dontWait)
   223  		c.Assert(err, jc.ErrorIsNil)
   224  	}
   225  	for _, volumeTag := range volumeTags {
   226  		err = sb.DetachVolume(machineTag, volumeTag, false)
   227  		c.Assert(err, jc.ErrorIsNil)
   228  		err = sb.RemoveVolumeAttachment(machineTag, volumeTag, false)
   229  		c.Assert(err, jc.ErrorIsNil)
   230  	}
   231  
   232  	_, err = s.State.AddApplication(state.AddApplicationArgs{
   233  		Name:  "storage-block-the-second",
   234  		Charm: ch,
   235  		CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{
   236  			OS:      "ubuntu",
   237  			Channel: "20.04/stable",
   238  		}},
   239  		NumUnits: 1,
   240  		Placement: []*instance.Placement{{
   241  			Scope:     s.State.ModelUUID(),
   242  			Directive: "whatever",
   243  		}},
   244  		AttachStorage: storageTags,
   245  	})
   246  	c.Assert(err, jc.ErrorIsNil)
   247  
   248  	// The volume corresponding to the provisioned storage volume (only)
   249  	// should be presented to PrecheckInstance. The unprovisioned volume
   250  	// will be provisioned later by the storage provisioner.
   251  	c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, "whatever")
   252  	c.Assert(s.prechecker.precheckInstanceArgs.VolumeAttachments, jc.DeepEquals, []storage.VolumeAttachmentParams{{
   253  		AttachmentParams: storage.AttachmentParams{
   254  			Provider: "modelscoped",
   255  		},
   256  		Volume:   volumeTags[0],
   257  		VolumeId: "foo",
   258  	}})
   259  }
   260  
   261  func (s *PrecheckerSuite) TestPrecheckAddApplicationNoPlacement(c *gc.C) {
   262  	s.prechecker.precheckInstanceError = errors.Errorf("failed for some reason")
   263  	ch := s.AddTestingCharm(c, "wordpress")
   264  	_, err := s.State.AddApplication(state.AddApplicationArgs{
   265  		Name:  "wordpress",
   266  		Charm: ch,
   267  		CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{
   268  			OS:      "ubuntu",
   269  			Channel: "12.10/stable",
   270  		}},
   271  		NumUnits:    1,
   272  		Constraints: constraints.MustParse("root-disk=20G"),
   273  	})
   274  	c.Assert(err, gc.ErrorMatches, `cannot add application "wordpress": failed for some reason`)
   275  	c.Assert(s.prechecker.precheckInstanceArgs, jc.DeepEquals, environs.PrecheckInstanceParams{
   276  		Base:        corebase.MakeDefaultBase("ubuntu", "12.10"),
   277  		Constraints: constraints.MustParse("arch=amd64 root-disk=20G"),
   278  	})
   279  }
   280  
   281  func (s *PrecheckerSuite) TestPrecheckAddApplicationAllMachinePlacement(c *gc.C) {
   282  	m1, _, err := s.addMachine(c, constraints.MustParse(""), "")
   283  	c.Assert(err, jc.ErrorIsNil)
   284  	m2, _, err := s.addMachine(c, constraints.MustParse(""), "")
   285  	c.Assert(err, jc.ErrorIsNil)
   286  
   287  	// Make sure the prechecker isn't called.
   288  	s.prechecker.precheckInstanceError = errors.Errorf("boom!")
   289  
   290  	ch := s.AddTestingCharm(c, "wordpress")
   291  	_, err = s.State.AddApplication(state.AddApplicationArgs{
   292  		Name: "wordpress",
   293  		CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{
   294  			OS:      "ubuntu",
   295  			Channel: "20.04/stable",
   296  		}},
   297  		Charm:    ch,
   298  		NumUnits: 2,
   299  		Placement: []*instance.Placement{
   300  			instance.MustParsePlacement(m1.Id()),
   301  			instance.MustParsePlacement(m2.Id()),
   302  		},
   303  	})
   304  	c.Assert(err, jc.ErrorIsNil)
   305  }
   306  
   307  func (s *PrecheckerSuite) TestPrecheckAddApplicationMixedPlacement(c *gc.C) {
   308  	m1, _, err := s.addMachine(c, constraints.MustParse(""), "")
   309  	c.Assert(err, jc.ErrorIsNil)
   310  
   311  	// Make sure the prechecker still gets called if there's a machine
   312  	// placement and a directive that needs to be passed to the
   313  	// provider.
   314  
   315  	s.prechecker.precheckInstanceError = errors.Errorf("hey now")
   316  	ch := s.AddTestingCharm(c, "wordpress")
   317  	_, err = s.State.AddApplication(state.AddApplicationArgs{
   318  		Name: "wordpress",
   319  		CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{
   320  			OS:      "ubuntu",
   321  			Channel: "20.04/stable",
   322  		}},
   323  		Charm:    ch,
   324  		NumUnits: 2,
   325  		Placement: []*instance.Placement{
   326  			{Scope: instance.MachineScope, Directive: m1.Id()},
   327  			{Scope: s.State.ModelUUID(), Directive: "somewhere"},
   328  		},
   329  	})
   330  	c.Assert(err, gc.ErrorMatches, `cannot add application "wordpress": hey now`)
   331  	c.Assert(s.prechecker.precheckInstanceArgs, jc.DeepEquals, environs.PrecheckInstanceParams{
   332  		Base:        corebase.MakeDefaultBase("ubuntu", "20.04"),
   333  		Placement:   "somewhere",
   334  		Constraints: constraints.MustParse("arch=amd64"),
   335  	})
   336  }