github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/core/charm/downloader/downloader.go (about) 1 // Copyright 2021 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package downloader 5 6 import ( 7 "io" 8 "net/url" 9 "os" 10 "strings" 11 12 "github.com/juju/charm/v12" 13 "github.com/juju/errors" 14 "github.com/juju/utils/v3" 15 16 "github.com/juju/juju/core/arch" 17 corecharm "github.com/juju/juju/core/charm" 18 "github.com/juju/juju/core/lxdprofile" 19 "github.com/juju/juju/version" 20 ) 21 22 // Logger defines the logging methods that the package uses. 23 type Logger interface { 24 Tracef(string, ...interface{}) 25 Debugf(string, ...interface{}) 26 Warningf(string, ...interface{}) 27 } 28 29 // CharmArchive provides information about a downloaded charm archive. 30 type CharmArchive interface { 31 corecharm.CharmArchive 32 } 33 34 // CharmRepository provides an API for downloading charms/bundles. 35 type CharmRepository interface { 36 GetDownloadURL(string, corecharm.Origin) (*url.URL, corecharm.Origin, error) 37 ResolveWithPreferredChannel(charmName string, requestedOrigin corecharm.Origin) (*charm.URL, corecharm.Origin, []corecharm.Platform, error) 38 DownloadCharm(charmName string, requestedOrigin corecharm.Origin, archivePath string) (corecharm.CharmArchive, corecharm.Origin, error) 39 } 40 41 // RepositoryGetter returns a suitable CharmRepository for the specified Source. 42 type RepositoryGetter interface { 43 GetCharmRepository(corecharm.Source) (CharmRepository, error) 44 } 45 46 // Storage provides an API for storing downloaded charms. 47 type Storage interface { 48 PrepareToStoreCharm(string) error 49 Store(string, DownloadedCharm) error 50 } 51 52 // DownloadedCharm encapsulates the details of a downloaded charm. 53 type DownloadedCharm struct { 54 // Charm provides information about the charm contents. 55 Charm charm.Charm 56 57 // The charm version. 58 CharmVersion string 59 60 // CharmData provides byte-level access to the downloaded charm data. 61 CharmData io.Reader 62 63 // The Size of the charm data in bytes. 64 Size int64 65 66 // SHA256 is the hash of the bytes in Data. 67 SHA256 string 68 69 // The LXD profile or nil if no profile specified by the charm. 70 LXDProfile *charm.LXDProfile 71 } 72 73 // verify checks that the charm is compatible with the specified Juju version 74 // and ensure that the LXDProfile (if one is specified) is valid. 75 func (dc DownloadedCharm) verify(downloadOrigin corecharm.Origin, force bool) error { 76 if err := version.CheckJujuMinVersion(dc.Charm.Meta().MinJujuVersion, version.Current); err != nil { 77 return errors.Trace(err) 78 } 79 80 if dc.LXDProfile != nil { 81 if err := lxdprofile.ValidateLXDProfile(lxdProfiler{dc.LXDProfile}); err != nil && !force { 82 return errors.Annotate(err, "cannot verify charm-provided LXD profile") 83 } 84 } 85 86 if downloadOrigin.Hash != "" && downloadOrigin.Hash != dc.SHA256 { 87 return errors.NewNotValid(nil, "detected SHA256 hash mismatch") 88 } 89 90 return nil 91 } 92 93 // Downloader implements store-agnostic download and pesistence of charm blobs. 94 type Downloader struct { 95 logger Logger 96 repoGetter RepositoryGetter 97 storage Storage 98 } 99 100 // NewDownloader returns a new charm downloader instance. 101 func NewDownloader(logger Logger, storage Storage, repoGetter RepositoryGetter) *Downloader { 102 return &Downloader{ 103 repoGetter: repoGetter, 104 storage: storage, 105 logger: logger, 106 } 107 } 108 109 // DownloadAndStore looks up the requested charm using the appropriate store, 110 // downloads it to a temporary file and passes it to the configured storage 111 // API so it can be persisted. 112 // 113 // The method ensures that all temporary resources are cleaned up before returning. 114 func (d *Downloader) DownloadAndStore(charmURL *charm.URL, requestedOrigin corecharm.Origin, force bool) (corecharm.Origin, error) { 115 var ( 116 err error 117 channelOrigin = requestedOrigin 118 ) 119 channelOrigin.Platform, err = d.normalizePlatform(charmURL.Name, requestedOrigin.Platform) 120 if err != nil { 121 return corecharm.Origin{}, errors.Trace(err) 122 } 123 124 // Notify the storage layer that we are preparing to upload a charm. 125 if err := d.storage.PrepareToStoreCharm(charmURL.String()); err != nil { 126 // The charm blob is already uploaded this is a no-op. However, 127 // as the original origin might be different that the one 128 // requested by the caller, make sure to resolve it again. 129 if alreadyUploadedErr, valid := errors.Cause(err).(errCharmAlreadyStored); valid { 130 d.logger.Debugf("%v", alreadyUploadedErr) 131 132 repo, err := d.getRepo(requestedOrigin.Source) 133 if err != nil { 134 return corecharm.Origin{}, errors.Trace(err) 135 } 136 _, resolvedOrigin, err := repo.GetDownloadURL(charmURL.Name, requestedOrigin) 137 return resolvedOrigin, errors.Trace(err) 138 } 139 140 return corecharm.Origin{}, errors.Trace(err) 141 } 142 143 // Download charm blob to a temp file 144 tmpFile, err := os.CreateTemp("", charmURL.Name) 145 if err != nil { 146 return corecharm.Origin{}, errors.Trace(err) 147 } 148 defer func() { 149 _ = tmpFile.Close() 150 if err := os.Remove(tmpFile.Name()); err != nil { 151 d.logger.Warningf("unable to remove temporary charm download path %q", tmpFile.Name()) 152 } 153 }() 154 155 repo, err := d.getRepo(requestedOrigin.Source) 156 if err != nil { 157 return corecharm.Origin{}, errors.Trace(err) 158 } 159 160 downloadedCharm, actualOrigin, err := d.downloadAndHash(charmURL.Name, channelOrigin, repo, tmpFile.Name()) 161 if err != nil { 162 return corecharm.Origin{}, errors.Annotatef(err, "downloading charm %q from origin %v", charmURL.Name, requestedOrigin) 163 } 164 165 // Validate charm 166 if err := downloadedCharm.verify(actualOrigin, force); err != nil { 167 return corecharm.Origin{}, errors.Annotatef(err, "verifying downloaded charm %q from origin %v", charmURL.Name, requestedOrigin) 168 } 169 170 // Store Charm 171 if err := d.storeCharm(charmURL.String(), downloadedCharm, tmpFile.Name()); err != nil { 172 return corecharm.Origin{}, errors.Annotatef(err, "storing charm %q from origin %v", charmURL, requestedOrigin) 173 } 174 175 return actualOrigin, nil 176 } 177 178 func (d *Downloader) downloadAndHash(charmName string, requestedOrigin corecharm.Origin, repo CharmRepository, dstPath string) (DownloadedCharm, corecharm.Origin, error) { 179 d.logger.Debugf("downloading charm %q from requested origin %v", charmName, requestedOrigin) 180 chArchive, actualOrigin, err := repo.DownloadCharm(charmName, requestedOrigin, dstPath) 181 if err != nil { 182 return DownloadedCharm{}, corecharm.Origin{}, errors.Trace(err) 183 } 184 d.logger.Debugf("downloaded charm %q from actual origin %v", charmName, actualOrigin) 185 186 // Calculate SHA256 for the downloaded archive 187 f, err := os.Open(dstPath) 188 if err != nil { 189 return DownloadedCharm{}, corecharm.Origin{}, errors.Annotatef(err, "cannot read downloaded charm") 190 } 191 defer func() { _ = f.Close() }() 192 193 sha, size, err := utils.ReadSHA256(f) 194 if err != nil { 195 return DownloadedCharm{}, corecharm.Origin{}, errors.Annotate(err, "cannot calculate SHA256 hash of charm") 196 } 197 198 d.logger.Tracef("downloadResult(%q) sha: %q, size: %d", f.Name(), sha, size) 199 return DownloadedCharm{ 200 Charm: chArchive, 201 CharmVersion: chArchive.Version(), 202 Size: size, 203 LXDProfile: chArchive.LXDProfile(), 204 SHA256: sha, 205 }, actualOrigin, nil 206 } 207 208 func (d *Downloader) storeCharm(charmURL string, dc DownloadedCharm, archivePath string) error { 209 charmArchive, err := os.Open(archivePath) 210 if err != nil { 211 return errors.Annotatef(err, "unable to open downloaded charm archive at %q", archivePath) 212 } 213 defer func() { _ = charmArchive.Close() }() 214 215 dc.CharmData = charmArchive 216 if err := d.storage.Store(charmURL, dc); err != nil { 217 return errors.Trace(err) 218 } 219 return nil 220 } 221 222 func (d *Downloader) normalizePlatform(charmName string, platform corecharm.Platform) (corecharm.Platform, error) { 223 arc := platform.Architecture 224 if platform.Architecture == "" || platform.Architecture == "all" { 225 d.logger.Warningf("received charm Architecture: %q, changing to %q, for charm %q", platform.Architecture, arch.DefaultArchitecture, charmName) 226 arc = arch.DefaultArchitecture 227 } 228 229 // Values passed to the api are case sensitive: ubuntu succeeds and 230 // Ubuntu returns `"code": "revision-not-found"` 231 return corecharm.Platform{ 232 Architecture: arc, 233 OS: strings.ToLower(platform.OS), 234 Channel: platform.Channel, 235 }, nil 236 } 237 238 func (d *Downloader) getRepo(src corecharm.Source) (CharmRepository, error) { 239 repo, err := d.repoGetter.GetCharmRepository(src) 240 if err != nil { 241 return nil, errors.Trace(err) 242 } 243 244 return repo, nil 245 } 246 247 // lxdProfiler is an adaptor that allows passing a charm.LXDProfile to the 248 // core/lxdprofile validation logic. 249 type lxdProfiler struct { 250 profile *charm.LXDProfile 251 } 252 253 func (p lxdProfiler) LXDProfile() lxdprofile.LXDProfile { 254 return p.profile 255 }