github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/apiserver/images_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // +build !windows
     5  
     6  package apiserver_test
     7  
     8  import (
     9  	"crypto/sha256"
    10  	"encoding/base64"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io/ioutil"
    14  	"net/http"
    15  	"net/url"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	"github.com/juju/testing"
    21  	jc "github.com/juju/testing/checkers"
    22  	gc "gopkg.in/check.v1"
    23  
    24  	"github.com/juju/juju/apiserver/params"
    25  	containertesting "github.com/juju/juju/container/testing"
    26  	sstesting "github.com/juju/juju/environs/simplestreams/testing"
    27  	"github.com/juju/juju/state"
    28  	"github.com/juju/juju/state/imagestorage"
    29  	coretesting "github.com/juju/juju/testing"
    30  )
    31  
    32  const testImageData = "abc"
    33  
    34  var testImageChecksum = fmt.Sprintf("%x", sha256.Sum256([]byte(testImageData)))
    35  
    36  type imageSuite struct {
    37  	authHttpSuite
    38  }
    39  
    40  var _ = gc.Suite(&imageSuite{})
    41  
    42  func (s *imageSuite) SetUpSuite(c *gc.C) {
    43  	s.authHttpSuite.SetUpSuite(c)
    44  }
    45  
    46  func (s *imageSuite) TestDownloadMissingModelUUIDPath(c *gc.C) {
    47  	s.storeFakeImage(c, s.State, "lxc", "trusty", "amd64")
    48  
    49  	s.modelUUID = ""
    50  	url := s.imageURL(c, "lxc", "trusty", "amd64")
    51  	c.Assert(url.Path, jc.HasPrefix, "/model//images")
    52  
    53  	response := s.downloadRequest(c, url)
    54  	s.testDownload(c, response)
    55  }
    56  
    57  func (s *imageSuite) TestDownloadEnvironmentPath(c *gc.C) {
    58  	s.storeFakeImage(c, s.State, "lxc", "trusty", "amd64")
    59  
    60  	url := s.imageURL(c, "lxc", "trusty", "amd64")
    61  	c.Assert(url.Path, jc.HasPrefix, fmt.Sprintf("/model/%s/", s.State.ModelUUID()))
    62  
    63  	response := s.downloadRequest(c, url)
    64  	s.testDownload(c, response)
    65  }
    66  
    67  func (s *imageSuite) TestDownloadOtherEnvironmentPath(c *gc.C) {
    68  	envState := s.setupOtherModel(c)
    69  	s.storeFakeImage(c, envState, "lxc", "trusty", "amd64")
    70  
    71  	url := s.imageURL(c, "lxc", "trusty", "amd64")
    72  	c.Assert(url.Path, jc.HasPrefix, fmt.Sprintf("/model/%s/", envState.ModelUUID()))
    73  
    74  	response := s.downloadRequest(c, url)
    75  	s.testDownload(c, response)
    76  }
    77  
    78  func (s *imageSuite) TestDownloadRejectsWrongModelUUIDPath(c *gc.C) {
    79  	s.modelUUID = "dead-beef-123456"
    80  	url := s.imageURL(c, "lxc", "trusty", "amd64")
    81  	response := s.downloadRequest(c, url)
    82  	s.assertErrorResponse(c, response, http.StatusNotFound, `unknown model: "dead-beef-123456"`)
    83  }
    84  
    85  type CountingRoundTripper struct {
    86  	count int
    87  	*coretesting.CannedRoundTripper
    88  }
    89  
    90  func (v *CountingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    91  	v.count += 1
    92  	return v.CannedRoundTripper.RoundTrip(req)
    93  }
    94  
    95  func useTestImageData(files map[string]string) {
    96  	if files != nil {
    97  		sstesting.TestRoundTripper.Sub = &CountingRoundTripper{
    98  			CannedRoundTripper: coretesting.NewCannedRoundTripper(files, nil),
    99  		}
   100  	} else {
   101  		sstesting.TestRoundTripper.Sub = nil
   102  	}
   103  }
   104  
   105  func (s *imageSuite) TestDownloadFetchesAndCaches(c *gc.C) {
   106  	// Set up some image data for a fake server.
   107  	testing.PatchExecutable(c, s, "ubuntu-cloudimg-query", containertesting.FakeLxcURLScript)
   108  	useTestImageData(map[string]string{
   109  		"/trusty-released-amd64-root.tar.gz": testImageData,
   110  		"/SHA256SUMS":                        testImageChecksum + " *trusty-released-amd64-root.tar.gz",
   111  	})
   112  	defer func() {
   113  		useTestImageData(nil)
   114  	}()
   115  
   116  	// The image is not in imagestorage, so the download request causes
   117  	// the API server to search for the image on cloud-images, fetches it,
   118  	// and then cache it in imagestorage.
   119  	url := s.imageURL(c, "lxc", "trusty", "amd64")
   120  	response := s.downloadRequest(c, url)
   121  	data := s.testDownload(c, response)
   122  
   123  	metadata, cachedData := s.getImageFromStorage(c, s.State, "lxc", "trusty", "amd64")
   124  	c.Assert(metadata.Size, gc.Equals, int64(len(testImageData)))
   125  	c.Assert(metadata.SHA256, gc.Equals, testImageChecksum)
   126  	c.Assert(metadata.SourceURL, gc.Equals, "test://cloud-images/trusty-released-amd64-root.tar.gz")
   127  	c.Assert(string(data), gc.Equals, string(testImageData))
   128  	c.Assert(string(data), gc.Equals, string(cachedData))
   129  }
   130  
   131  func (s *imageSuite) TestDownloadFetchesAndCachesConcurrent(c *gc.C) {
   132  	// Set up some image data for a fake server.
   133  	testing.PatchExecutable(c, s, "ubuntu-cloudimg-query", containertesting.FakeLxcURLScript)
   134  	useTestImageData(map[string]string{
   135  		"/trusty-released-amd64-root.tar.gz": testImageData,
   136  		"/SHA256SUMS":                        testImageChecksum + " *trusty-released-amd64-root.tar.gz",
   137  	})
   138  	defer func() {
   139  		useTestImageData(nil)
   140  	}()
   141  
   142  	// Fetch the same image multiple times concurrently and ensure that
   143  	// it is only downloaded from the external URL once.
   144  	done := make(chan struct{})
   145  	go func() {
   146  		var wg sync.WaitGroup
   147  		wg.Add(10)
   148  		for i := 0; i < 10; i++ {
   149  			go func() {
   150  				defer wg.Done()
   151  				url := s.imageURL(c, "lxc", "trusty", "amd64")
   152  				response := s.downloadRequest(c, url)
   153  				data := s.testDownload(c, response)
   154  				c.Assert(string(data), gc.Equals, string(testImageData))
   155  			}()
   156  		}
   157  		wg.Wait()
   158  		done <- struct{}{}
   159  	}()
   160  	select {
   161  	case <-done:
   162  	case <-time.After(coretesting.LongWait):
   163  		c.Fatalf("timed out waiting for images to be fetced")
   164  	}
   165  
   166  	// Downloading an image is 2 requests - one for image, one for SA256.
   167  	c.Assert(sstesting.TestRoundTripper.Sub.(*CountingRoundTripper).count, gc.Equals, 2)
   168  
   169  	// Check that the image is correctly cached.
   170  	metadata, cachedData := s.getImageFromStorage(c, s.State, "lxc", "trusty", "amd64")
   171  	c.Assert(metadata.Size, gc.Equals, int64(len(testImageData)))
   172  	c.Assert(metadata.SHA256, gc.Equals, testImageChecksum)
   173  	c.Assert(metadata.SourceURL, gc.Equals, "test://cloud-images/trusty-released-amd64-root.tar.gz")
   174  	c.Assert(testImageData, gc.Equals, string(cachedData))
   175  }
   176  
   177  func (s *imageSuite) TestDownloadFetchChecksumMismatch(c *gc.C) {
   178  	// Set up some image data for a fake server.
   179  	testing.PatchExecutable(c, s, "ubuntu-cloudimg-query", containertesting.FakeLxcURLScript)
   180  	useTestImageData(map[string]string{
   181  		"/trusty-released-amd64-root.tar.gz": testImageData,
   182  		"/SHA256SUMS":                        "different-checksum *trusty-released-amd64-root.tar.gz",
   183  	})
   184  	defer func() {
   185  		useTestImageData(nil)
   186  	}()
   187  
   188  	resp := s.downloadRequest(c, s.imageURL(c, "lxc", "trusty", "amd64"))
   189  	defer resp.Body.Close()
   190  	s.assertErrorResponse(c, resp, http.StatusInternalServerError, ".* download checksum mismatch .*")
   191  }
   192  
   193  func (s *imageSuite) TestDownloadFetchNoSHA256File(c *gc.C) {
   194  	// Set up some image data for a fake server.
   195  	testing.PatchExecutable(c, s, "ubuntu-cloudimg-query", containertesting.FakeLxcURLScript)
   196  	useTestImageData(map[string]string{
   197  		"/trusty-released-amd64-root.tar.gz": testImageData,
   198  	})
   199  	defer func() {
   200  		useTestImageData(nil)
   201  	}()
   202  
   203  	resp := s.downloadRequest(c, s.imageURL(c, "lxc", "trusty", "amd64"))
   204  	defer resp.Body.Close()
   205  	s.assertErrorResponse(c, resp, http.StatusInternalServerError, ".* cannot find sha256 checksum .*")
   206  }
   207  
   208  func (s *imageSuite) testDownload(c *gc.C, resp *http.Response) []byte {
   209  	c.Check(resp.StatusCode, gc.Equals, http.StatusOK)
   210  	expectedChecksum := base64.StdEncoding.EncodeToString([]byte(testImageChecksum))
   211  	c.Check(resp.Header.Get("Digest"), gc.Equals, string(params.DigestSHA256)+"="+expectedChecksum)
   212  	c.Check(resp.Header.Get("Content-Type"), gc.Equals, "application/x-tar-gz")
   213  	c.Check(resp.Header.Get("Content-Length"), gc.Equals, fmt.Sprintf("%v", len(testImageData)))
   214  
   215  	defer resp.Body.Close()
   216  	data, err := ioutil.ReadAll(resp.Body)
   217  	c.Assert(err, gc.IsNil)
   218  
   219  	c.Assert(data, gc.HasLen, len(testImageData))
   220  
   221  	hash := sha256.New()
   222  	hash.Write(data)
   223  	c.Assert(fmt.Sprintf("%x", hash.Sum(nil)), gc.Equals, testImageChecksum)
   224  	return data
   225  }
   226  
   227  func (s *imageSuite) downloadRequest(c *gc.C, url *url.URL) *http.Response {
   228  	return s.sendRequest(c, httpRequestParams{method: "GET", url: url.String()})
   229  }
   230  
   231  func (s *imageSuite) storeFakeImage(c *gc.C, st *state.State, kind, series, arch string) {
   232  	storage := st.ImageStorage()
   233  	metadata := &imagestorage.Metadata{
   234  		ModelUUID: st.ModelUUID(),
   235  		Kind:      kind,
   236  		Series:    series,
   237  		Arch:      arch,
   238  		Size:      int64(len(testImageData)),
   239  		SHA256:    testImageChecksum,
   240  		SourceURL: "http://path",
   241  	}
   242  	err := storage.AddImage(strings.NewReader(testImageData), metadata)
   243  	c.Assert(err, gc.IsNil)
   244  }
   245  
   246  func (s *imageSuite) getImageFromStorage(c *gc.C, st *state.State, kind, series, arch string) (*imagestorage.Metadata, []byte) {
   247  	storage := st.ImageStorage()
   248  	metadata, r, err := storage.Image(kind, series, arch)
   249  	c.Assert(err, gc.IsNil)
   250  	data, err := ioutil.ReadAll(r)
   251  	r.Close()
   252  	c.Assert(err, gc.IsNil)
   253  	return metadata, data
   254  }
   255  
   256  func (s *imageSuite) imageURL(c *gc.C, kind, series, arch string) *url.URL {
   257  	uri := s.baseURL(c)
   258  	uri.Path = fmt.Sprintf("/model/%s/images/%s/%s/%s/trusty-released-amd64-root.tar.gz", s.modelUUID, kind, series, arch)
   259  	return uri
   260  }
   261  
   262  func (s *imageSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
   263  	body := assertResponse(c, resp, expCode, "application/json")
   264  	err := jsonImageResponse(c, body).Error
   265  	c.Assert(err, gc.NotNil)
   266  	c.Assert(err, gc.ErrorMatches, expError)
   267  }
   268  
   269  func jsonImageResponse(c *gc.C, body []byte) (jsonResponse params.ErrorResult) {
   270  	err := json.Unmarshal(body, &jsonResponse)
   271  	c.Assert(err, gc.IsNil)
   272  	return
   273  }