github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/state/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 "encoding/json" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/url" 14 "os" 15 "path/filepath" 16 "strings" 17 18 gc "launchpad.net/gocheck" 19 20 "launchpad.net/juju-core/charm" 21 envtesting "launchpad.net/juju-core/environs/testing" 22 jujutesting "launchpad.net/juju-core/juju/testing" 23 "launchpad.net/juju-core/state" 24 "launchpad.net/juju-core/state/api/params" 25 coretesting "launchpad.net/juju-core/testing" 26 jc "launchpad.net/juju-core/testing/checkers" 27 "launchpad.net/juju-core/utils" 28 ) 29 30 type charmsSuite struct { 31 jujutesting.JujuConnSuite 32 userTag string 33 password string 34 } 35 36 var _ = gc.Suite(&charmsSuite{}) 37 38 func (s *charmsSuite) SetUpTest(c *gc.C) { 39 s.JujuConnSuite.SetUpTest(c) 40 password, err := utils.RandomPassword() 41 c.Assert(err, gc.IsNil) 42 user, err := s.State.AddUser("joe", password) 43 c.Assert(err, gc.IsNil) 44 s.userTag = user.Tag() 45 s.password = password 46 } 47 48 func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) { 49 _, info, err := s.APIConn.Environ.StateInfo() 50 c.Assert(err, gc.IsNil) 51 uri := "http://" + info.Addrs[0] + "/charms" 52 _, err = s.sendRequest(c, "", "", "GET", uri, "", nil) 53 c.Assert(err, gc.ErrorMatches, `.*malformed HTTP response.*`) 54 } 55 56 func (s *charmsSuite) TestRequiresAuth(c *gc.C) { 57 resp, err := s.sendRequest(c, "", "", "GET", s.charmsURI(c, ""), "", nil) 58 c.Assert(err, gc.IsNil) 59 s.assertErrorResponse(c, resp, http.StatusUnauthorized, "unauthorized") 60 } 61 62 func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) { 63 resp, err := s.authRequest(c, "PUT", s.charmsURI(c, ""), "", nil) 64 c.Assert(err, gc.IsNil) 65 s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`) 66 } 67 68 func (s *charmsSuite) TestAuthRequiresUser(c *gc.C) { 69 // Add a machine and try to login. 70 machine, err := s.State.AddMachine("quantal", state.JobHostUnits) 71 c.Assert(err, gc.IsNil) 72 err = machine.SetProvisioned("foo", "fake_nonce", nil) 73 c.Assert(err, gc.IsNil) 74 password, err := utils.RandomPassword() 75 c.Assert(err, gc.IsNil) 76 err = machine.SetPassword(password) 77 c.Assert(err, gc.IsNil) 78 79 resp, err := s.sendRequest(c, machine.Tag(), password, "GET", s.charmsURI(c, ""), "", nil) 80 c.Assert(err, gc.IsNil) 81 s.assertErrorResponse(c, resp, http.StatusUnauthorized, "unauthorized") 82 83 // Now try a user login. 84 resp, err = s.authRequest(c, "GET", s.charmsURI(c, ""), "", nil) 85 c.Assert(err, gc.IsNil) 86 s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected url=CharmURL query argument") 87 } 88 89 func (s *charmsSuite) TestUploadRequiresSeries(c *gc.C) { 90 resp, err := s.authRequest(c, "POST", s.charmsURI(c, ""), "", nil) 91 c.Assert(err, gc.IsNil) 92 s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument") 93 } 94 95 func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) { 96 // Create an empty file. 97 tempFile, err := ioutil.TempFile(c.MkDir(), "charm") 98 c.Assert(err, gc.IsNil) 99 100 // Pretend we upload a zip by setting the Content-Type, so we can 101 // check the error at extraction time later. 102 resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name()) 103 c.Assert(err, gc.IsNil) 104 s.assertErrorResponse(c, resp, http.StatusBadRequest, "cannot open charm archive: zip: not a valid zip file") 105 106 // Now try with the default Content-Type. 107 resp, err = s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), false, tempFile.Name()) 108 c.Assert(err, gc.IsNil) 109 s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip, got: application/octet-stream") 110 } 111 112 func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) { 113 // Add the dummy charm with revision 1. 114 ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") 115 curl := charm.MustParseURL( 116 fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()), 117 ) 118 bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1") 119 c.Assert(err, gc.IsNil) 120 _, err = s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256") 121 c.Assert(err, gc.IsNil) 122 123 // Now try uploading the same revision and verify it gets bumped, 124 // and the BundleURL and BundleSha256 are calculated. 125 resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, ch.Path) 126 c.Assert(err, gc.IsNil) 127 expectedURL := charm.MustParseURL("local:quantal/dummy-2") 128 s.assertUploadResponse(c, resp, expectedURL.String()) 129 sch, err := s.State.Charm(expectedURL) 130 c.Assert(err, gc.IsNil) 131 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 132 c.Assert(sch.Revision(), gc.Equals, 2) 133 c.Assert(sch.IsUploaded(), jc.IsTrue) 134 // No more checks for these two here, because they 135 // are verified in TestUploadRespectsLocalRevision. 136 c.Assert(sch.BundleURL(), gc.Not(gc.Equals), "") 137 c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "") 138 } 139 140 func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) { 141 // Make a dummy charm dir with revision 123. 142 dir := coretesting.Charms.ClonedDir(c.MkDir(), "dummy") 143 dir.SetDiskRevision(123) 144 // Now bundle the dir. 145 tempFile, err := ioutil.TempFile(c.MkDir(), "charm") 146 c.Assert(err, gc.IsNil) 147 defer tempFile.Close() 148 defer os.Remove(tempFile.Name()) 149 err = dir.BundleTo(tempFile) 150 c.Assert(err, gc.IsNil) 151 152 // Now try uploading it and ensure the revision persists. 153 resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name()) 154 c.Assert(err, gc.IsNil) 155 expectedURL := charm.MustParseURL("local:quantal/dummy-123") 156 s.assertUploadResponse(c, resp, expectedURL.String()) 157 sch, err := s.State.Charm(expectedURL) 158 c.Assert(err, gc.IsNil) 159 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 160 c.Assert(sch.Revision(), gc.Equals, 123) 161 c.Assert(sch.IsUploaded(), jc.IsTrue) 162 163 // First rewind the reader, which was reset but BundleTo() above. 164 _, err = tempFile.Seek(0, 0) 165 c.Assert(err, gc.IsNil) 166 167 // Finally, verify the SHA256 and uploaded URL. 168 expectedSHA256, _, err := utils.ReadSHA256(tempFile) 169 c.Assert(err, gc.IsNil) 170 name := charm.Quote(expectedURL.String()) 171 storage, err := envtesting.GetEnvironStorage(s.State) 172 c.Assert(err, gc.IsNil) 173 expectedUploadURL, err := storage.URL(name) 174 c.Assert(err, gc.IsNil) 175 176 c.Assert(sch.BundleURL().String(), gc.Equals, expectedUploadURL) 177 c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256) 178 179 reader, err := storage.Get(name) 180 c.Assert(err, gc.IsNil) 181 defer reader.Close() 182 downloadedSHA256, _, err := utils.ReadSHA256(reader) 183 c.Assert(err, gc.IsNil) 184 c.Assert(downloadedSHA256, gc.Equals, expectedSHA256) 185 } 186 187 func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) { 188 // Make a clone of the dummy charm in a nested directory. 189 rootDir := c.MkDir() 190 dirPath := filepath.Join(rootDir, "subdir1", "subdir2") 191 err := os.MkdirAll(dirPath, 0755) 192 c.Assert(err, gc.IsNil) 193 dir := coretesting.Charms.ClonedDir(dirPath, "dummy") 194 // Now tweak the path the dir thinks it is in and bundle it. 195 dir.Path = rootDir 196 tempFile, err := ioutil.TempFile(c.MkDir(), "charm") 197 c.Assert(err, gc.IsNil) 198 defer tempFile.Close() 199 defer os.Remove(tempFile.Name()) 200 err = dir.BundleTo(tempFile) 201 c.Assert(err, gc.IsNil) 202 203 // Try reading it as a bundle - should fail due to nested dirs. 204 _, err = charm.ReadBundle(tempFile.Name()) 205 c.Assert(err, gc.ErrorMatches, "bundle file not found: metadata.yaml") 206 207 // Now try uploading it - should succeeed and be repackaged. 208 resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name()) 209 c.Assert(err, gc.IsNil) 210 expectedURL := charm.MustParseURL("local:quantal/dummy-1") 211 s.assertUploadResponse(c, resp, expectedURL.String()) 212 sch, err := s.State.Charm(expectedURL) 213 c.Assert(err, gc.IsNil) 214 c.Assert(sch.URL(), gc.DeepEquals, expectedURL) 215 c.Assert(sch.Revision(), gc.Equals, 1) 216 c.Assert(sch.IsUploaded(), jc.IsTrue) 217 218 // Get it from the storage and try to read it as a bundle - it 219 // should succeed, because it was repackaged during upload to 220 // strip nested dirs. 221 archiveName := strings.TrimPrefix(sch.BundleURL().RequestURI(), "/dummyenv/private/") 222 storage, err := envtesting.GetEnvironStorage(s.State) 223 c.Assert(err, gc.IsNil) 224 reader, err := storage.Get(archiveName) 225 c.Assert(err, gc.IsNil) 226 defer reader.Close() 227 228 data, err := ioutil.ReadAll(reader) 229 c.Assert(err, gc.IsNil) 230 downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded") 231 c.Assert(err, gc.IsNil) 232 defer downloadedFile.Close() 233 defer os.Remove(downloadedFile.Name()) 234 err = ioutil.WriteFile(downloadedFile.Name(), data, 0644) 235 c.Assert(err, gc.IsNil) 236 237 bundle, err := charm.ReadBundle(downloadedFile.Name()) 238 c.Assert(err, gc.IsNil) 239 c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision()) 240 c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta()) 241 c.Assert(bundle.Config(), jc.DeepEquals, sch.Config()) 242 } 243 244 func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) { 245 uri := s.charmsURI(c, "?file=hooks/install") 246 resp, err := s.authRequest(c, "GET", uri, "", nil) 247 c.Assert(err, gc.IsNil) 248 s.assertErrorResponse( 249 c, resp, http.StatusBadRequest, 250 "expected url=CharmURL query argument", 251 ) 252 } 253 254 func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) { 255 uri := s.charmsURI(c, "?url=local:precise/no-such") 256 resp, err := s.authRequest(c, "GET", uri, "", nil) 257 c.Assert(err, gc.IsNil) 258 s.assertErrorResponse( 259 c, resp, http.StatusBadRequest, 260 "unable to retrieve and save the charm: charm not found in the provider storage: .*", 261 ) 262 } 263 264 func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) { 265 // Add the dummy charm. 266 ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") 267 _, err := s.uploadRequest( 268 c, s.charmsURI(c, "?series=quantal"), true, ch.Path) 269 c.Assert(err, gc.IsNil) 270 271 // Ensure a 404 is returned for files not included in the charm. 272 for i, file := range []string{ 273 "no-such-file", "..", "../../../etc/passwd", "hooks/delete", 274 } { 275 c.Logf("test %d: %s", i, file) 276 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+file) 277 resp, err := s.authRequest(c, "GET", uri, "", nil) 278 c.Assert(err, gc.IsNil) 279 c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound) 280 } 281 } 282 283 func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) { 284 // Add the dummy charm. 285 ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") 286 _, err := s.uploadRequest( 287 c, s.charmsURI(c, "?series=quantal"), true, ch.Path) 288 c.Assert(err, gc.IsNil) 289 290 // Ensure a 403 is returned if the requested file is a directory. 291 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=hooks") 292 resp, err := s.authRequest(c, "GET", uri, "", nil) 293 c.Assert(err, gc.IsNil) 294 c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden) 295 } 296 297 func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) { 298 // Add the dummy charm. 299 ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") 300 _, err := s.uploadRequest( 301 c, s.charmsURI(c, "?series=quantal"), true, ch.Path) 302 c.Assert(err, gc.IsNil) 303 304 // Ensure the file contents are properly returned. 305 for i, t := range []struct { 306 summary string 307 file string 308 response string 309 }{{ 310 summary: "relative path", 311 file: "revision", 312 response: "1", 313 }, { 314 summary: "exotic path", 315 file: "./hooks/../revision", 316 response: "1", 317 }, { 318 summary: "sub-directory path", 319 file: "hooks/install", 320 response: "#!/bin/bash\necho \"Done!\"\n", 321 }, 322 } { 323 c.Logf("test %d: %s", i, t.summary) 324 uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+t.file) 325 resp, err := s.authRequest(c, "GET", uri, "", nil) 326 c.Assert(err, gc.IsNil) 327 s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8") 328 } 329 } 330 331 func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) { 332 // Add the dummy charm. 333 ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") 334 _, err := s.uploadRequest( 335 c, s.charmsURI(c, "?series=quantal"), true, ch.Path) 336 c.Assert(err, gc.IsNil) 337 338 // Ensure charm files are properly listed. 339 uri := s.charmsURI(c, "?url=local:quantal/dummy-1") 340 resp, err := s.authRequest(c, "GET", uri, "", nil) 341 c.Assert(err, gc.IsNil) 342 manifest, err := ch.Manifest() 343 c.Assert(err, gc.IsNil) 344 expectedFiles := manifest.SortedValues() 345 s.assertGetFileListResponse(c, resp, expectedFiles) 346 ctype := resp.Header.Get("content-type") 347 c.Assert(ctype, gc.Equals, "application/json") 348 } 349 350 func (s *charmsSuite) TestGetUsesCache(c *gc.C) { 351 // Add a fake charm archive in the cache directory. 352 cacheDir := filepath.Join(s.DataDir(), "charm-get-cache") 353 err := os.MkdirAll(cacheDir, 0755) 354 c.Assert(err, gc.IsNil) 355 356 // Create and save a bundle in it. 357 charmDir := coretesting.Charms.ClonedDir(c.MkDir(), "dummy") 358 testPath := filepath.Join(charmDir.Path, "utils.js") 359 contents := "// blah blah" 360 err = ioutil.WriteFile(testPath, []byte(contents), 0755) 361 c.Assert(err, gc.IsNil) 362 var buffer bytes.Buffer 363 err = charmDir.BundleTo(&buffer) 364 c.Assert(err, gc.IsNil) 365 charmArchivePath := filepath.Join( 366 cacheDir, charm.Quote("local:trusty/django-42")+".zip") 367 err = ioutil.WriteFile(charmArchivePath, buffer.Bytes(), 0644) 368 c.Assert(err, gc.IsNil) 369 370 // Ensure the cached contents are properly retrieved. 371 uri := s.charmsURI(c, "?url=local:trusty/django-42&file=utils.js") 372 resp, err := s.authRequest(c, "GET", uri, "", nil) 373 c.Assert(err, gc.IsNil) 374 s.assertGetFileResponse(c, resp, contents, "application/javascript") 375 } 376 377 func (s *charmsSuite) charmsURI(c *gc.C, query string) string { 378 _, info, err := s.APIConn.Environ.StateInfo() 379 c.Assert(err, gc.IsNil) 380 return "https://" + info.Addrs[0] + "/charms" + query 381 } 382 383 func (s *charmsSuite) sendRequest(c *gc.C, tag, password, method, uri, contentType string, body io.Reader) (*http.Response, error) { 384 req, err := http.NewRequest(method, uri, body) 385 c.Assert(err, gc.IsNil) 386 if tag != "" && password != "" { 387 req.SetBasicAuth(tag, password) 388 } 389 if contentType != "" { 390 req.Header.Set("Content-Type", contentType) 391 } 392 return utils.GetNonValidatingHTTPClient().Do(req) 393 } 394 395 func (s *charmsSuite) authRequest(c *gc.C, method, uri, contentType string, body io.Reader) (*http.Response, error) { 396 return s.sendRequest(c, s.userTag, s.password, method, uri, contentType, body) 397 } 398 399 func (s *charmsSuite) uploadRequest(c *gc.C, uri string, asZip bool, path string) (*http.Response, error) { 400 contentType := "application/octet-stream" 401 if asZip { 402 contentType = "application/zip" 403 } 404 405 if path == "" { 406 return s.authRequest(c, "POST", uri, contentType, nil) 407 } 408 409 file, err := os.Open(path) 410 c.Assert(err, gc.IsNil) 411 defer file.Close() 412 return s.authRequest(c, "POST", uri, contentType, file) 413 } 414 415 func (s *charmsSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) { 416 body := assertResponse(c, resp, http.StatusOK, "application/json") 417 charmResponse := jsonResponse(c, body) 418 c.Check(charmResponse.Error, gc.Equals, "") 419 c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL) 420 } 421 422 func (s *charmsSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) { 423 body := assertResponse(c, resp, http.StatusOK, expContentType) 424 c.Check(string(body), gc.Equals, expBody) 425 } 426 427 func (s *charmsSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) { 428 body := assertResponse(c, resp, http.StatusOK, "application/json") 429 charmResponse := jsonResponse(c, body) 430 c.Check(charmResponse.Error, gc.Equals, "") 431 c.Check(charmResponse.Files, gc.DeepEquals, expFiles) 432 } 433 434 func (s *charmsSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { 435 body := assertResponse(c, resp, expCode, "application/json") 436 c.Check(jsonResponse(c, body).Error, gc.Matches, expError) 437 } 438 439 func assertResponse(c *gc.C, resp *http.Response, expCode int, expContentType string) []byte { 440 c.Check(resp.StatusCode, gc.Equals, expCode) 441 body, err := ioutil.ReadAll(resp.Body) 442 defer resp.Body.Close() 443 c.Assert(err, gc.IsNil) 444 ctype := resp.Header.Get("Content-Type") 445 c.Assert(ctype, gc.Equals, expContentType) 446 return body 447 } 448 449 func jsonResponse(c *gc.C, body []byte) (jsonResponse params.CharmsResponse) { 450 err := json.Unmarshal(body, &jsonResponse) 451 c.Assert(err, gc.IsNil) 452 return 453 }