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

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package migration_test
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"net/url"
    11  	"time"
    12  
    13  	"github.com/juju/charm/v12"
    14  	"github.com/juju/description/v5"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/testing"
    17  	jc "github.com/juju/testing/checkers"
    18  	"github.com/juju/utils/v3"
    19  	"github.com/juju/version/v2"
    20  	gc "gopkg.in/check.v1"
    21  
    22  	"github.com/juju/juju/core/leadership"
    23  	coremigration "github.com/juju/juju/core/migration"
    24  	"github.com/juju/juju/core/resources"
    25  	resourcetesting "github.com/juju/juju/core/resources/testing"
    26  	"github.com/juju/juju/migration"
    27  	"github.com/juju/juju/provider/dummy"
    28  	"github.com/juju/juju/state"
    29  	statetesting "github.com/juju/juju/state/testing"
    30  	coretesting "github.com/juju/juju/testing"
    31  	"github.com/juju/juju/testing/factory"
    32  	"github.com/juju/juju/tools"
    33  )
    34  
    35  type ImportSuite struct {
    36  	statetesting.StateSuite
    37  }
    38  
    39  var _ = gc.Suite(&ImportSuite{})
    40  
    41  func (s *ImportSuite) SetUpTest(c *gc.C) {
    42  	// Specify the config to use for the controller model before
    43  	// calling SetUpTest of the StateSuite, otherwise we get
    44  	// coretesting.ModelConfig(c). The default provider type
    45  	// specified in the coretesting.ModelConfig function is one that
    46  	// isn't registered as a valid provider. For our tests here we
    47  	// need a real registered provider, so we use the dummy provider.
    48  	// NOTE: make a better test provider.
    49  	s.InitialConfig = coretesting.CustomModelConfig(c, dummy.SampleConfig())
    50  	s.StateSuite.SetUpTest(c)
    51  }
    52  
    53  func (s *ImportSuite) TestBadBytes(c *gc.C) {
    54  	bytes := []byte("not a model")
    55  	controller := state.NewController(s.StatePool)
    56  	model, st, err := migration.ImportModel(controller, fakeGetClaimer, bytes)
    57  	c.Check(st, gc.IsNil)
    58  	c.Check(model, gc.IsNil)
    59  	c.Assert(err, gc.ErrorMatches, "yaml: unmarshal errors:\n.*")
    60  }
    61  
    62  func (s *ImportSuite) exportImport(c *gc.C, leaders map[string]string, getClaimer migration.ClaimerFunc) *state.State {
    63  	model, err := s.State.Export(leaders)
    64  	c.Assert(err, jc.ErrorIsNil)
    65  
    66  	// Update the config values in the exported model for different values for
    67  	// "state-port", "api-port", and "ca-cert". Also give the model a new UUID
    68  	// and name, so we can import it nicely.
    69  	uuid := utils.MustNewUUID().String()
    70  	model.UpdateConfig(map[string]interface{}{
    71  		"name": "new-model",
    72  		"uuid": uuid,
    73  	})
    74  
    75  	bytes, err := description.Serialize(model)
    76  	c.Check(err, jc.ErrorIsNil)
    77  
    78  	controller := state.NewController(s.StatePool)
    79  	dbModel, dbState, err := migration.ImportModel(controller, getClaimer, bytes)
    80  	c.Assert(err, jc.ErrorIsNil)
    81  	s.AddCleanup(func(*gc.C) { dbState.Close() })
    82  
    83  	dbConfig, err := dbModel.Config()
    84  	c.Assert(err, jc.ErrorIsNil)
    85  	c.Assert(dbConfig.UUID(), gc.Equals, uuid)
    86  	c.Assert(dbConfig.Name(), gc.Equals, "new-model")
    87  	return dbState
    88  }
    89  
    90  func (s *ImportSuite) TestImportModel(c *gc.C) {
    91  	s.exportImport(c, map[string]string{}, fakeGetClaimer)
    92  }
    93  
    94  func (s *ImportSuite) TestImportsLeadership(c *gc.C) {
    95  	s.makeApplicationWithUnits(c, "wordpress", 3)
    96  	s.makeApplicationWithUnits(c, "mysql", 2)
    97  	leaders := map[string]string{"wordpress": "wordpress/1"}
    98  
    99  	var (
   100  		claimer   fakeClaimer
   101  		modelUUID string
   102  	)
   103  	dbState := s.exportImport(c, leaders, func(uuid string) (leadership.Claimer, error) {
   104  		modelUUID = uuid
   105  		return &claimer, nil
   106  	})
   107  	c.Assert(modelUUID, gc.Equals, dbState.ModelUUID())
   108  	c.Assert(claimer.stub.Calls(), gc.HasLen, 1)
   109  	claimer.stub.CheckCall(c, 0, "ClaimLeadership", "wordpress", "wordpress/1", time.Minute)
   110  }
   111  
   112  func (s *ImportSuite) makeApplicationWithUnits(c *gc.C, applicationname string, count int) {
   113  	units := make([]*state.Unit, count)
   114  	application := s.Factory.MakeApplication(c, &factory.ApplicationParams{
   115  		Name: applicationname,
   116  		Charm: s.Factory.MakeCharm(c, &factory.CharmParams{
   117  			Name: applicationname,
   118  		}),
   119  	})
   120  	for i := 0; i < count; i++ {
   121  		units[i] = s.Factory.MakeUnit(c, &factory.UnitParams{
   122  			Application: application,
   123  		})
   124  	}
   125  }
   126  
   127  func (s *ImportSuite) TestUploadBinariesConfigValidate(c *gc.C) {
   128  	type T migration.UploadBinariesConfig // alias for brevity
   129  
   130  	check := func(modify func(*T), missing string) {
   131  		config := T{
   132  			CharmDownloader:    struct{ migration.CharmDownloader }{},
   133  			CharmUploader:      struct{ migration.CharmUploader }{},
   134  			ToolsDownloader:    struct{ migration.ToolsDownloader }{},
   135  			ToolsUploader:      struct{ migration.ToolsUploader }{},
   136  			ResourceDownloader: struct{ migration.ResourceDownloader }{},
   137  			ResourceUploader:   struct{ migration.ResourceUploader }{},
   138  		}
   139  		modify(&config)
   140  		realConfig := migration.UploadBinariesConfig(config)
   141  		c.Check(realConfig.Validate(), gc.ErrorMatches, fmt.Sprintf("missing %s not valid", missing))
   142  	}
   143  
   144  	check(func(c *T) { c.CharmDownloader = nil }, "CharmDownloader")
   145  	check(func(c *T) { c.CharmUploader = nil }, "CharmUploader")
   146  	check(func(c *T) { c.ToolsDownloader = nil }, "ToolsDownloader")
   147  	check(func(c *T) { c.ToolsUploader = nil }, "ToolsUploader")
   148  	check(func(c *T) { c.ResourceDownloader = nil }, "ResourceDownloader")
   149  	check(func(c *T) { c.ResourceUploader = nil }, "ResourceUploader")
   150  }
   151  
   152  func (s *ImportSuite) TestBinariesMigration(c *gc.C) {
   153  	downloader := &fakeDownloader{}
   154  	uploader := &fakeUploader{
   155  		tools:     make(map[version.Binary]string),
   156  		resources: make(map[string]string),
   157  	}
   158  
   159  	toolsMap := map[version.Binary]string{
   160  		version.MustParseBinary("2.1.0-ubuntu-amd64"): "/tools/0",
   161  		version.MustParseBinary("2.0.0-ubuntu-amd64"): "/tools/1",
   162  	}
   163  
   164  	app0Res := resourcetesting.NewResource(c, nil, "blob0", "app0", "blob0").Resource
   165  	app1Res := resourcetesting.NewResource(c, nil, "blob1", "app1", "blob1").Resource
   166  	app1UnitRes := app1Res
   167  	app1UnitRes.Revision = 1
   168  	app2Res := resourcetesting.NewPlaceholderResource(c, "blob2", "app2")
   169  	resources := []coremigration.SerializedModelResource{
   170  		{ApplicationRevision: app0Res},
   171  		{
   172  			ApplicationRevision: app1Res,
   173  			UnitRevisions:       map[string]resources.Resource{"app1/99": app1UnitRes},
   174  		},
   175  		{ApplicationRevision: app2Res},
   176  	}
   177  
   178  	config := migration.UploadBinariesConfig{
   179  		Charms: []string{
   180  			// These 2 are out of order. Rev 2 must be uploaded first.
   181  			"local:trusty/magic-10",
   182  			"local:trusty/magic-2",
   183  			"ch:trusty/postgresql-42",
   184  		},
   185  		CharmDownloader:    downloader,
   186  		CharmUploader:      uploader,
   187  		Tools:              toolsMap,
   188  		ToolsDownloader:    downloader,
   189  		ToolsUploader:      uploader,
   190  		Resources:          resources,
   191  		ResourceDownloader: downloader,
   192  		ResourceUploader:   uploader,
   193  	}
   194  	err := migration.UploadBinaries(config)
   195  	c.Assert(err, jc.ErrorIsNil)
   196  
   197  	expectedCurls := []string{
   198  		// Note ordering.
   199  		"ch:trusty/postgresql-42",
   200  		"local:trusty/magic-2",
   201  		"local:trusty/magic-10",
   202  	}
   203  	c.Assert(downloader.curls, jc.DeepEquals, expectedCurls)
   204  	c.Assert(uploader.curls, jc.DeepEquals, expectedCurls)
   205  
   206  	expectedRefs := []string{
   207  		"postgresql-a77196f",
   208  		"magic-d348864",
   209  		"magic-5f44d22",
   210  	}
   211  	c.Assert(uploader.charmRefs, jc.DeepEquals, expectedRefs)
   212  
   213  	c.Assert(downloader.uris, jc.SameContents, []string{
   214  		"/tools/0",
   215  		"/tools/1",
   216  	})
   217  	c.Assert(uploader.tools, jc.DeepEquals, toolsMap)
   218  
   219  	c.Assert(downloader.resources, jc.SameContents, []string{
   220  		"app0/blob0",
   221  		"app1/blob1",
   222  	})
   223  	c.Assert(uploader.resources, jc.DeepEquals, map[string]string{
   224  		"app0/blob0": "blob0",
   225  		"app1/blob1": "blob1",
   226  	})
   227  	c.Assert(uploader.unitResources, jc.SameContents, []string{"app1/99-blob1"})
   228  }
   229  
   230  func (s *ImportSuite) TestWrongCharmURLAssigned(c *gc.C) {
   231  	downloader := &fakeDownloader{}
   232  	uploader := &fakeUploader{
   233  		reassignCharmURL: true,
   234  	}
   235  
   236  	config := migration.UploadBinariesConfig{
   237  		Charms:             []string{"local:foo/bar-2"},
   238  		CharmDownloader:    downloader,
   239  		CharmUploader:      uploader,
   240  		ToolsDownloader:    downloader,
   241  		ToolsUploader:      uploader,
   242  		ResourceDownloader: downloader,
   243  		ResourceUploader:   uploader,
   244  	}
   245  	err := migration.UploadBinaries(config)
   246  	c.Assert(err, gc.ErrorMatches,
   247  		"cannot upload charms: charm local:foo/bar-2 unexpectedly assigned local:foo/bar-100")
   248  }
   249  
   250  type fakeDownloader struct {
   251  	curls     []string
   252  	uris      []string
   253  	resources []string
   254  }
   255  
   256  func (d *fakeDownloader) OpenCharm(curl string) (io.ReadCloser, error) {
   257  	d.curls = append(d.curls, curl)
   258  	// Return the charm URL string as the fake charm content
   259  	return io.NopCloser(bytes.NewReader([]byte(curl + " content"))), nil
   260  }
   261  
   262  func (d *fakeDownloader) OpenURI(uri string, query url.Values) (io.ReadCloser, error) {
   263  	if query != nil {
   264  		panic("query should be empty")
   265  	}
   266  	d.uris = append(d.uris, uri)
   267  	// Return the URI string as fake content
   268  	return io.NopCloser(bytes.NewReader([]byte(uri))), nil
   269  }
   270  
   271  func (d *fakeDownloader) OpenResource(app, name string) (io.ReadCloser, error) {
   272  	d.resources = append(d.resources, app+"/"+name)
   273  	// Use the resource name as the content.
   274  	return io.NopCloser(bytes.NewReader([]byte(name))), nil
   275  }
   276  
   277  type fakeUploader struct {
   278  	tools            map[version.Binary]string
   279  	curls            []string
   280  	charmRefs        []string
   281  	resources        map[string]string
   282  	unitResources    []string
   283  	reassignCharmURL bool
   284  }
   285  
   286  func (f *fakeUploader) UploadTools(r io.ReadSeeker, v version.Binary) (tools.List, error) {
   287  	data, err := io.ReadAll(r)
   288  	if err != nil {
   289  		return nil, errors.Trace(err)
   290  	}
   291  	f.tools[v] = string(data)
   292  	return tools.List{&tools.Tools{Version: v}}, nil
   293  }
   294  
   295  func (f *fakeUploader) UploadCharm(curl string, charmRef string, r io.ReadSeeker) (string, error) {
   296  	data, err := io.ReadAll(r)
   297  	if err != nil {
   298  		return "", errors.Trace(err)
   299  	}
   300  	if string(data) != curl+" content" {
   301  		panic(fmt.Sprintf("unexpected charm body for %s: %s", curl, data))
   302  	}
   303  	f.curls = append(f.curls, curl)
   304  	f.charmRefs = append(f.charmRefs, charmRef)
   305  
   306  	outU := curl
   307  	if f.reassignCharmURL {
   308  		outU = charm.MustParseURL(outU).WithRevision(100).String()
   309  	}
   310  	return outU, nil
   311  }
   312  
   313  func (f *fakeUploader) UploadResource(res resources.Resource, r io.ReadSeeker) error {
   314  	body, err := io.ReadAll(r)
   315  	if err != nil {
   316  		return errors.Trace(err)
   317  	}
   318  	f.resources[res.ApplicationID+"/"+res.Name] = string(body)
   319  	return nil
   320  }
   321  
   322  func (f *fakeUploader) SetPlaceholderResource(res resources.Resource) error {
   323  	f.resources[res.ApplicationID+"/"+res.Name] = "<placeholder>"
   324  	return nil
   325  }
   326  
   327  func (f *fakeUploader) SetUnitResource(unit string, res resources.Resource) error {
   328  	f.unitResources = append(f.unitResources, unit+"-"+res.Name)
   329  	return nil
   330  }
   331  
   332  func fakeGetClaimer(string) (leadership.Claimer, error) {
   333  	return &fakeClaimer{}, nil
   334  }
   335  
   336  type fakeClaimer struct {
   337  	leadership.Claimer
   338  	stub testing.Stub
   339  }
   340  
   341  func (c *fakeClaimer) ClaimLeadership(application, unit string, duration time.Duration) error {
   342  	c.stub.AddCall("ClaimLeadership", application, unit, duration)
   343  	return c.stub.NextErr()
   344  }