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  }