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 }