buildstep.PushBlob uploads by digest unless the store holds it, waits out a store refusing connections as mesh-build-image does, and names the store's own words; BlobReference is the form the key and the controller check.
152 lines
5.4 KiB
Go
152 lines
5.4 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
func digestOfBytes(b []byte) string {
|
|
h := sha256.Sum256(b)
|
|
return "sha256:" + hex.EncodeToString(h[:])
|
|
}
|
|
|
|
// registry is an artifact store as far as a blob push needs one: HEAD a blob, start an upload, PUT it with its digest.
|
|
type registry struct {
|
|
mu sync.Mutex
|
|
blobs map[string][]byte
|
|
calls []string
|
|
put int // the status a PUT answers, 201 when 0
|
|
}
|
|
|
|
func (r *registry) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.calls = append(r.calls, req.Method+" "+req.URL.Path)
|
|
switch {
|
|
case req.Method == http.MethodHead && strings.Contains(req.URL.Path, "/blobs/sha256:"):
|
|
if _, ok := r.blobs[req.URL.Path[strings.LastIndex(req.URL.Path, "/")+1:]]; ok {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
case req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/blobs/uploads/"):
|
|
w.Header().Set("Location", strings.TrimSuffix(req.URL.Path, "/")+"/u1?state=x")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
case req.Method == http.MethodPut && strings.Contains(req.URL.Path, "/blobs/uploads/u1"):
|
|
if r.put != 0 {
|
|
w.WriteHeader(r.put)
|
|
_, _ = io.WriteString(w, `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest did not match uploaded content"}]}`)
|
|
return
|
|
}
|
|
body, _ := io.ReadAll(req.Body)
|
|
d := req.URL.Query().Get("digest")
|
|
if req.URL.Query().Get("state") != "x" || d != digestOfBytes(body) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
r.blobs[d] = body
|
|
w.WriteHeader(http.StatusCreated)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// A done build's bundle or archive is pushed to the artifact store by its build module (novox/hq ADR 0498 clauses 1
|
|
// and 2): a blob the store already holds is done with no upload; one it lacks is uploaded by digest; its reference is
|
|
// the artifact-store form the builder's key and the controller check.
|
|
func TestABlobIsPushedOnceByItsDigest(t *testing.T) {
|
|
reg := ®istry{blobs: map[string][]byte{}}
|
|
srv := httptest.NewServer(reg)
|
|
defer srv.Close()
|
|
addr := strings.TrimPrefix(srv.URL, "http://")
|
|
body := []byte("the packed bundle")
|
|
d := digestOfBytes(body)
|
|
if err := PushBlob(context.Background(), srv.Client(), addr, "mail/code", d, body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(reg.blobs[d]) != string(body) {
|
|
t.Fatalf("the store holds %q", reg.blobs[d])
|
|
}
|
|
reg.calls = nil
|
|
if err := PushBlob(context.Background(), srv.Client(), addr, "mail/code", d, body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(reg.calls) != 1 || !strings.HasPrefix(reg.calls[0], "HEAD ") {
|
|
t.Fatalf("a blob the store holds was uploaded again: %v", reg.calls)
|
|
}
|
|
ref := BlobReference("mail", "code", d)
|
|
if ref != "artifact-store://mail/code/blobs/"+d {
|
|
t.Fatalf("BlobReference = %q", ref)
|
|
}
|
|
b := delivery.BuilderBuild{State: delivery.BuildDone, Manifest: strings.Repeat("4", 64),
|
|
Artifacts: []delivery.BuiltArtifact{{Name: "code", Kind: "bundle", Digest: d, Reference: ref}}}
|
|
if _, err := delivery.EncodeBuilderBuild(b); err != nil {
|
|
t.Fatalf("the key refuses the reference BlobReference gives: %v", err)
|
|
}
|
|
}
|
|
|
|
// A store that refuses the upload fails the push naming the store's own words; bytes that are not the digest are
|
|
// refused before anything is sent.
|
|
func TestAPushTheStoreRefusesSaysTheStoresWords(t *testing.T) {
|
|
reg := ®istry{blobs: map[string][]byte{}, put: http.StatusBadRequest}
|
|
srv := httptest.NewServer(reg)
|
|
defer srv.Close()
|
|
addr := strings.TrimPrefix(srv.URL, "http://")
|
|
body := []byte("x")
|
|
err := PushBlob(context.Background(), srv.Client(), addr, "mail/code", digestOfBytes(body), body)
|
|
if err == nil || !strings.Contains(err.Error(), "provided digest did not match") || !strings.Contains(err.Error(), "400") {
|
|
t.Fatalf("a refused upload said %v", err)
|
|
}
|
|
reg.calls = nil
|
|
err = PushBlob(context.Background(), srv.Client(), addr, "mail/code", digestOfBytes([]byte("other")), body)
|
|
if err == nil || len(reg.calls) != 0 {
|
|
t.Fatalf("bytes that are not their digest: %v, calls %v", err, reg.calls)
|
|
}
|
|
}
|
|
|
|
// A store refusing connections (its nightly collection holds it still) is waited out, up to RegistryWait (issue 457).
|
|
func TestAPushWaitsOutAStoreRefusingConnections(t *testing.T) {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
addr := l.Addr().String()
|
|
l.Close() // refusing now
|
|
reg := ®istry{blobs: map[string][]byte{}}
|
|
go func() {
|
|
time.Sleep(300 * time.Millisecond)
|
|
l2, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = http.Serve(l2, reg)
|
|
}()
|
|
beforeWait, beforePause := RegistryWait, RegistryFirstPause
|
|
RegistryWait, RegistryFirstPause = 3*time.Second, 100*time.Millisecond
|
|
defer func() { RegistryWait, RegistryFirstPause = beforeWait, beforePause }()
|
|
body := []byte("late")
|
|
if err := PushBlob(context.Background(), http.DefaultClient, addr, "mail/code", digestOfBytes(body), body); err != nil {
|
|
t.Fatalf("a store that came back was not waited for: %v", err)
|
|
}
|
|
|
|
l3, _ := net.Listen("tcp", "127.0.0.1:0")
|
|
gone := l3.Addr().String()
|
|
l3.Close()
|
|
RegistryWait = 300 * time.Millisecond
|
|
err = PushBlob(context.Background(), http.DefaultClient, gone, "mail/code", digestOfBytes(body), body)
|
|
if err == nil || !strings.Contains(err.Error(), "refused every connection") {
|
|
t.Fatalf("a store that stayed down said %v", err)
|
|
}
|
|
}
|