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 }