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  }