24 "github.com/curoverse/azure-sdk-for-go/storage"
28 // The same fake credentials used by Microsoft's Azure emulator
29 emulatorAccountName = "devstoreaccount1"
30 emulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
33 var azureTestContainer string
38 "test.azure-storage-container-volume",
40 "Name of Azure container to use for testing. Do not use a container with real data! Use -azure-storage-account-name and -azure-storage-key-file arguments to supply credentials.")
46 Metadata map[string]string
48 Uncommitted map[string][]byte
51 type azStubHandler struct {
53 blobs map[string]*azBlob
54 race chan chan struct{}
57 func newAzStubHandler() *azStubHandler {
58 return &azStubHandler{
59 blobs: make(map[string]*azBlob),
63 func (h *azStubHandler) TouchWithDate(container, hash string, t time.Time) {
64 blob, ok := h.blobs[container+"|"+hash]
71 func (h *azStubHandler) PutRaw(container, hash string, data []byte) {
74 h.blobs[container+"|"+hash] = &azBlob{
77 Uncommitted: make(map[string][]byte),
81 func (h *azStubHandler) unlockAndRace() {
86 // Signal caller that race is starting by reading from
87 // h.race. If we get a channel, block until that channel is
88 // ready to receive. If we get nil (or h.race is closed) just
90 if c := <-h.race; c != nil {
96 var rangeRegexp = regexp.MustCompile(`^bytes=(\d+)-(\d+)$`)
98 func (h *azStubHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
101 // defer log.Printf("azStubHandler: %+v", r)
103 path := strings.Split(r.URL.Path, "/")
110 if err := r.ParseForm(); err != nil {
111 log.Printf("azStubHandler(%+v): %s", r, err)
112 rw.WriteHeader(http.StatusBadRequest)
116 body, err := ioutil.ReadAll(r.Body)
121 type blockListRequestBody struct {
122 XMLName xml.Name `xml:"BlockList"`
126 blob, blobExists := h.blobs[container+"|"+hash]
129 case r.Method == "PUT" && r.Form.Get("comp") == "":
131 if _, ok := h.blobs[container+"|"+hash]; !ok {
132 // Like the real Azure service, we offer a
133 // race window during which other clients can
134 // list/get the new blob before any data is
136 h.blobs[container+"|"+hash] = &azBlob{
138 Uncommitted: make(map[string][]byte),
143 h.blobs[container+"|"+hash] = &azBlob{
146 Uncommitted: make(map[string][]byte),
149 rw.WriteHeader(http.StatusCreated)
150 case r.Method == "PUT" && r.Form.Get("comp") == "block":
153 log.Printf("Got block for nonexistent blob: %+v", r)
154 rw.WriteHeader(http.StatusBadRequest)
157 blockID, err := base64.StdEncoding.DecodeString(r.Form.Get("blockid"))
158 if err != nil || len(blockID) == 0 {
159 log.Printf("Invalid blockid: %+q", r.Form.Get("blockid"))
160 rw.WriteHeader(http.StatusBadRequest)
163 blob.Uncommitted[string(blockID)] = body
164 rw.WriteHeader(http.StatusCreated)
165 case r.Method == "PUT" && r.Form.Get("comp") == "blocklist":
166 // "Put Block List" API
167 bl := &blockListRequestBody{}
168 if err := xml.Unmarshal(body, bl); err != nil {
169 log.Printf("xml Unmarshal: %s", err)
170 rw.WriteHeader(http.StatusBadRequest)
173 for _, encBlockID := range bl.Uncommitted {
174 blockID, err := base64.StdEncoding.DecodeString(encBlockID)
175 if err != nil || len(blockID) == 0 || blob.Uncommitted[string(blockID)] == nil {
176 log.Printf("Invalid blockid: %+q", encBlockID)
177 rw.WriteHeader(http.StatusBadRequest)
180 blob.Data = blob.Uncommitted[string(blockID)]
181 blob.Etag = makeEtag()
182 blob.Mtime = time.Now()
183 delete(blob.Uncommitted, string(blockID))
185 rw.WriteHeader(http.StatusCreated)
186 case r.Method == "PUT" && r.Form.Get("comp") == "metadata":
187 // "Set Metadata Headers" API. We don't bother
188 // stubbing "Get Metadata Headers": AzureBlobVolume
189 // sets metadata headers only as a way to bump Etag
190 // and Last-Modified.
192 log.Printf("Got metadata for nonexistent blob: %+v", r)
193 rw.WriteHeader(http.StatusBadRequest)
196 blob.Metadata = make(map[string]string)
197 for k, v := range r.Header {
198 if strings.HasPrefix(strings.ToLower(k), "x-ms-meta-") {
199 blob.Metadata[k] = v[0]
202 blob.Mtime = time.Now()
203 blob.Etag = makeEtag()
204 case (r.Method == "GET" || r.Method == "HEAD") && hash != "":
207 rw.WriteHeader(http.StatusNotFound)
211 if rangeSpec := rangeRegexp.FindStringSubmatch(r.Header.Get("Range")); rangeSpec != nil {
212 b0, err0 := strconv.Atoi(rangeSpec[1])
213 b1, err1 := strconv.Atoi(rangeSpec[2])
214 if err0 != nil || err1 != nil || b0 >= len(data) || b1 >= len(data) || b0 > b1 {
215 rw.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", len(data)))
216 rw.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
219 rw.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", b0, b1, len(data)))
220 rw.WriteHeader(http.StatusPartialContent)
221 data = data[b0 : b1+1]
223 rw.Header().Set("Last-Modified", blob.Mtime.Format(time.RFC1123))
224 rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
225 if r.Method == "GET" {
226 if _, err := rw.Write(data); err != nil {
227 log.Printf("write %+q: %s", data, err)
231 case r.Method == "DELETE" && hash != "":
234 rw.WriteHeader(http.StatusNotFound)
237 delete(h.blobs, container+"|"+hash)
238 rw.WriteHeader(http.StatusAccepted)
239 case r.Method == "GET" && r.Form.Get("comp") == "list" && r.Form.Get("restype") == "container":
241 prefix := container + "|" + r.Form.Get("prefix")
242 marker := r.Form.Get("marker")
245 if n, err := strconv.Atoi(r.Form.Get("maxresults")); err == nil && n >= 1 && n <= 5000 {
249 resp := storage.BlobListResponse{
252 MaxResults: int64(maxResults),
254 var hashes sort.StringSlice
255 for k := range h.blobs {
256 if strings.HasPrefix(k, prefix) {
257 hashes = append(hashes, k[len(container)+1:])
261 for _, hash := range hashes {
262 if len(resp.Blobs) == maxResults {
263 resp.NextMarker = hash
266 if len(resp.Blobs) > 0 || marker == "" || marker == hash {
267 blob := h.blobs[container+"|"+hash]
268 resp.Blobs = append(resp.Blobs, storage.Blob{
270 Properties: storage.BlobProperties{
271 LastModified: blob.Mtime.Format(time.RFC1123),
272 ContentLength: int64(len(blob.Data)),
278 buf, err := xml.Marshal(resp)
281 rw.WriteHeader(http.StatusInternalServerError)
285 log.Printf("azStubHandler: not implemented: %+v Body:%+q", r, body)
286 rw.WriteHeader(http.StatusNotImplemented)
290 // azStubDialer is a net.Dialer that notices when the Azure driver
291 // tries to connect to "devstoreaccount1.blob.127.0.0.1:46067", and
292 // in such cases transparently dials "127.0.0.1:46067" instead.
293 type azStubDialer struct {
297 var localHostPortRe = regexp.MustCompile(`(127\.0\.0\.1|localhost|\[::1\]):\d+`)
299 func (d *azStubDialer) Dial(network, address string) (net.Conn, error) {
300 if hp := localHostPortRe.FindString(address); hp != "" {
301 log.Println("azStubDialer: dial", hp, "instead of", address)
304 return d.Dialer.Dial(network, address)
307 type TestableAzureBlobVolume struct {
309 azHandler *azStubHandler
310 azStub *httptest.Server
314 func NewTestableAzureBlobVolume(t TB, readonly bool, replication int) *TestableAzureBlobVolume {
315 azHandler := newAzStubHandler()
316 azStub := httptest.NewServer(azHandler)
318 var azClient storage.Client
320 container := azureTestContainer
322 // Connect to stub instead of real Azure storage service
323 stubURLBase := strings.Split(azStub.URL, "://")[1]
325 if azClient, err = storage.NewClient(emulatorAccountName, emulatorAccountKey, stubURLBase, storage.DefaultAPIVersion, false); err != nil {
328 container = "fakecontainername"
330 // Connect to real Azure storage service
331 accountKey, err := readKeyFromFile(azureStorageAccountKeyFile)
335 azClient, err = storage.NewBasicClient(azureStorageAccountName, accountKey)
341 v := NewAzureBlobVolume(azClient, container, readonly, replication)
343 return &TestableAzureBlobVolume{
345 azHandler: azHandler,
351 func TestAzureBlobVolumeWithGeneric(t *testing.T) {
352 defer func(t http.RoundTripper) {
353 http.DefaultTransport = t
354 }(http.DefaultTransport)
355 http.DefaultTransport = &http.Transport{
356 Dial: (&azStubDialer{}).Dial,
358 azureWriteRaceInterval = time.Millisecond
359 azureWriteRacePollTime = time.Nanosecond
360 DoGenericVolumeTests(t, func(t TB) TestableVolume {
361 return NewTestableAzureBlobVolume(t, false, azureStorageReplication)
365 func TestAzureBlobVolumeConcurrentRanges(t *testing.T) {
370 defer func(t http.RoundTripper) {
371 http.DefaultTransport = t
372 }(http.DefaultTransport)
373 http.DefaultTransport = &http.Transport{
374 Dial: (&azStubDialer{}).Dial,
376 azureWriteRaceInterval = time.Millisecond
377 azureWriteRacePollTime = time.Nanosecond
378 // Test (BlockSize mod azureMaxGetBytes)==0 and !=0 cases
379 for _, azureMaxGetBytes = range []int{2 << 22, 2<<22 - 1} {
380 DoGenericVolumeTests(t, func(t TB) TestableVolume {
381 return NewTestableAzureBlobVolume(t, false, azureStorageReplication)
386 func TestReadonlyAzureBlobVolumeWithGeneric(t *testing.T) {
387 defer func(t http.RoundTripper) {
388 http.DefaultTransport = t
389 }(http.DefaultTransport)
390 http.DefaultTransport = &http.Transport{
391 Dial: (&azStubDialer{}).Dial,
393 azureWriteRaceInterval = time.Millisecond
394 azureWriteRacePollTime = time.Nanosecond
395 DoGenericVolumeTests(t, func(t TB) TestableVolume {
396 return NewTestableAzureBlobVolume(t, true, azureStorageReplication)
400 func TestAzureBlobVolumeRangeFenceposts(t *testing.T) {
401 defer func(t http.RoundTripper) {
402 http.DefaultTransport = t
403 }(http.DefaultTransport)
404 http.DefaultTransport = &http.Transport{
405 Dial: (&azStubDialer{}).Dial,
408 v := NewTestableAzureBlobVolume(t, false, 3)
411 for _, size := range []int{
412 2<<22 - 1, // one <max read
413 2 << 22, // one =max read
414 2<<22 + 1, // one =max read, one <max
415 2 << 23, // two =max reads
419 data := make([]byte, size)
420 for i := range data {
421 data[i] = byte((i + 7) & 0xff)
423 hash := fmt.Sprintf("%x", md5.Sum(data))
424 err := v.Put(hash, data)
428 gotData, err := v.Get(hash)
432 gotHash := fmt.Sprintf("%x", md5.Sum(gotData))
433 gotLen := len(gotData)
436 t.Error("length mismatch: got %d != %d", gotLen, size)
439 t.Error("hash mismatch: got %s != %s", gotHash, hash)
444 func TestAzureBlobVolumeReplication(t *testing.T) {
445 for r := 1; r <= 4; r++ {
446 v := NewTestableAzureBlobVolume(t, false, r)
448 if n := v.Replication(); n != r {
449 t.Errorf("Got replication %d, expected %d", n, r)
454 func TestAzureBlobVolumeCreateBlobRace(t *testing.T) {
455 defer func(t http.RoundTripper) {
456 http.DefaultTransport = t
457 }(http.DefaultTransport)
458 http.DefaultTransport = &http.Transport{
459 Dial: (&azStubDialer{}).Dial,
462 v := NewTestableAzureBlobVolume(t, false, 3)
465 azureWriteRaceInterval = time.Second
466 azureWriteRacePollTime = time.Millisecond
468 allDone := make(chan struct{})
469 v.azHandler.race = make(chan chan struct{})
471 err := v.Put(TestHash, TestBlock)
476 continuePut := make(chan struct{})
477 // Wait for the stub's Put to create the empty blob
478 v.azHandler.race <- continuePut
480 buf, err := v.Get(TestHash)
488 // Wait for the stub's Get to get the empty blob
489 close(v.azHandler.race)
490 // Allow stub's Put to continue, so the real data is ready
491 // when the volume's Get retries
493 // Wait for volume's Get to return the real data
497 func TestAzureBlobVolumeCreateBlobRaceDeadline(t *testing.T) {
498 defer func(t http.RoundTripper) {
499 http.DefaultTransport = t
500 }(http.DefaultTransport)
501 http.DefaultTransport = &http.Transport{
502 Dial: (&azStubDialer{}).Dial,
505 v := NewTestableAzureBlobVolume(t, false, 3)
508 azureWriteRaceInterval = 2 * time.Second
509 azureWriteRacePollTime = 5 * time.Millisecond
511 v.PutRaw(TestHash, nil)
513 buf := new(bytes.Buffer)
516 t.Errorf("Index %+q should be empty", buf.Bytes())
519 v.TouchWithDate(TestHash, time.Now().Add(-1982*time.Millisecond))
521 allDone := make(chan struct{})
524 buf, err := v.Get(TestHash)
530 t.Errorf("Got %+q, expected empty buf", buf)
536 case <-time.After(time.Second):
537 t.Error("Get should have stopped waiting for race when block was 2s old")
542 if !bytes.HasPrefix(buf.Bytes(), []byte(TestHash+"+0")) {
543 t.Errorf("Index %+q should have %+q", buf.Bytes(), TestHash+"+0")
547 func (v *TestableAzureBlobVolume) PutRaw(locator string, data []byte) {
548 v.azHandler.PutRaw(v.containerName, locator, data)
551 func (v *TestableAzureBlobVolume) TouchWithDate(locator string, lastPut time.Time) {
552 v.azHandler.TouchWithDate(v.containerName, locator, lastPut)
555 func (v *TestableAzureBlobVolume) Teardown() {
559 func makeEtag() string {
560 return fmt.Sprintf("0x%x", rand.Int63())