github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/charms_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "bytes" 8 "crypto/sha256" 9 "encoding/hex" 10 "encoding/json" 11 "fmt" 12 "io" 13 "mime" 14 "net/http" 15 "net/url" 16 "os" 17 "path/filepath" 18 19 "github.com/juju/charm/v12" 20 jc "github.com/juju/testing/checkers" 21 "github.com/juju/utils/v3" 22 gc "gopkg.in/check.v1" 23 24 "github.com/juju/juju/apiserver/common" 25 apitesting "github.com/juju/juju/apiserver/testing" 26 jujutesting "github.com/juju/juju/juju/testing" 27 "github.com/juju/juju/rpc/params" 28 "github.com/juju/juju/state" 29 "github.com/juju/juju/state/storage" 30 "github.com/juju/juju/testcharms" 31 "github.com/juju/juju/testing/factory" 32 ) 33 34 type charmsSuite struct { 35 apiserverBaseSuite 36 } 37 38 var _ = gc.Suite(&charmsSuite{}) 39 40 func (s *charmsSuite) charmsURL(query string) *url.URL { 41 url := s.URL(fmt.Sprintf("/model/%s/charms", s.State.ModelUUID()), nil) 42 url.RawQuery = query 43 return url 44 } 45 46 func (s *charmsSuite) charmsURI(query string) string { 47 if query != "" && query[0] == '?' { 48 query = query[1:] 49 } 50 return s.charmsURL(query).String() 51 } 52 53 func (s *charmsSuite) uploadRequest(c *gc.C, url, contentType string, content io.Reader) *http.Response { 54 return s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 55 Method: "POST", 56 URL: url, 57 ContentType: contentType, 58 Body: content, 59 }) 60 } 61 62 func (s *charmsSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) { 63 charmResponse := s.assertResponse(c, resp, http.StatusOK) 64 c.Check(charmResponse.Error, gc.Equals, "") 65 c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL) 66 } 67 68 func (s *charmsSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) { 69 body := apitesting.AssertResponse(c, resp, http.StatusOK, expContentType) 70 c.Check(string(body), gc.Equals, expBody) 71 } 72 73 func (s *charmsSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) { 74 charmResponse := s.assertResponse(c, resp, http.StatusOK) 75 c.Check(charmResponse.Error, gc.Equals, "") 76 c.Check(charmResponse.Files, gc.DeepEquals, expFiles) 77 } 78 79 func (s *charmsSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { 80 charmResponse := s.assertResponse(c, resp, expCode) 81 c.Check(charmResponse.Error, gc.Matches, expError) 82 } 83 84 func (s *charmsSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse { 85 body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON) 86 var charmResponse params.CharmsResponse 87 err := json.Unmarshal(body, &charmResponse) 88 c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body)) 89 return charmResponse 90 } 91 92 func (s *charmsSuite) setModelImporting(c *gc.C) { 93 model, err := s.State.Model() 94 c.Assert(err, jc.ErrorIsNil) 95 err = model.SetMigrationMode(state.MigrationModeImporting) 96 c.Assert(err, jc.ErrorIsNil) 97 } 98 99 func (s *charmsSuite) SetUpSuite(c *gc.C) { 100 s.apiserverBaseSuite.SetUpSuite(c) 101 } 102 103 func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) { 104 url := s.charmsURL("") 105 url.Scheme = "http" 106 apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 107 Method: "GET", 108 URL: url.String(), 109 ExpectStatus: http.StatusBadRequest, 110 }) 111 } 112 113 func (s *charmsSuite) TestPOSTRequiresAuth(c *gc.C) { 114 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")}) 115 body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8") 116 c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n") 117 } 118 119 func (s *charmsSuite) TestGETRequiresAuth(c *gc.C) { 120 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: s.charmsURI("")}) 121 body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8") 122 c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n") 123 } 124 125 func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) { 126 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "PUT", URL: s.charmsURI("")}) 127 body := apitesting.AssertResponse(c, resp, http.StatusMethodNotAllowed, "text/plain; charset=utf-8") 128 c.Assert(string(body), gc.Equals, "Method Not Allowed\n") 129 } 130 131 func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) { 132 // Add a machine and try to login. 133 machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 134 Nonce: "noncy", 135 }) 136 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 137 Tag: machine.Tag().String(), 138 Password: password, 139 Method: "POST", 140 URL: s.charmsURI(""), 141 Nonce: "noncy", 142 ContentType: "foo/bar", 143 }) 144 body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8") 145 c.Assert(string(body), gc.Equals, "authorization failed: tag kind machine not valid\n") 146 147 // Now try a user login. 148 resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")}) 149 s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+") 150 } 151 152 func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) { 153 var empty bytes.Buffer 154 155 // Pretend we upload a zip by setting the Content-Type, so we can 156 // check the error at extraction time later. 157 resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &empty) 158 s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*cannot open charm archive: zip: not a valid zip file$") 159 160 // Now try with the default Content-Type. 161 resp = s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/octet-stream", &empty) 162 s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip, got: application/octet-stream$") 163 } 164 165 func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) { 166 // Add the dummy charm with revision 1. 167 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 168 curl := fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()) 169 info := state.CharmInfo{ 170 Charm: ch, 171 ID: curl, 172 StoragePath: "dummy-storage-path", 173 SHA256: "dummy-1-sha256", 174 } 175 _, err := s.State.AddCharm(info) 176 c.Assert(err, jc.ErrorIsNil) 177 178 // Now try uploading the same revision and verify it gets bumped, 179 // and the BundleSha256 is calculated. 180 f, err := os.Open(ch.Path) 181 c.Assert(err, jc.ErrorIsNil) 182 defer f.Close() 183 resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", f) 184 expectedURL := "local:quantal/dummy-2" 185 s.assertUploadResponse(c, resp, expectedURL) 186 sch, err := s.State.Charm(expectedURL) 187 c.Assert(err, jc.ErrorIsNil) 188 c.Assert(sch.URL(), gc.Equals, expectedURL) 189 c.Assert(sch.Revision(), gc.Equals, 2) 190 c.Assert(sch.IsUploaded(), jc.IsTrue) 191 // No more checks for the hash here, because it is 192 // verified in TestUploadRespectsLocalRevision. 193 c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "") 194 } 195 196 func (s *charmsSuite) TestUploadVersion(c *gc.C) { 197 expectedVersion := "dummy-146-g725cfd3-dirty" 198 199 // Add the dummy charm with version "juju-2.4-beta3-146-g725cfd3-dirty". 200 pathToArchive := testcharms.Repo.CharmArchivePath(c.MkDir(), "dummy") 201 err := testcharms.InjectFilesToCharmArchive(pathToArchive, map[string]string{ 202 "version": expectedVersion, 203 }) 204 c.Assert(err, gc.IsNil) 205 ch, err := charm.ReadCharmArchive(pathToArchive) 206 c.Assert(err, gc.IsNil) 207 208 f, err := os.Open(ch.Path) 209 c.Assert(err, jc.ErrorIsNil) 210 defer f.Close() 211 resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", f) 212 213 inputURL := "local:quantal/dummy-1" 214 s.assertUploadResponse(c, resp, inputURL) 215 sch, err := s.State.Charm(inputURL) 216 c.Assert(err, jc.ErrorIsNil) 217 218 version := sch.Version() 219 c.Assert(version, gc.Equals, expectedVersion) 220 } 221 222 func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) { 223 // Make a dummy charm dir with revision 123. 224 dir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy") 225 dir.SetDiskRevision(123) 226 // Now bundle the dir. 227 var buf bytes.Buffer 228 err := dir.ArchiveTo(&buf) 229 c.Assert(err, jc.ErrorIsNil) 230 hash := sha256.New() 231 hash.Write(buf.Bytes()) 232 expectedSHA256 := hex.EncodeToString(hash.Sum(nil)) 233 234 // Now try uploading it and ensure the revision persists. 235 resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &buf) 236 expectedURL := "local:quantal/dummy-123" 237 s.assertUploadResponse(c, resp, expectedURL) 238 sch, err := s.State.Charm(expectedURL) 239 c.Assert(err, jc.ErrorIsNil) 240 c.Assert(sch.URL(), gc.Equals, expectedURL) 241 c.Assert(sch.Revision(), gc.Equals, 123) 242 c.Assert(sch.IsUploaded(), jc.IsTrue) 243 c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256) 244 245 storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession()) 246 reader, _, err := storage.Get(sch.StoragePath()) 247 c.Assert(err, jc.ErrorIsNil) 248 defer reader.Close() 249 downloadedSHA256, _, err := utils.ReadSHA256(reader) 250 c.Assert(err, jc.ErrorIsNil) 251 c.Assert(downloadedSHA256, gc.Equals, expectedSHA256) 252 } 253 254 func (s *charmsSuite) TestUploadWithMultiSeriesCharm(c *gc.C) { 255 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 256 resp := s.uploadRequest(c, s.charmsURL("").String(), "application/zip", &fileReader{path: ch.Path}) 257 expectedURL := "local:dummy-1" 258 s.assertUploadResponse(c, resp, expectedURL) 259 } 260 261 func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) { 262 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 263 // Backwards compatibility check, that we can upload charms to 264 // https://host:port/charms 265 url := s.charmsURL("series=quantal") 266 url.Path = "/charms" 267 resp := s.uploadRequest(c, url.String(), "application/zip", &fileReader{path: ch.Path}) 268 expectedURL := "local:quantal/dummy-1" 269 s.assertUploadResponse(c, resp, expectedURL) 270 } 271 272 func (s *charmsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) { 273 // Check that we can upload charms to https://host:port/ModelUUID/charms 274 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 275 url := s.charmsURL("series=quantal") 276 resp := s.uploadRequest(c, url.String(), "application/zip", &fileReader{path: ch.Path}) 277 expectedURL := "local:quantal/dummy-1" 278 s.assertUploadResponse(c, resp, expectedURL) 279 } 280 281 func (s *charmsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) { 282 newSt := s.Factory.MakeModel(c, nil) 283 defer newSt.Close() 284 285 // Check that we can upload charms to https://host:port/ModelUUID/charms 286 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 287 url := s.charmsURL("series=quantal") 288 url.Path = fmt.Sprintf("/model/%s/charms", newSt.ModelUUID()) 289 resp := s.uploadRequest(c, url.String(), "application/zip", &fileReader{path: ch.Path}) 290 expectedURL := "local:quantal/dummy-1" 291 s.assertUploadResponse(c, resp, expectedURL) 292 } 293 294 func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) { 295 // Make a clone of the dummy charm in a nested directory. 296 rootDir := c.MkDir() 297 dirPath := filepath.Join(rootDir, "subdir1", "subdir2") 298 err := os.MkdirAll(dirPath, 0755) 299 c.Assert(err, jc.ErrorIsNil) 300 dir := testcharms.Repo.ClonedDir(dirPath, "dummy") 301 // Now tweak the path the dir thinks it is in and bundle it. 302 dir.Path = rootDir 303 var buf bytes.Buffer 304 err = dir.ArchiveTo(&buf) 305 c.Assert(err, jc.ErrorIsNil) 306 307 // Try reading it as a bundle - should fail due to nested dirs. 308 _, err = charm.ReadCharmArchiveBytes(buf.Bytes()) 309 c.Assert(err, gc.ErrorMatches, `archive file "metadata.yaml" not found`) 310 311 // Now try uploading it - should succeed and be repackaged. 312 resp := s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &buf) 313 expectedURL := "local:quantal/dummy-1" 314 s.assertUploadResponse(c, resp, expectedURL) 315 sch, err := s.State.Charm(expectedURL) 316 c.Assert(err, jc.ErrorIsNil) 317 c.Assert(sch.URL(), gc.Equals, expectedURL) 318 c.Assert(sch.Revision(), gc.Equals, 1) 319 c.Assert(sch.IsUploaded(), jc.IsTrue) 320 321 // Get it from the storage and try to read it as a bundle - it 322 // should succeed, because it was repackaged during upload to 323 // strip nested dirs. 324 storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession()) 325 reader, _, err := storage.Get(sch.StoragePath()) 326 c.Assert(err, jc.ErrorIsNil) 327 defer reader.Close() 328 329 data, err := io.ReadAll(reader) 330 c.Assert(err, jc.ErrorIsNil) 331 downloadedFile, err := os.CreateTemp(c.MkDir(), "downloaded") 332 c.Assert(err, jc.ErrorIsNil) 333 defer downloadedFile.Close() 334 defer os.Remove(downloadedFile.Name()) 335 err = os.WriteFile(downloadedFile.Name(), data, 0644) 336 c.Assert(err, jc.ErrorIsNil) 337 338 bundle, err := charm.ReadCharmArchive(downloadedFile.Name()) 339 c.Assert(err, jc.ErrorIsNil) 340 c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision()) 341 c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta()) 342 c.Assert(bundle.Config(), jc.DeepEquals, sch.Config()) 343 } 344 345 func (s *charmsSuite) TestNonLocalCharmUploadFailsIfNotMigrating(c *gc.C) { 346 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 347 curl := fmt.Sprintf("ch:quantal/%s-%d", ch.Meta().Name, ch.Revision()) 348 info := state.CharmInfo{ 349 Charm: ch, 350 ID: curl, 351 StoragePath: "dummy-storage-path", 352 SHA256: "dummy-1-sha256", 353 } 354 _, err := s.State.AddCharm(info) 355 c.Assert(err, jc.ErrorIsNil) 356 357 resp := s.uploadRequest(c, s.charmsURI("?schema=ch&series=quantal"), "application/zip", &fileReader{path: ch.Path}) 358 s.assertErrorResponse(c, resp, 400, ".*charms may only be uploaded during model migration import$") 359 } 360 361 func (s *charmsSuite) TestNonLocalCharmUpload(c *gc.C) { 362 // Check that upload of charms with the "ch:" schema works (for 363 // model migrations). 364 s.setModelImporting(c) 365 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 366 367 resp := s.uploadRequest(c, s.charmsURI("?schema=ch&series=quantal"), "application/zip", &fileReader{path: ch.Path}) 368 369 expectedURL := "ch:quantal/dummy-1" 370 s.assertUploadResponse(c, resp, expectedURL) 371 sch, err := s.State.Charm(expectedURL) 372 c.Assert(err, jc.ErrorIsNil) 373 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 374 c.Assert(sch.Revision(), gc.Equals, 1) 375 c.Assert(sch.IsUploaded(), jc.IsTrue) 376 } 377 378 func (s *charmsSuite) TestCharmHubCharmUpload(c *gc.C) { 379 // Check that upload of charms with the "ch:" schema works (for 380 // model migrations). 381 s.setModelImporting(c) 382 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 383 expectedURL := "ch:s390x/bionic/dummy-15" 384 info := state.CharmInfo{ 385 Charm: ch, 386 ID: expectedURL, 387 StoragePath: "dummy-storage-path", 388 SHA256: "dummy-1-sha256", 389 } 390 _, err := s.State.AddCharm(info) 391 c.Assert(err, jc.ErrorIsNil) 392 393 resp := s.uploadRequest(c, s.charmsURI("?arch=s390x&revision=15&schema=ch&series=bionic"), "application/zip", &fileReader{path: ch.Path}) 394 395 s.assertUploadResponse(c, resp, expectedURL) 396 sch, err := s.State.Charm(expectedURL) 397 c.Assert(err, jc.ErrorIsNil) 398 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 399 c.Assert(sch.Revision(), gc.Equals, 15) 400 c.Assert(sch.IsUploaded(), jc.IsTrue) 401 } 402 403 func (s *charmsSuite) TestUnsupportedSchema(c *gc.C) { 404 s.setModelImporting(c) 405 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 406 407 resp := s.uploadRequest(c, s.charmsURI("?schema=zz"), "application/zip", &fileReader{path: ch.Path}) 408 s.assertErrorResponse( 409 c, resp, http.StatusBadRequest, 410 `cannot upload charm: unsupported schema "zz"`, 411 ) 412 } 413 414 func (s *charmsSuite) TestCharmUploadWithUserOverride(c *gc.C) { 415 s.setModelImporting(c) 416 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 417 418 resp := s.uploadRequest(c, s.charmsURI("?schema=ch"), "application/zip", &fileReader{path: ch.Path}) 419 420 expectedURL := "ch:dummy-1" 421 s.assertUploadResponse(c, resp, expectedURL) 422 sch, err := s.State.Charm(expectedURL) 423 c.Assert(err, jc.ErrorIsNil) 424 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 425 c.Assert(sch.IsUploaded(), jc.IsTrue) 426 } 427 428 func (s *charmsSuite) TestNonLocalCharmUploadWithRevisionOverride(c *gc.C) { 429 s.setModelImporting(c) 430 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 431 432 resp := s.uploadRequest(c, s.charmsURI("?schema=ch&&revision=99"), "application/zip", &fileReader{path: ch.Path}) 433 434 expectedURL := "ch:dummy-99" 435 s.assertUploadResponse(c, resp, expectedURL) 436 sch, err := s.State.Charm(expectedURL) 437 c.Assert(err, jc.ErrorIsNil) 438 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 439 c.Assert(sch.Revision(), gc.Equals, 99) 440 c.Assert(sch.IsUploaded(), jc.IsTrue) 441 } 442 443 func (s *charmsSuite) TestMigrateCharm(c *gc.C) { 444 newSt := s.Factory.MakeModel(c, nil) 445 defer newSt.Close() 446 importedModel, err := newSt.Model() 447 c.Assert(err, jc.ErrorIsNil) 448 err = importedModel.SetMigrationMode(state.MigrationModeImporting) 449 c.Assert(err, jc.ErrorIsNil) 450 451 // The default user is just a normal user, not a controller admin 452 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 453 url := s.charmsURL("series=quantal") 454 url.Path = "/migrate/charms" 455 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 456 Method: "POST", 457 URL: url.String(), 458 ContentType: "application/zip", 459 Body: &fileReader{path: ch.Path}, 460 ExtraHeaders: map[string]string{ 461 params.MigrationModelHTTPHeader: importedModel.UUID(), 462 }, 463 }) 464 expectedURL := "local:quantal/dummy-1" 465 s.assertUploadResponse(c, resp, expectedURL) 466 467 // The charm was added to the migrated model. 468 _, err = newSt.Charm(expectedURL) 469 c.Assert(err, jc.ErrorIsNil) 470 } 471 472 func (s *charmsSuite) TestMigrateCharmName(c *gc.C) { 473 newSt := s.Factory.MakeModel(c, nil) 474 defer newSt.Close() 475 importedModel, err := newSt.Model() 476 c.Assert(err, jc.ErrorIsNil) 477 err = importedModel.SetMigrationMode(state.MigrationModeImporting) 478 c.Assert(err, jc.ErrorIsNil) 479 480 // The default user is just a normal user, not a controller admin 481 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 482 url := s.charmsURL("series=quantal&name=meshuggah") 483 url.Path = "/migrate/charms" 484 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 485 Method: "POST", 486 URL: url.String(), 487 ContentType: "application/zip", 488 Body: &fileReader{path: ch.Path}, 489 ExtraHeaders: map[string]string{ 490 params.MigrationModelHTTPHeader: importedModel.UUID(), 491 }, 492 }) 493 expectedURL := "local:quantal/meshuggah-1" 494 s.assertUploadResponse(c, resp, expectedURL) 495 496 // The charm was added to the migrated model. 497 _, err = newSt.Charm(expectedURL) 498 c.Assert(err, jc.ErrorIsNil) 499 } 500 501 func (s *charmsSuite) TestMigrateCharmNotMigrating(c *gc.C) { 502 migratedModel := s.Factory.MakeModel(c, nil) 503 defer migratedModel.Close() 504 505 // The default user is just a normal user, not a controller admin 506 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 507 url := s.charmsURL("series=quantal") 508 url.Path = "/migrate/charms" 509 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 510 Method: "POST", 511 URL: url.String(), 512 ContentType: "application/zip", 513 Body: &fileReader{path: ch.Path}, 514 ExtraHeaders: map[string]string{ 515 params.MigrationModelHTTPHeader: migratedModel.ModelUUID(), 516 }, 517 }) 518 s.assertErrorResponse( 519 c, resp, http.StatusBadRequest, 520 `cannot upload charm: model migration mode is "" instead of "importing"`, 521 ) 522 } 523 524 func (s *charmsSuite) TestMigrateCharmUnauthorized(c *gc.C) { 525 user := s.Factory.MakeUser(c, &factory.UserParams{Password: "hunter2"}) 526 url := s.charmsURL("series=quantal") 527 url.Path = "/migrate/charms" 528 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 529 Method: "POST", 530 URL: url.String(), 531 Tag: user.Tag().String(), 532 Password: "hunter2", 533 }) 534 body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8") 535 c.Assert(string(body), gc.Matches, "authorization failed: user .* not a controller admin\n") 536 } 537 538 func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) { 539 uri := s.charmsURI("?file=hooks/install") 540 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 541 s.assertErrorResponse( 542 c, resp, http.StatusBadRequest, 543 ".*expected url=CharmURL query argument$", 544 ) 545 } 546 547 func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) { 548 uri := s.charmsURI("?url=local:precise/no-such") 549 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 550 s.assertErrorResponse( 551 c, resp, http.StatusNotFound, 552 `.*cannot get charm from state: charm "local:precise/no-such" not found$`, 553 ) 554 } 555 556 func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) { 557 // Add the dummy charm. 558 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 559 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 560 561 // Ensure a 404 is returned for files not included in the charm. 562 for i, file := range []string{ 563 "no-such-file", "..", "../../../etc/passwd", "hooks/delete", 564 } { 565 c.Logf("test %d: %s", i, file) 566 uri := s.charmsURI("?url=local:quantal/dummy-1&file=" + file) 567 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 568 c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound) 569 } 570 } 571 572 func (s *charmsSuite) TestGetReturnsNotYetAvailableForPendingCharms(c *gc.C) { 573 // Add a charm in pending mode. 574 chInfo := state.CharmInfo{ 575 ID: "ch:focal/dummy-1", 576 Charm: testcharms.Repo.CharmArchive(c.MkDir(), "dummy"), 577 StoragePath: "", // indicates that we don't have the data in the blobstore yet. 578 SHA256: "", // indicates that we don't have the data in the blobstore yet. 579 Version: "42", 580 } 581 _, err := s.State.AddCharmMetadata(chInfo) 582 c.Assert(err, jc.ErrorIsNil) 583 584 // Ensure a 490 is returned if the charm is pending to be downloaded. 585 uri := s.charmsURI("?url=ch:focal/dummy-1") 586 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 587 c.Assert(resp.StatusCode, gc.Equals, http.StatusConflict, gc.Commentf("expected to get 409 for charm that is pending to be downloaded")) 588 } 589 590 func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) { 591 // Add the dummy charm. 592 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 593 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 594 595 // Ensure a 403 is returned if the requested file is a directory. 596 uri := s.charmsURI("?url=local:quantal/dummy-1&file=hooks") 597 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 598 c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden) 599 } 600 601 func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) { 602 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 603 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 604 605 // Ensure the file contents are properly returned. 606 for i, t := range []struct { 607 summary string 608 file string 609 response string 610 }{{ 611 summary: "relative path", 612 file: "revision", 613 response: "1", 614 }, { 615 summary: "exotic path", 616 file: "./hooks/../revision", 617 response: "1", 618 }, { 619 summary: "sub-directory path", 620 file: "hooks/install", 621 response: "#!/bin/bash\necho \"Done!\"\n", 622 }, 623 } { 624 c.Logf("test %d: %s", i, t.summary) 625 uri := s.charmsURI("?url=local:quantal/dummy-1&file=" + t.file) 626 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 627 s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8") 628 } 629 } 630 631 func (s *charmsSuite) TestGetCharmIcon(c *gc.C) { 632 // Upload the local charms. 633 ch := testcharms.Repo.CharmArchive(c.MkDir(), "mysql") 634 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 635 ch = testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 636 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 637 638 // Prepare the tests. 639 svgMimeType := mime.TypeByExtension(".svg") 640 iconPath := filepath.Join(testcharms.Repo.CharmDirPath("mysql"), "icon.svg") 641 icon, err := os.ReadFile(iconPath) 642 c.Assert(err, jc.ErrorIsNil) 643 tests := []struct { 644 about string 645 query string 646 expectType string 647 expectBody string 648 }{{ 649 about: "icon found", 650 query: "?url=local:quantal/mysql-1&file=icon.svg", 651 expectBody: string(icon), 652 }, { 653 about: "icon not found", 654 query: "?url=local:quantal/dummy-1&file=icon.svg", 655 }, { 656 about: "default icon requested: icon found", 657 query: "?url=local:quantal/mysql-1&icon=1", 658 expectBody: string(icon), 659 }, { 660 about: "default icon requested: icon not found", 661 query: "?url=local:quantal/dummy-1&icon=1", 662 expectBody: common.DefaultCharmIcon, 663 }, { 664 about: "default icon request ignored", 665 query: "?url=local:quantal/mysql-1&file=revision&icon=1", 666 expectType: "text/plain; charset=utf-8", 667 expectBody: "1", 668 }} 669 670 for i, test := range tests { 671 c.Logf("\ntest %d: %s", i, test.about) 672 uri := s.charmsURI(test.query) 673 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 674 if test.expectBody == "" { 675 s.assertErrorResponse(c, resp, http.StatusNotFound, ".*charm file not found$") 676 continue 677 } 678 if test.expectType == "" { 679 test.expectType = svgMimeType 680 } 681 s.assertGetFileResponse(c, resp, test.expectBody, test.expectType) 682 } 683 } 684 685 func (s *charmsSuite) TestGetWorksForControllerMachines(c *gc.C) { 686 // Make a controller machine. 687 const nonce = "noncey" 688 m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 689 Jobs: []state.MachineJob{state.JobManageModel}, 690 Nonce: nonce, 691 }) 692 693 // Create a hosted model and upload a charm for it. 694 newSt := s.Factory.MakeModel(c, nil) 695 defer newSt.Close() 696 697 curl := charm.MustParseURL("local:quantal/dummy-1") 698 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 699 _, err := jujutesting.AddCharm(newSt, curl, ch, false) 700 c.Assert(err, jc.ErrorIsNil) 701 702 // Controller machine should be able to download the charm from 703 // the hosted model. This is required for controller workers which 704 // are acting on behalf of a particular hosted model. 705 url := s.charmsURL("url=" + curl.String() + "&file=revision") 706 url.Path = fmt.Sprintf("/model/%s/charms", newSt.ModelUUID()) 707 params := apitesting.HTTPRequestParams{ 708 Method: "GET", 709 URL: url.String(), 710 Tag: m.Tag().String(), 711 Password: password, 712 Nonce: nonce, 713 } 714 resp := apitesting.SendHTTPRequest(c, params) 715 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 716 } 717 718 func (s *charmsSuite) TestGetStarReturnsArchiveBytes(c *gc.C) { 719 // Add the dummy charm. 720 ch, err := charm.ReadCharmDir( 721 testcharms.RepoWithSeries("quantal").ClonedDirPath(c.MkDir(), "dummy")) 722 c.Assert(err, jc.ErrorIsNil) 723 // Create an archive from the charm dir. 724 tempFile, err := os.CreateTemp(c.MkDir(), "charm") 725 c.Assert(err, jc.ErrorIsNil) 726 defer tempFile.Close() 727 defer os.Remove(tempFile.Name()) 728 err = ch.ArchiveTo(tempFile) 729 c.Assert(err, jc.ErrorIsNil) 730 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: tempFile.Name()}) 731 732 data, err := os.ReadFile(tempFile.Name()) 733 c.Assert(err, jc.ErrorIsNil) 734 735 uri := s.charmsURI("?url=local:quantal/dummy-1&file=*") 736 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 737 s.assertGetFileResponse(c, resp, string(data), "application/zip") 738 } 739 740 func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) { 741 // Backwards compatibility check, that we can GET from charms at 742 // https://host:port/charms 743 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 744 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 745 url := s.charmsURL("url=local:quantal/dummy-1&file=revision") 746 url.Path = "/charms" 747 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: url.String()}) 748 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 749 } 750 751 func (s *charmsSuite) TestGetAllowsModelUUIDPath(c *gc.C) { 752 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 753 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 754 url := s.charmsURL("url=local:quantal/dummy-1&file=revision") 755 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: url.String()}) 756 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 757 } 758 759 func (s *charmsSuite) TestGetAllowsOtherEnvironment(c *gc.C) { 760 newSt := s.Factory.MakeModel(c, nil) 761 defer newSt.Close() 762 763 curl := charm.MustParseURL("local:quantal/dummy-1") 764 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 765 _, err := jujutesting.AddCharm(newSt, curl, ch, false) 766 c.Assert(err, jc.ErrorIsNil) 767 768 url := s.charmsURL("url=" + curl.String() + "&file=revision") 769 url.Path = fmt.Sprintf("/model/%s/charms", newSt.ModelUUID()) 770 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: url.String()}) 771 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 772 } 773 774 func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) { 775 // Add the dummy charm. 776 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 777 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 778 779 // Ensure charm files are properly listed. 780 uri := s.charmsURI("?url=local:quantal/dummy-1") 781 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 782 manifest, err := ch.ArchiveMembers() 783 c.Assert(err, jc.ErrorIsNil) 784 expectedFiles := manifest.SortedValues() 785 s.assertGetFileListResponse(c, resp, expectedFiles) 786 ctype := resp.Header.Get("content-type") 787 c.Assert(ctype, gc.Equals, params.ContentTypeJSON) 788 } 789 790 func (s *charmsSuite) TestNoTempFilesLeftBehind(c *gc.C) { 791 // Add the dummy charm. 792 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 793 s.uploadRequest(c, s.charmsURI("?series=quantal"), "application/zip", &fileReader{path: ch.Path}) 794 795 // Download it. 796 uri := s.charmsURI("?url=local:quantal/dummy-1&file=*") 797 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: uri}) 798 apitesting.AssertResponse(c, resp, http.StatusOK, "application/zip") 799 800 // Ensure the tmp directory exists but nothing is in it. 801 files, err := os.ReadDir(filepath.Join(s.config.DataDir, "charm-get-tmp")) 802 c.Assert(err, jc.ErrorIsNil) 803 c.Check(files, gc.HasLen, 0) 804 } 805 806 type fileReader struct { 807 path string 808 r io.Reader 809 } 810 811 func (r *fileReader) Read(out []byte) (int, error) { 812 if r.r == nil { 813 content, err := os.ReadFile(r.path) 814 if err != nil { 815 return 0, err 816 } 817 r.r = bytes.NewReader(content) 818 } 819 return r.r.Read(out) 820 }