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 }