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

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package migration
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/hex"
     9  	"fmt"
    10  	"io"
    11  	"net/url"
    12  	"os"
    13  	"time"
    14  
    15  	"github.com/juju/charm/v12"
    16  	"github.com/juju/description/v5"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/naturalsort"
    20  	"github.com/juju/version/v2"
    21  
    22  	"github.com/juju/juju/core/leadership"
    23  	corelogger "github.com/juju/juju/core/logger"
    24  	"github.com/juju/juju/core/migration"
    25  	"github.com/juju/juju/core/resources"
    26  	"github.com/juju/juju/state"
    27  	"github.com/juju/juju/tools"
    28  )
    29  
    30  var logger = loggo.GetLoggerWithLabels("juju.migration", corelogger.MIGRATION)
    31  
    32  // StateExporter describes interface on state required to export a
    33  // model.
    34  type StateExporter interface {
    35  	// Export generates an abstract representation of a model.
    36  	Export(leaders map[string]string) (description.Model, error)
    37  }
    38  
    39  // StateImporter describes the method needed to import a model
    40  // into the database.
    41  type StateImporter interface {
    42  	Import(model description.Model) (*state.Model, *state.State, error)
    43  }
    44  
    45  // ClaimerFunc is a function that returns a leadership claimer for the
    46  // model UUID passed.
    47  type ClaimerFunc func(string) (leadership.Claimer, error)
    48  
    49  // ImportModel deserializes a model description from the bytes, transforms
    50  // the model config based on information from the controller model, and then
    51  // imports that as a new database model.
    52  func ImportModel(importer StateImporter, getClaimer ClaimerFunc, bytes []byte) (*state.Model, *state.State, error) {
    53  	model, err := description.Deserialize(bytes)
    54  	if err != nil {
    55  		return nil, nil, errors.Trace(err)
    56  	}
    57  
    58  	dbModel, dbState, err := importer.Import(model)
    59  	if err != nil {
    60  		return nil, nil, errors.Trace(err)
    61  	}
    62  
    63  	claimer, err := getClaimer(dbModel.UUID())
    64  	if err != nil {
    65  		return nil, nil, errors.Annotate(err, "getting leadership claimer")
    66  	}
    67  
    68  	logger.Debugf("importing leadership")
    69  	for _, application := range model.Applications() {
    70  		if application.Leader() == "" {
    71  			continue
    72  		}
    73  		// When we import a new model, we need to give the leaders
    74  		// some time to settle. We don't want to have leader switches
    75  		// just because we migrated a model, so this time needs to be
    76  		// long enough to make sure we cover the time taken to migrate
    77  		// a reasonable sized model. We don't yet know how long this
    78  		// is going to be, but we need something.
    79  		// TODO(babbageclunk): Handle this better - maybe a way to
    80  		// suppress leadership expirations for a model until it is
    81  		// finished importing?
    82  		logger.Debugf("%q is the leader for %q", application.Leader(), application.Name())
    83  		err := claimer.ClaimLeadership(
    84  			application.Name(),
    85  			application.Leader(),
    86  			time.Minute,
    87  		)
    88  		if err != nil {
    89  			return nil, nil, errors.Annotatef(
    90  				err,
    91  				"claiming leadership for %q",
    92  				application.Leader(),
    93  			)
    94  		}
    95  	}
    96  
    97  	return dbModel, dbState, nil
    98  }
    99  
   100  // CharmDownloader defines a single method that is used to download a
   101  // charm from the source controller in a migration.
   102  type CharmDownloader interface {
   103  	OpenCharm(string) (io.ReadCloser, error)
   104  }
   105  
   106  // CharmUploader defines a single method that is used to upload a
   107  // charm to the target controller in a migration.
   108  type CharmUploader interface {
   109  	UploadCharm(charmURL string, charmRef string, content io.ReadSeeker) (string, error)
   110  }
   111  
   112  // ToolsDownloader defines a single method that is used to download
   113  // tools from the source controller in a migration.
   114  type ToolsDownloader interface {
   115  	OpenURI(string, url.Values) (io.ReadCloser, error)
   116  }
   117  
   118  // ToolsUploader defines a single method that is used to upload tools
   119  // to the target controller in a migration.
   120  type ToolsUploader interface {
   121  	UploadTools(io.ReadSeeker, version.Binary) (tools.List, error)
   122  }
   123  
   124  // ResourceDownloader defines the interface for downloading resources
   125  // from the source controller during a migration.
   126  type ResourceDownloader interface {
   127  	OpenResource(string, string) (io.ReadCloser, error)
   128  }
   129  
   130  // ResourceUploader defines the interface for uploading resources into
   131  // the target controller during a migration.
   132  type ResourceUploader interface {
   133  	UploadResource(resources.Resource, io.ReadSeeker) error
   134  	SetPlaceholderResource(resources.Resource) error
   135  	SetUnitResource(string, resources.Resource) error
   136  }
   137  
   138  // UploadBinariesConfig provides all the configuration that the
   139  // UploadBinaries function needs to operate. To construct the config
   140  // with the default helper functions, use `NewUploadBinariesConfig`.
   141  type UploadBinariesConfig struct {
   142  	Charms          []string
   143  	CharmDownloader CharmDownloader
   144  	CharmUploader   CharmUploader
   145  
   146  	Tools           map[version.Binary]string
   147  	ToolsDownloader ToolsDownloader
   148  	ToolsUploader   ToolsUploader
   149  
   150  	Resources          []migration.SerializedModelResource
   151  	ResourceDownloader ResourceDownloader
   152  	ResourceUploader   ResourceUploader
   153  }
   154  
   155  // Validate makes sure that all the config values are non-nil.
   156  func (c *UploadBinariesConfig) Validate() error {
   157  	if c.CharmDownloader == nil {
   158  		return errors.NotValidf("missing CharmDownloader")
   159  	}
   160  	if c.CharmUploader == nil {
   161  		return errors.NotValidf("missing CharmUploader")
   162  	}
   163  	if c.ToolsDownloader == nil {
   164  		return errors.NotValidf("missing ToolsDownloader")
   165  	}
   166  	if c.ToolsUploader == nil {
   167  		return errors.NotValidf("missing ToolsUploader")
   168  	}
   169  	if c.ResourceDownloader == nil {
   170  		return errors.NotValidf("missing ResourceDownloader")
   171  	}
   172  	if c.ResourceUploader == nil {
   173  		return errors.NotValidf("missing ResourceUploader")
   174  	}
   175  	return nil
   176  }
   177  
   178  // UploadBinaries will send binaries stored in the source blobstore to
   179  // the target controller.
   180  func UploadBinaries(config UploadBinariesConfig) error {
   181  	if err := config.Validate(); err != nil {
   182  		return errors.Trace(err)
   183  	}
   184  	if err := uploadCharms(config); err != nil {
   185  		return errors.Annotatef(err, "cannot upload charms")
   186  	}
   187  	if err := uploadTools(config); err != nil {
   188  		return errors.Annotatef(err, "cannot upload agent binaries")
   189  	}
   190  	if err := uploadResources(config); err != nil {
   191  		return errors.Annotatef(err, "cannot upload resources")
   192  	}
   193  	return nil
   194  }
   195  
   196  func streamThroughTempFile(r io.Reader) (_ io.ReadSeeker, cleanup func(), err error) {
   197  	tempFile, err := os.CreateTemp("", "juju-migrate-binary")
   198  	if err != nil {
   199  		return nil, nil, errors.Trace(err)
   200  	}
   201  	defer func() {
   202  		if err != nil {
   203  			_ = tempFile.Close()
   204  			_ = os.Remove(tempFile.Name())
   205  		}
   206  	}()
   207  	_, err = io.Copy(tempFile, r)
   208  	if err != nil {
   209  		return nil, nil, errors.Trace(err)
   210  	}
   211  	_, err = tempFile.Seek(0, 0)
   212  	if err != nil {
   213  		return nil, nil, errors.Annotatef(err, "potentially corrupt binary")
   214  	}
   215  	rmTempFile := func() {
   216  		filename := tempFile.Name()
   217  		_ = tempFile.Close()
   218  		_ = os.Remove(filename)
   219  	}
   220  
   221  	return tempFile, rmTempFile, nil
   222  }
   223  
   224  func hashArchive(archive io.ReadSeeker) (string, error) {
   225  	hash := sha256.New()
   226  	_, err := io.Copy(hash, archive)
   227  	if err != nil {
   228  		return "", errors.Trace(err)
   229  	}
   230  	_, err = archive.Seek(0, os.SEEK_SET)
   231  	if err != nil {
   232  		return "", errors.Trace(err)
   233  	}
   234  	return hex.EncodeToString(hash.Sum(nil))[0:7], nil
   235  }
   236  
   237  func uploadCharms(config UploadBinariesConfig) error {
   238  	// It is critical that charms are uploaded in ascending charm URL
   239  	// order so that charm revisions end up the same in the target as
   240  	// they were in the source.
   241  	naturalsort.Sort(config.Charms)
   242  
   243  	for _, charmURL := range config.Charms {
   244  		logger.Debugf("sending charm %s to target", charmURL)
   245  		reader, err := config.CharmDownloader.OpenCharm(charmURL)
   246  		if err != nil {
   247  			return errors.Annotate(err, "cannot open charm")
   248  		}
   249  		defer func() { _ = reader.Close() }()
   250  
   251  		content, cleanup, err := streamThroughTempFile(reader)
   252  		if err != nil {
   253  			return errors.Trace(err)
   254  		}
   255  		defer cleanup()
   256  
   257  		curl, err := charm.ParseURL(charmURL)
   258  		if err != nil {
   259  			return errors.Annotate(err, "bad charm URL")
   260  		}
   261  		hash, err := hashArchive(content)
   262  		if err != nil {
   263  			return errors.Trace(err)
   264  		}
   265  		charmRef := fmt.Sprintf("%s-%s", curl.Name, hash)
   266  
   267  		if usedCurl, err := config.CharmUploader.UploadCharm(charmURL, charmRef, content); err != nil {
   268  			return errors.Annotate(err, "cannot upload charm")
   269  		} else if usedCurl != charmURL {
   270  			// The target controller shouldn't assign a different charm URL.
   271  			return errors.Errorf("charm %s unexpectedly assigned %s", charmURL, usedCurl)
   272  		}
   273  	}
   274  	return nil
   275  }
   276  
   277  func uploadTools(config UploadBinariesConfig) error {
   278  	for v, uri := range config.Tools {
   279  		logger.Debugf("sending agent binaries to target: %s", v)
   280  
   281  		reader, err := config.ToolsDownloader.OpenURI(uri, nil)
   282  		if err != nil {
   283  			return errors.Annotate(err, "cannot open charm")
   284  		}
   285  		defer func() { _ = reader.Close() }()
   286  
   287  		content, cleanup, err := streamThroughTempFile(reader)
   288  		if err != nil {
   289  			return errors.Trace(err)
   290  		}
   291  		defer cleanup()
   292  
   293  		if _, err := config.ToolsUploader.UploadTools(content, v); err != nil {
   294  			return errors.Annotate(err, "cannot upload agent binaries")
   295  		}
   296  	}
   297  	return nil
   298  }
   299  
   300  func uploadResources(config UploadBinariesConfig) error {
   301  	for _, res := range config.Resources {
   302  		if res.ApplicationRevision.IsPlaceholder() {
   303  			// Resource placeholders created in the migration import rather
   304  			// than attempting to post empty resources.
   305  		} else {
   306  			err := uploadAppResource(config, res.ApplicationRevision)
   307  			if err != nil {
   308  				return errors.Trace(err)
   309  			}
   310  		}
   311  		for unitName, unitRev := range res.UnitRevisions {
   312  			if err := config.ResourceUploader.SetUnitResource(unitName, unitRev); err != nil {
   313  				return errors.Annotate(err, "cannot set unit resource")
   314  			}
   315  		}
   316  		// Each config.Resources element also contains a
   317  		// CharmStoreRevision field. This isn't especially important
   318  		// to migrate so is skipped for now.
   319  	}
   320  	return nil
   321  }
   322  
   323  func uploadAppResource(config UploadBinariesConfig, rev resources.Resource) error {
   324  	logger.Debugf("opening application resource for %s: %s", rev.ApplicationID, rev.Name)
   325  	reader, err := config.ResourceDownloader.OpenResource(rev.ApplicationID, rev.Name)
   326  	if err != nil {
   327  		return errors.Annotate(err, "cannot open resource")
   328  	}
   329  	defer func() { _ = reader.Close() }()
   330  
   331  	// TODO(menn0) - validate that the downloaded revision matches
   332  	// the expected metadata. Check revision and fingerprint.
   333  
   334  	content, cleanup, err := streamThroughTempFile(reader)
   335  	if err != nil {
   336  		return errors.Trace(err)
   337  	}
   338  	defer cleanup()
   339  
   340  	if err := config.ResourceUploader.UploadResource(rev, content); err != nil {
   341  		return errors.Annotate(err, "cannot upload resource")
   342  	}
   343  	return nil
   344  }