github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "mime" 11 "net/http" 12 "net/url" 13 "os" 14 "path/filepath" 15 "runtime" 16 17 "github.com/juju/errors" 18 jc "github.com/juju/testing/checkers" 19 "github.com/juju/utils" 20 gc "gopkg.in/check.v1" 21 "gopkg.in/juju/charm.v6-unstable" 22 "gopkg.in/macaroon-bakery.v1/httpbakery" 23 24 "github.com/juju/juju/apiserver" 25 "github.com/juju/juju/apiserver/params" 26 "github.com/juju/juju/state" 27 "github.com/juju/juju/state/storage" 28 "github.com/juju/juju/testcharms" 29 "github.com/juju/juju/testing/factory" 30 ) 31 32 // charmsCommonSuite wraps authHTTPSuite and adds 33 // some helper methods suitable for working with the 34 // charms endpoint. 35 type charmsCommonSuite struct { 36 authHTTPSuite 37 } 38 39 func (s *charmsCommonSuite) charmsURL(c *gc.C, query string) *url.URL { 40 uri := s.baseURL(c) 41 if s.modelUUID == "" { 42 uri.Path = "/charms" 43 } else { 44 uri.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID) 45 } 46 uri.RawQuery = query 47 return uri 48 } 49 50 func (s *charmsCommonSuite) charmsURI(c *gc.C, query string) string { 51 if query != "" && query[0] == '?' { 52 query = query[1:] 53 } 54 return s.charmsURL(c, query).String() 55 } 56 57 func (s *charmsCommonSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) { 58 charmResponse := s.assertResponse(c, resp, http.StatusOK) 59 c.Check(charmResponse.Error, gc.Equals, "") 60 c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL) 61 } 62 63 func (s *charmsCommonSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) { 64 body := assertResponse(c, resp, http.StatusOK, expContentType) 65 c.Check(string(body), gc.Equals, expBody) 66 } 67 68 func (s *charmsCommonSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) { 69 charmResponse := s.assertResponse(c, resp, http.StatusOK) 70 c.Check(charmResponse.Error, gc.Equals, "") 71 c.Check(charmResponse.Files, gc.DeepEquals, expFiles) 72 } 73 74 func (s *charmsCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { 75 charmResponse := s.assertResponse(c, resp, expCode) 76 c.Check(charmResponse.Error, gc.Matches, expError) 77 } 78 79 func (s *charmsCommonSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse { 80 body := assertResponse(c, resp, expStatus, params.ContentTypeJSON) 81 var charmResponse params.CharmsResponse 82 err := json.Unmarshal(body, &charmResponse) 83 c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body)) 84 return charmResponse 85 } 86 87 func (s *charmsCommonSuite) setModelImporting(c *gc.C) { 88 model, err := s.State.Model() 89 c.Assert(err, jc.ErrorIsNil) 90 err = model.SetMigrationMode(state.MigrationModeImporting) 91 c.Assert(err, jc.ErrorIsNil) 92 } 93 94 type charmsSuite struct { 95 charmsCommonSuite 96 } 97 98 var _ = gc.Suite(&charmsSuite{}) 99 100 func (s *charmsSuite) SetUpSuite(c *gc.C) { 101 // TODO(bogdanteleaga): Fix this on windows 102 if runtime.GOOS == "windows" { 103 c.Skip("bug 1403084: Skipping this on windows for now") 104 } 105 s.charmsCommonSuite.SetUpSuite(c) 106 } 107 108 func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) { 109 info := s.APIInfo(c) 110 uri := "http://" + info.Addrs[0] + "/charms" 111 s.sendRequest(c, httpRequestParams{ 112 method: "GET", 113 url: uri, 114 expectError: `.*malformed HTTP response.*`, 115 }) 116 } 117 118 func (s *charmsSuite) TestPOSTRequiresAuth(c *gc.C) { 119 resp := s.sendRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")}) 120 s.assertErrorResponse(c, resp, http.StatusUnauthorized, ".*no credentials provided$") 121 } 122 123 func (s *charmsSuite) TestGETRequiresAuth(c *gc.C) { 124 resp := s.sendRequest(c, httpRequestParams{method: "GET", url: s.charmsURI(c, "")}) 125 s.assertErrorResponse(c, resp, http.StatusUnauthorized, ".*no credentials provided$") 126 } 127 128 func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) { 129 resp := s.authRequest(c, httpRequestParams{method: "PUT", url: s.charmsURI(c, "")}) 130 s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`) 131 } 132 133 func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) { 134 // Add a machine and try to login. 135 machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 136 Nonce: "noncy", 137 }) 138 resp := s.sendRequest(c, httpRequestParams{ 139 tag: machine.Tag().String(), 140 password: password, 141 method: "POST", 142 url: s.charmsURI(c, ""), 143 nonce: "noncy", 144 contentType: "foo/bar", 145 }) 146 s.assertErrorResponse(c, resp, http.StatusInternalServerError, ".*tag kind machine not valid$") 147 148 // Now try a user login. 149 resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")}) 150 s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+") 151 } 152 153 func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) { 154 // Create an empty file. 155 tempFile, err := ioutil.TempFile(c.MkDir(), "charm") 156 c.Assert(err, jc.ErrorIsNil) 157 158 // Pretend we upload a zip by setting the Content-Type, so we can 159 // check the error at extraction time later. 160 resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name()) 161 s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*cannot open charm archive: zip: not a valid zip file$") 162 163 // Now try with the default Content-Type. 164 resp = s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/octet-stream", tempFile.Name()) 165 s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip, got: application/octet-stream$") 166 } 167 168 func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) { 169 // Add the dummy charm with revision 1. 170 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 171 curl := charm.MustParseURL( 172 fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()), 173 ) 174 info := state.CharmInfo{ 175 Charm: ch, 176 ID: curl, 177 StoragePath: "dummy-storage-path", 178 SHA256: "dummy-1-sha256", 179 } 180 _, err := s.State.AddCharm(info) 181 c.Assert(err, jc.ErrorIsNil) 182 183 // Now try uploading the same revision and verify it gets bumped, 184 // and the BundleSha256 is calculated. 185 resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 186 expectedURL := charm.MustParseURL("local:quantal/dummy-2") 187 s.assertUploadResponse(c, resp, expectedURL.String()) 188 sch, err := s.State.Charm(expectedURL) 189 c.Assert(err, jc.ErrorIsNil) 190 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 191 c.Assert(sch.Revision(), gc.Equals, 2) 192 c.Assert(sch.IsUploaded(), jc.IsTrue) 193 // No more checks for the hash here, because it is 194 // verified in TestUploadRespectsLocalRevision. 195 c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "") 196 } 197 198 func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) { 199 // Make a dummy charm dir with revision 123. 200 dir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy") 201 dir.SetDiskRevision(123) 202 // Now bundle the dir. 203 tempFile, err := ioutil.TempFile(c.MkDir(), "charm") 204 c.Assert(err, jc.ErrorIsNil) 205 defer tempFile.Close() 206 defer os.Remove(tempFile.Name()) 207 err = dir.ArchiveTo(tempFile) 208 c.Assert(err, jc.ErrorIsNil) 209 210 // Now try uploading it and ensure the revision persists. 211 resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name()) 212 expectedURL := charm.MustParseURL("local:quantal/dummy-123") 213 s.assertUploadResponse(c, resp, expectedURL.String()) 214 sch, err := s.State.Charm(expectedURL) 215 c.Assert(err, jc.ErrorIsNil) 216 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 217 c.Assert(sch.Revision(), gc.Equals, 123) 218 c.Assert(sch.IsUploaded(), jc.IsTrue) 219 220 // First rewind the reader, which was reset but BundleTo() above. 221 _, err = tempFile.Seek(0, 0) 222 c.Assert(err, jc.ErrorIsNil) 223 224 // Finally, verify the SHA256. 225 expectedSHA256, _, err := utils.ReadSHA256(tempFile) 226 c.Assert(err, jc.ErrorIsNil) 227 228 c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256) 229 230 storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession()) 231 reader, _, err := storage.Get(sch.StoragePath()) 232 c.Assert(err, jc.ErrorIsNil) 233 defer reader.Close() 234 downloadedSHA256, _, err := utils.ReadSHA256(reader) 235 c.Assert(err, jc.ErrorIsNil) 236 c.Assert(downloadedSHA256, gc.Equals, expectedSHA256) 237 } 238 239 func (s *charmsSuite) TestUploadWithMultiSeriesCharm(c *gc.C) { 240 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 241 resp := s.uploadRequest(c, s.charmsURL(c, "").String(), "application/zip", ch.Path) 242 expectedURL := charm.MustParseURL("local:dummy-1") 243 s.assertUploadResponse(c, resp, expectedURL.String()) 244 } 245 246 func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) { 247 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 248 // Backwards compatibility check, that we can upload charms to 249 // https://host:port/charms 250 url := s.charmsURL(c, "series=quantal") 251 url.Path = "/charms" 252 resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path) 253 expectedURL := charm.MustParseURL("local:quantal/dummy-1") 254 s.assertUploadResponse(c, resp, expectedURL.String()) 255 } 256 257 func (s *charmsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) { 258 // Check that we can upload charms to https://host:port/ModelUUID/charms 259 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 260 url := s.charmsURL(c, "series=quantal") 261 url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID) 262 resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path) 263 expectedURL := charm.MustParseURL("local:quantal/dummy-1") 264 s.assertUploadResponse(c, resp, expectedURL.String()) 265 } 266 267 func (s *charmsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) { 268 envState := s.setupOtherModel(c) 269 // Check that we can upload charms to https://host:port/ModelUUID/charms 270 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 271 url := s.charmsURL(c, "series=quantal") 272 url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID()) 273 resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path) 274 expectedURL := charm.MustParseURL("local:quantal/dummy-1") 275 s.assertUploadResponse(c, resp, expectedURL.String()) 276 } 277 278 func (s *charmsSuite) TestUploadRejectsWrongModelUUIDPath(c *gc.C) { 279 // Check that we cannot upload charms to https://host:port/BADModelUUID/charms 280 url := s.charmsURL(c, "series=quantal") 281 url.Path = "/model/dead-beef-123456/charms" 282 resp := s.authRequest(c, httpRequestParams{method: "POST", url: url.String()}) 283 s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"$`) 284 } 285 286 func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) { 287 // Make a clone of the dummy charm in a nested directory. 288 rootDir := c.MkDir() 289 dirPath := filepath.Join(rootDir, "subdir1", "subdir2") 290 err := os.MkdirAll(dirPath, 0755) 291 c.Assert(err, jc.ErrorIsNil) 292 dir := testcharms.Repo.ClonedDir(dirPath, "dummy") 293 // Now tweak the path the dir thinks it is in and bundle it. 294 dir.Path = rootDir 295 tempFile, err := ioutil.TempFile(c.MkDir(), "charm") 296 c.Assert(err, jc.ErrorIsNil) 297 defer tempFile.Close() 298 defer os.Remove(tempFile.Name()) 299 err = dir.ArchiveTo(tempFile) 300 c.Assert(err, jc.ErrorIsNil) 301 302 // Try reading it as a bundle - should fail due to nested dirs. 303 _, err = charm.ReadCharmArchive(tempFile.Name()) 304 c.Assert(err, gc.ErrorMatches, `archive file "metadata.yaml" not found`) 305 306 // Now try uploading it - should succeeed and be repackaged. 307 resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name()) 308 expectedURL := charm.MustParseURL("local:quantal/dummy-1") 309 s.assertUploadResponse(c, resp, expectedURL.String()) 310 sch, err := s.State.Charm(expectedURL) 311 c.Assert(err, jc.ErrorIsNil) 312 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 313 c.Assert(sch.Revision(), gc.Equals, 1) 314 c.Assert(sch.IsUploaded(), jc.IsTrue) 315 316 // Get it from the storage and try to read it as a bundle - it 317 // should succeed, because it was repackaged during upload to 318 // strip nested dirs. 319 storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession()) 320 reader, _, err := storage.Get(sch.StoragePath()) 321 c.Assert(err, jc.ErrorIsNil) 322 defer reader.Close() 323 324 data, err := ioutil.ReadAll(reader) 325 c.Assert(err, jc.ErrorIsNil) 326 downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded") 327 c.Assert(err, jc.ErrorIsNil) 328 defer downloadedFile.Close() 329 defer os.Remove(downloadedFile.Name()) 330 err = ioutil.WriteFile(downloadedFile.Name(), data, 0644) 331 c.Assert(err, jc.ErrorIsNil) 332 333 bundle, err := charm.ReadCharmArchive(downloadedFile.Name()) 334 c.Assert(err, jc.ErrorIsNil) 335 c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision()) 336 c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta()) 337 c.Assert(bundle.Config(), jc.DeepEquals, sch.Config()) 338 } 339 340 func (s *charmsSuite) TestNonLocalCharmUploadFailsIfNotMigrating(c *gc.C) { 341 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 342 curl := charm.MustParseURL( 343 fmt.Sprintf("cs:quantal/%s-%d", ch.Meta().Name, ch.Revision()), 344 ) 345 info := state.CharmInfo{ 346 Charm: ch, 347 ID: curl, 348 StoragePath: "dummy-storage-path", 349 SHA256: "dummy-1-sha256", 350 } 351 _, err := s.State.AddCharm(info) 352 c.Assert(err, jc.ErrorIsNil) 353 354 resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path) 355 s.assertErrorResponse(c, resp, 400, ".*cs charms may only be uploaded during model migration import$") 356 } 357 358 func (s *charmsSuite) TestNonLocalCharmUpload(c *gc.C) { 359 // Check that upload of charms with the "cs:" schema works (for 360 // model migrations). 361 s.setModelImporting(c) 362 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 363 364 resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path) 365 366 expectedURL := charm.MustParseURL("cs:quantal/dummy-1") 367 s.assertUploadResponse(c, resp, expectedURL.String()) 368 sch, err := s.State.Charm(expectedURL) 369 c.Assert(err, jc.ErrorIsNil) 370 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 371 c.Assert(sch.Revision(), gc.Equals, 1) 372 c.Assert(sch.IsUploaded(), jc.IsTrue) 373 } 374 375 func (s *charmsSuite) TestNonLocalCharmUploadWithRevisionOverride(c *gc.C) { 376 s.setModelImporting(c) 377 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 378 379 resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&name=dummy&revision=99"), "application/zip", ch.Path) 380 381 expectedURL := charm.MustParseURL("cs:dummy-99") 382 s.assertUploadResponse(c, resp, expectedURL.String()) 383 sch, err := s.State.Charm(expectedURL) 384 c.Assert(err, jc.ErrorIsNil) 385 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 386 c.Assert(sch.Revision(), gc.Equals, 99) 387 c.Assert(sch.IsUploaded(), jc.IsTrue) 388 } 389 390 func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) { 391 uri := s.charmsURI(c, "?file=hooks/install") 392 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 393 s.assertErrorResponse( 394 c, resp, http.StatusBadRequest, 395 ".*expected url=CharmURL query argument$", 396 ) 397 } 398 399 func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) { 400 uri := s.charmsURI(c, "?url=local:precise/no-such") 401 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 402 s.assertErrorResponse( 403 c, resp, http.StatusNotFound, 404 `.*cannot get charm from state: charm "local:precise/no-such" not found$`, 405 ) 406 } 407 408 func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) { 409 // Add the dummy charm. 410 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 411 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 412 413 // Ensure a 404 is returned for files not included in the charm. 414 for i, file := range []string{ 415 "no-such-file", "..", "../../../etc/passwd", "hooks/delete", 416 } { 417 c.Logf("test %d: %s", i, file) 418 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+file) 419 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 420 c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound) 421 } 422 } 423 424 func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) { 425 // Add the dummy charm. 426 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 427 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 428 429 // Ensure a 403 is returned if the requested file is a directory. 430 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=hooks") 431 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 432 c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden) 433 } 434 435 func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) { 436 // Add the dummy charm. 437 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 438 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 439 440 // Ensure the file contents are properly returned. 441 for i, t := range []struct { 442 summary string 443 file string 444 response string 445 }{{ 446 summary: "relative path", 447 file: "revision", 448 response: "1", 449 }, { 450 summary: "exotic path", 451 file: "./hooks/../revision", 452 response: "1", 453 }, { 454 summary: "sub-directory path", 455 file: "hooks/install", 456 response: "#!/bin/bash\necho \"Done!\"\n", 457 }, 458 } { 459 c.Logf("test %d: %s", i, t.summary) 460 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+t.file) 461 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 462 s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8") 463 } 464 } 465 466 func (s *charmsSuite) TestGetCharmIcon(c *gc.C) { 467 // Upload the local charms. 468 ch := testcharms.Repo.CharmArchive(c.MkDir(), "mysql") 469 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 470 ch = testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 471 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 472 473 // Prepare the tests. 474 svgMimeType := mime.TypeByExtension(".svg") 475 iconPath := filepath.Join(testcharms.Repo.CharmDirPath("mysql"), "icon.svg") 476 icon, err := ioutil.ReadFile(iconPath) 477 c.Assert(err, jc.ErrorIsNil) 478 tests := []struct { 479 about string 480 query string 481 expectType string 482 expectBody string 483 }{{ 484 about: "icon found", 485 query: "?url=local:quantal/mysql-1&file=icon.svg", 486 expectBody: string(icon), 487 }, { 488 about: "icon not found", 489 query: "?url=local:quantal/dummy-1&file=icon.svg", 490 }, { 491 about: "default icon requested: icon found", 492 query: "?url=local:quantal/mysql-1&icon=1", 493 expectBody: string(icon), 494 }, { 495 about: "default icon requested: icon not found", 496 query: "?url=local:quantal/dummy-1&icon=1", 497 expectBody: apiserver.DefaultIcon, 498 }, { 499 about: "default icon request ignored", 500 query: "?url=local:quantal/mysql-1&file=revision&icon=1", 501 expectType: "text/plain; charset=utf-8", 502 expectBody: "1", 503 }} 504 505 for i, test := range tests { 506 c.Logf("\ntest %d: %s", i, test.about) 507 uri := s.charmsURI(c, test.query) 508 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 509 if test.expectBody == "" { 510 s.assertErrorResponse(c, resp, http.StatusNotFound, ".*charm file not found$") 511 continue 512 } 513 if test.expectType == "" { 514 test.expectType = svgMimeType 515 } 516 s.assertGetFileResponse(c, resp, test.expectBody, test.expectType) 517 } 518 } 519 520 func (s *charmsSuite) TestGetWorksForControllerMachines(c *gc.C) { 521 // Make a controller machine. 522 const nonce = "noncey" 523 m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 524 Jobs: []state.MachineJob{state.JobManageModel}, 525 Nonce: nonce, 526 }) 527 528 // Create a hosted model and upload a charm for it. 529 envState := s.setupOtherModel(c) 530 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 531 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 532 533 // Controller machine should be able to download the charm from 534 // the hosted model. This is required for controller workers which 535 // are acting on behalf of a particular hosted model. 536 url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") 537 url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID()) 538 params := httpRequestParams{ 539 method: "GET", 540 url: url.String(), 541 tag: m.Tag().String(), 542 password: password, 543 nonce: nonce, 544 } 545 resp := s.sendRequest(c, params) 546 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 547 } 548 549 func (s *charmsSuite) TestGetStarReturnsArchiveBytes(c *gc.C) { 550 // Add the dummy charm. 551 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 552 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 553 554 data, err := ioutil.ReadFile(ch.Path) 555 c.Assert(err, jc.ErrorIsNil) 556 557 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=*") 558 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 559 s.assertGetFileResponse(c, resp, string(data), "application/zip") 560 } 561 562 func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) { 563 // Backwards compatibility check, that we can GET from charms at 564 // https://host:port/charms 565 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 566 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 567 url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") 568 url.Path = "/charms" 569 resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()}) 570 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 571 } 572 573 func (s *charmsSuite) TestGetAllowsModelUUIDPath(c *gc.C) { 574 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 575 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 576 url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") 577 url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID) 578 resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()}) 579 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 580 } 581 582 func (s *charmsSuite) TestGetAllowsOtherEnvironment(c *gc.C) { 583 envState := s.setupOtherModel(c) 584 585 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 586 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 587 url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") 588 url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID()) 589 resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()}) 590 s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") 591 } 592 593 func (s *charmsSuite) TestGetRejectsWrongModelUUIDPath(c *gc.C) { 594 url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") 595 url.Path = "/model/dead-beef-123456/charms" 596 resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()}) 597 s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"$`) 598 } 599 600 func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) { 601 // Add the dummy charm. 602 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 603 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 604 605 // Ensure charm files are properly listed. 606 uri := s.charmsURI(c, "?url=local:quantal/dummy-1") 607 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 608 manifest, err := ch.Manifest() 609 c.Assert(err, jc.ErrorIsNil) 610 expectedFiles := manifest.SortedValues() 611 s.assertGetFileListResponse(c, resp, expectedFiles) 612 ctype := resp.Header.Get("content-type") 613 c.Assert(ctype, gc.Equals, params.ContentTypeJSON) 614 } 615 616 func (s *charmsSuite) TestNoTempFilesLeftBehind(c *gc.C) { 617 // Add the dummy charm. 618 ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 619 s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path) 620 621 // Download it. 622 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=*") 623 resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri}) 624 assertResponse(c, resp, http.StatusOK, "application/zip") 625 626 // Ensure the tmp directory exists but nothing is in it. 627 files, err := ioutil.ReadDir(filepath.Join(s.DataDir(), "charm-get-tmp")) 628 c.Assert(err, jc.ErrorIsNil) 629 c.Check(files, gc.HasLen, 0) 630 } 631 632 func (s *charmsSuite) TestPOST_BadCharmNameErrorMessage(c *gc.C) { 633 url := s.charmsURL(c, "url=local:quantal/bad-name-1&file=revision") 634 url.Path = "/model/dead-beef-123456/charms" 635 resp := s.authRequest(c, httpRequestParams{method: "POST", url: url.String()}) 636 s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"$`) 637 } 638 639 type charmsWithMacaroonsSuite struct { 640 charmsCommonSuite 641 } 642 643 var _ = gc.Suite(&charmsWithMacaroonsSuite{}) 644 645 func (s *charmsWithMacaroonsSuite) SetUpTest(c *gc.C) { 646 s.macaroonAuthEnabled = true 647 s.authHTTPSuite.SetUpTest(c) 648 } 649 650 func (s *charmsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) { 651 resp := s.sendRequest(c, httpRequestParams{ 652 method: "POST", 653 url: s.charmsURI(c, ""), 654 }) 655 656 charmResponse := s.assertResponse(c, resp, http.StatusUnauthorized) 657 c.Assert(charmResponse.Error, gc.Matches, ".*verification failed: no macaroons$") 658 c.Assert(charmResponse.ErrorCode, gc.Equals, params.CodeDischargeRequired) 659 c.Assert(charmResponse.ErrorInfo, gc.NotNil) 660 c.Assert(charmResponse.ErrorInfo.Macaroon, gc.NotNil) 661 } 662 663 func (s *charmsWithMacaroonsSuite) TestCanPostWithDischargedMacaroon(c *gc.C) { 664 checkCount := 0 665 s.DischargerLogin = func() string { 666 checkCount++ 667 return s.userTag.Id() 668 } 669 resp := s.sendRequest(c, httpRequestParams{ 670 do: s.doer(), 671 method: "POST", 672 url: s.charmsURI(c, ""), 673 contentType: "foo/bar", 674 }) 675 s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+") 676 c.Assert(checkCount, gc.Equals, 1) 677 } 678 679 // doer returns a Do function that can make a bakery request 680 // appropriate for a charms endpoint. 681 func (s *charmsWithMacaroonsSuite) doer() func(*http.Request) (*http.Response, error) { 682 return bakeryDo(nil, charmsBakeryGetError) 683 } 684 685 // charmsBakeryGetError implements a getError function 686 // appropriate for passing to httpbakery.Client.DoWithBodyAndCustomError 687 // for the charms endpoint. 688 func charmsBakeryGetError(resp *http.Response) error { 689 if resp.StatusCode != http.StatusUnauthorized { 690 return nil 691 } 692 data, err := ioutil.ReadAll(resp.Body) 693 if err != nil { 694 return errors.Annotatef(err, "cannot read body") 695 } 696 var charmResp params.CharmsResponse 697 if err := json.Unmarshal(data, &charmResp); err != nil { 698 return errors.Annotatef(err, "cannot unmarshal body") 699 } 700 errResp := ¶ms.Error{ 701 Message: charmResp.Error, 702 Code: charmResp.ErrorCode, 703 Info: charmResp.ErrorInfo, 704 } 705 if errResp.Code != params.CodeDischargeRequired { 706 return errResp 707 } 708 if errResp.Info == nil { 709 return errors.Annotatef(err, "no error info found in discharge-required response error") 710 } 711 // It's a discharge-required error, so make an appropriate httpbakery 712 // error from it. 713 return &httpbakery.Error{ 714 Message: errResp.Message, 715 Code: httpbakery.ErrDischargeRequired, 716 Info: &httpbakery.ErrorInfo{ 717 Macaroon: errResp.Info.Macaroon, 718 MacaroonPath: errResp.Info.MacaroonPath, 719 }, 720 } 721 }