github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/core/charm/downloader/downloader_test.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package downloader_test
     5  
     6  import (
     7  	"errors"
     8  	"io"
     9  	"net/url"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"github.com/juju/charm/v12"
    14  	"github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	"github.com/juju/version/v2"
    17  	"go.uber.org/mock/gomock"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	corecharm "github.com/juju/juju/core/charm"
    21  	"github.com/juju/juju/core/charm/downloader"
    22  	"github.com/juju/juju/core/charm/downloader/mocks"
    23  )
    24  
    25  var _ = gc.Suite(&downloaderSuite{})
    26  var _ = gc.Suite(&downloadedCharmVerificationSuite{})
    27  
    28  type downloadedCharmVerificationSuite struct {
    29  	testing.IsolationSuite
    30  }
    31  
    32  func (s *downloadedCharmVerificationSuite) TestVersionMismatch(c *gc.C) {
    33  	ctrl := gomock.NewController(c)
    34  	defer ctrl.Finish()
    35  
    36  	charmArchive := mocks.NewMockCharmArchive(ctrl)
    37  	charmArchive.EXPECT().Meta().Return(&charm.Meta{
    38  		MinJujuVersion: version.MustParse("42.0.0"),
    39  	})
    40  
    41  	dc := downloader.DownloadedCharm{
    42  		Charm: charmArchive,
    43  	}
    44  
    45  	err := dc.Verify(corecharm.Origin{}, false)
    46  	c.Assert(err, gc.ErrorMatches, ".*min version.*is higher.*")
    47  }
    48  
    49  // TestSHA256CheckSkipping ensures that SHA256 checks are skipped when
    50  // downloading charms from charmstore which does not return an expected SHA256
    51  // hash to check against.
    52  func (s *downloadedCharmVerificationSuite) TestSHA256CheckSkipping(c *gc.C) {
    53  	ctrl := gomock.NewController(c)
    54  	defer ctrl.Finish()
    55  
    56  	charmArchive := mocks.NewMockCharmArchive(ctrl)
    57  	charmArchive.EXPECT().Meta().Return(&charm.Meta{
    58  		MinJujuVersion: version.MustParse("0.0.42"),
    59  	})
    60  
    61  	dc := downloader.DownloadedCharm{
    62  		Charm:  charmArchive,
    63  		SHA256: "this-is-not-the-hash-that-you-are-looking-for",
    64  	}
    65  
    66  	err := dc.Verify(corecharm.Origin{}, false)
    67  	c.Assert(err, jc.ErrorIsNil)
    68  }
    69  
    70  func (s *downloadedCharmVerificationSuite) TestSHA256Mismatch(c *gc.C) {
    71  	ctrl := gomock.NewController(c)
    72  	defer ctrl.Finish()
    73  
    74  	charmArchive := mocks.NewMockCharmArchive(ctrl)
    75  	charmArchive.EXPECT().Meta().Return(&charm.Meta{
    76  		MinJujuVersion: version.MustParse("0.0.42"),
    77  	})
    78  
    79  	dc := downloader.DownloadedCharm{
    80  		Charm:  charmArchive,
    81  		SHA256: "this-is-not-the-hash-that-you-are-looking-for",
    82  	}
    83  
    84  	err := dc.Verify(corecharm.Origin{Hash: "the-real-hash"}, false)
    85  	c.Assert(err, gc.ErrorMatches, "detected SHA256 hash mismatch")
    86  }
    87  
    88  func (s *downloadedCharmVerificationSuite) TestLXDProfileValidationError(c *gc.C) {
    89  	ctrl := gomock.NewController(c)
    90  	defer ctrl.Finish()
    91  
    92  	charmArchive := mocks.NewMockCharmArchive(ctrl)
    93  	charmArchive.EXPECT().Meta().Return(&charm.Meta{
    94  		MinJujuVersion: version.MustParse("0.0.42"),
    95  	})
    96  
    97  	dc := downloader.DownloadedCharm{
    98  		Charm:  charmArchive,
    99  		SHA256: "sha256",
   100  		LXDProfile: &charm.LXDProfile{
   101  			Config: map[string]string{
   102  				"boot": "run-a-keylogger",
   103  			},
   104  		},
   105  	}
   106  
   107  	err := dc.Verify(corecharm.Origin{Hash: "sha256"}, false)
   108  	c.Assert(err, gc.ErrorMatches, ".*cannot verify charm-provided LXD profile.*")
   109  }
   110  
   111  type downloaderSuite struct {
   112  	testing.IsolationSuite
   113  	charmArchive *mocks.MockCharmArchive
   114  	repoGetter   *mocks.MockRepositoryGetter
   115  	repo         *mocks.MockCharmRepository
   116  	storage      *mocks.MockStorage
   117  	logger       *mocks.MockLogger
   118  }
   119  
   120  func (s *downloaderSuite) TestDownloadAndHash(c *gc.C) {
   121  	defer s.setupMocks(c).Finish()
   122  
   123  	tmpFile := filepath.Join(c.MkDir(), "ubuntu-lite.zip")
   124  	c.Assert(os.WriteFile(tmpFile, []byte("meshuggah\n"), 0644), jc.ErrorIsNil)
   125  
   126  	name := "ch:ubuntu-lite"
   127  	requestedOrigin := corecharm.Origin{Source: corecharm.CharmHub, Channel: mustParseChannel(c, "20.04/edge")}
   128  	resolvedOrigin := corecharm.Origin{Source: corecharm.CharmHub, Channel: mustParseChannel(c, "20.04/candidate")}
   129  
   130  	s.repo.EXPECT().DownloadCharm(name, requestedOrigin, tmpFile).Return(s.charmArchive, resolvedOrigin, nil)
   131  	s.charmArchive.EXPECT().Version().Return("the-version")
   132  	s.charmArchive.EXPECT().LXDProfile().Return(nil)
   133  
   134  	dl := s.newDownloader()
   135  	dc, gotOrigin, err := dl.DownloadAndHash(name, requestedOrigin, repoAdapter{s.repo}, tmpFile)
   136  	c.Assert(err, jc.ErrorIsNil)
   137  	c.Assert(gotOrigin, gc.DeepEquals, resolvedOrigin, gc.Commentf("expected to get back the resolved origin"))
   138  	c.Assert(dc.SHA256, gc.Equals, "4e97ed7423be2ea12939e8fdd592cfb3dcd4d0097d7d193ef998ab6b4db70461")
   139  	c.Assert(dc.Size, gc.Equals, int64(10))
   140  }
   141  
   142  func (s downloaderSuite) TestCharmAlreadyStored(c *gc.C) {
   143  	defer s.setupMocks(c).Finish()
   144  
   145  	curl := charm.MustParseURL("ch:redis-0")
   146  	requestedOrigin := corecharm.Origin{Source: corecharm.CharmHub, Channel: mustParseChannel(c, "20.04/edge")}
   147  	knownOrigin := corecharm.Origin{Source: corecharm.CharmHub, ID: "knowncharmhubid", Hash: "knowncharmhash", Channel: mustParseChannel(c, "20.04/candidate")}
   148  
   149  	s.storage.EXPECT().PrepareToStoreCharm(curl.String()).Return(
   150  		downloader.NewCharmAlreadyStoredError(curl.String()),
   151  	)
   152  	s.repoGetter.EXPECT().GetCharmRepository(corecharm.CharmHub).Return(repoAdapter{s.repo}, nil)
   153  	retURL, _ := url.Parse(curl.String())
   154  	s.repo.EXPECT().GetDownloadURL(curl.Name, requestedOrigin).Return(retURL, knownOrigin, nil)
   155  
   156  	dl := s.newDownloader()
   157  	gotOrigin, err := dl.DownloadAndStore(curl, requestedOrigin, false)
   158  	c.Assert(gotOrigin, gc.DeepEquals, knownOrigin, gc.Commentf("expected to get back the known origin for the existing charm"))
   159  	c.Assert(err, jc.ErrorIsNil)
   160  }
   161  
   162  func (s downloaderSuite) TestPrepareToStoreCharmError(c *gc.C) {
   163  	defer s.setupMocks(c).Finish()
   164  
   165  	curl := charm.MustParseURL("ch:redis-0")
   166  	requestedOrigin := corecharm.Origin{Source: corecharm.CharmHub, Channel: mustParseChannel(c, "20.04/edge")}
   167  
   168  	s.storage.EXPECT().PrepareToStoreCharm(curl.String()).Return(
   169  		errors.New("something went wrong"),
   170  	)
   171  
   172  	dl := s.newDownloader()
   173  	gotOrigin, err := dl.DownloadAndStore(curl, requestedOrigin, false)
   174  	c.Assert(gotOrigin, gc.DeepEquals, corecharm.Origin{}, gc.Commentf("expected a blank origin when encountering errors"))
   175  	c.Assert(err, gc.ErrorMatches, "something went wrong")
   176  }
   177  
   178  func (s downloaderSuite) TestNormalizePlatform(c *gc.C) {
   179  	name := "ubuntu-lite"
   180  	requestedPlatform := corecharm.Platform{
   181  		Channel: "20.04",
   182  		OS:      "Ubuntu",
   183  	}
   184  
   185  	gotPlatform, err := s.newDownloader().NormalizePlatform(name, requestedPlatform)
   186  	c.Assert(err, jc.ErrorIsNil)
   187  	c.Assert(gotPlatform, gc.DeepEquals, corecharm.Platform{
   188  		Architecture: "amd64",
   189  		Channel:      "20.04",
   190  		OS:           "ubuntu", // notice lower case
   191  	})
   192  }
   193  
   194  func (s downloaderSuite) TestDownloadAndStore(c *gc.C) {
   195  	defer s.setupMocks(c).Finish()
   196  
   197  	curl := charm.MustParseURL("ch:ubuntu-lite")
   198  	requestedOrigin := corecharm.Origin{
   199  		Source: corecharm.CharmHub,
   200  	}
   201  	requestedOriginWithPlatform := corecharm.Origin{
   202  		Source: corecharm.CharmHub,
   203  		Platform: corecharm.Platform{
   204  			Architecture: "amd64",
   205  		},
   206  	}
   207  	resolvedOrigin := corecharm.Origin{
   208  		Source: corecharm.CharmHub,
   209  		Hash:   "4e97ed7423be2ea12939e8fdd592cfb3dcd4d0097d7d193ef998ab6b4db70461",
   210  		Platform: corecharm.Platform{
   211  			Architecture: "amd64",
   212  		},
   213  	}
   214  
   215  	c.Log(curl.String())
   216  	s.storage.EXPECT().PrepareToStoreCharm(curl.String()).Return(nil)
   217  	s.storage.EXPECT().Store(curl.String(), gomock.AssignableToTypeOf(downloader.DownloadedCharm{})).DoAndReturn(
   218  		func(_ string, dc downloader.DownloadedCharm) error {
   219  			c.Assert(dc.Size, gc.Equals, int64(10))
   220  
   221  			contents, err := io.ReadAll(dc.CharmData)
   222  			c.Assert(err, jc.ErrorIsNil)
   223  			c.Assert(string(contents), gc.DeepEquals, "meshuggah\n", gc.Commentf("read charm contents do not match the data written to disk"))
   224  			c.Assert(dc.CharmVersion, gc.Equals, "the-version")
   225  			c.Assert(dc.SHA256, gc.Equals, "4e97ed7423be2ea12939e8fdd592cfb3dcd4d0097d7d193ef998ab6b4db70461")
   226  
   227  			return nil
   228  		},
   229  	)
   230  	s.repoGetter.EXPECT().GetCharmRepository(corecharm.CharmHub).Return(repoAdapter{s.repo}, nil)
   231  	s.repo.EXPECT().DownloadCharm(curl.Name, requestedOriginWithPlatform, gomock.Any()).DoAndReturn(
   232  		func(_ string, requestedOrigin corecharm.Origin, archivePath string) (downloader.CharmArchive, corecharm.Origin, error) {
   233  			c.Assert(os.WriteFile(archivePath, []byte("meshuggah\n"), 0644), jc.ErrorIsNil)
   234  			return s.charmArchive, resolvedOrigin, nil
   235  		},
   236  	)
   237  	s.charmArchive.EXPECT().Meta().Return(&charm.Meta{
   238  		MinJujuVersion: version.MustParse("0.0.42"),
   239  	})
   240  	s.charmArchive.EXPECT().Version().Return("the-version")
   241  	s.charmArchive.EXPECT().LXDProfile().Return(nil)
   242  
   243  	dl := s.newDownloader()
   244  	gotOrigin, err := dl.DownloadAndStore(curl, requestedOrigin, false)
   245  	c.Assert(gotOrigin, gc.DeepEquals, resolvedOrigin, gc.Commentf("expected to get back the resolved origin"))
   246  	c.Assert(err, jc.ErrorIsNil)
   247  }
   248  
   249  func (s *downloaderSuite) setupMocks(c *gc.C) *gomock.Controller {
   250  	ctrl := gomock.NewController(c)
   251  	s.charmArchive = mocks.NewMockCharmArchive(ctrl)
   252  	s.repo = mocks.NewMockCharmRepository(ctrl)
   253  	s.repoGetter = mocks.NewMockRepositoryGetter(ctrl)
   254  	s.storage = mocks.NewMockStorage(ctrl)
   255  	s.logger = mocks.NewMockLogger(ctrl)
   256  	s.logger.EXPECT().Warningf(gomock.Any(), gomock.Any()).AnyTimes()
   257  	s.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
   258  	s.logger.EXPECT().Tracef(gomock.Any(), gomock.Any()).AnyTimes()
   259  	return ctrl
   260  }
   261  
   262  func (s *downloaderSuite) newDownloader() *downloader.Downloader {
   263  	return downloader.NewDownloader(s.logger, s.storage, s.repoGetter)
   264  }
   265  
   266  func mustParseChannel(c *gc.C, channel string) *charm.Channel {
   267  	ch, err := charm.ParseChannel(channel)
   268  	c.Assert(err, jc.ErrorIsNil)
   269  	return &ch
   270  }
   271  
   272  // repoAdapter is an adapter that allows us to use MockCharmRepository whose
   273  // DownloadCharm method returns a CharmArchive instead of the similarly named
   274  // interface in core/charm (which the package-local version embeds).
   275  //
   276  // This allows us to use a package-local mock for CharmArchive while testing.
   277  type repoAdapter struct {
   278  	repo *mocks.MockCharmRepository
   279  }
   280  
   281  func (r repoAdapter) DownloadCharm(charmName string, requestedOrigin corecharm.Origin, archivePath string) (corecharm.CharmArchive, corecharm.Origin, error) {
   282  	return r.repo.DownloadCharm(charmName, requestedOrigin, archivePath)
   283  }
   284  
   285  func (r repoAdapter) ResolveWithPreferredChannel(charmName string, requestedOrigin corecharm.Origin) (*charm.URL, corecharm.Origin, []corecharm.Platform, error) {
   286  	return r.repo.ResolveWithPreferredChannel(charmName, requestedOrigin)
   287  }
   288  
   289  func (r repoAdapter) GetDownloadURL(charmName string, requestedOrigin corecharm.Origin) (*url.URL, corecharm.Origin, error) {
   290  	return r.repo.GetDownloadURL(charmName, requestedOrigin)
   291  }