blob: 622a3ab3528ef92ec58d765855b6345f251c1171 [file] [log] [blame]
Jan Schär56d12992025-04-14 11:49:37 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package registry
5
6import (
7 "context"
8 "fmt"
9 "net"
10 "net/http"
11 "strings"
12 "sync"
13 "testing"
14 "time"
15
16 "github.com/bazelbuild/rules_go/go/runfiles"
17 "github.com/cenkalti/backoff/v4"
18
19 "source.monogon.dev/osbase/oci"
20)
21
22var (
23 // These are filled by bazel at linking time with the canonical path of
24 // their corresponding file. Inside the init function we resolve it
25 // with the rules_go runfiles package to the real path.
26 xImagePath string
27)
28
29func init() {
30 var err error
31 for _, path := range []*string{
32 &xImagePath,
33 } {
34 *path, err = runfiles.Rlocation(*path)
35 if err != nil {
36 panic(err)
37 }
38 }
39}
40
41func TestRetries(t *testing.T) {
Jan Schär2963b682025-07-17 17:03:44 +020042 srcImage, err := oci.AsImage(oci.ReadLayout(xImagePath))
Jan Schär56d12992025-04-14 11:49:37 +000043 if err != nil {
44 t.Fatal(err)
45 }
46 server := NewServer()
Jan Schär2963b682025-07-17 17:03:44 +020047 server.AddRef("test/repo", "test-tag", srcImage)
Jan Schär56d12992025-04-14 11:49:37 +000048 wrapper := &unreliableServer{
49 handler: server,
50 blobLimit: srcImage.Manifest.Config.Size / 2,
51 seen: make(map[string]bool),
52 }
53
54 listener, err := net.Listen("tcp", "127.0.0.1:0")
55 if err != nil {
56 t.Fatal(err)
57 }
58 defer listener.Close()
59 go http.Serve(listener, wrapper)
60 wrapper.host = listener.Addr().String()
61
62 client := &Client{
63 GetBackOff: func() backoff.BackOff {
64 return backoff.NewExponentialBackOff(backoff.WithInitialInterval(time.Millisecond))
65 },
66 RetryNotify: func(err error, d time.Duration) {
67 fmt.Printf("Retrying in %v: %v\n", d, err)
68 },
69 Scheme: "http",
70 Host: listener.Addr().String(),
71 Repository: "test/repo",
72 }
73
Jan Schär2963b682025-07-17 17:03:44 +020074 image, err := oci.AsImage(client.Read(context.Background(), "test-tag", srcImage.Digest()))
Jan Schär56d12992025-04-14 11:49:37 +000075 if err != nil {
76 t.Fatal(err)
77 }
78 _, err = image.ReadBlobVerified(&image.Manifest.Config)
79 if err != nil {
80 t.Error(err)
81 }
82}
83
84type unreliableServer struct {
85 handler http.Handler
86 host string
87 blobLimit int64
88 mu sync.Mutex
89 seen map[string]bool
90}
91
92func (s *unreliableServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
93 fmt.Printf("%s %s %s\n", req.Method, req.URL.String(), req.Header.Get("Range"))
94
95 // Every path returns a temporary error the first time it is hit. This
96 // includes the redirected and token paths.
97 s.mu.Lock()
98 if !s.seen[req.URL.Path] {
99 s.seen[req.URL.Path] = true
100 s.mu.Unlock()
101 w.WriteHeader(http.StatusServiceUnavailable)
102 return
103 }
104 s.mu.Unlock()
105
106 // Every path is redirected.
107 var ok bool
108 req.URL.Path, ok = strings.CutPrefix(req.URL.Path, "/redirected")
109 if !ok {
110 req.URL.Path = "/redirected" + req.URL.Path
111 w.Header().Set("Location", req.URL.String())
112 w.WriteHeader(http.StatusTemporaryRedirect)
113 return
114 }
115
116 // Each request requires a token.
117 if req.URL.Path == "/token" {
118 query := req.URL.Query()
119 if query.Get("service") != "myregistry.test" || query.Get("scope") != "repository:test/repo:pull" {
120 w.WriteHeader(http.StatusBadRequest)
121 return
122 }
123 w.WriteHeader(http.StatusOK)
124 w.Write([]byte(`{"token": "the_token"}`))
125 return
126 } else if req.Header.Get("Authorization") != "Bearer the_token" {
127 w.Header().Set("Www-Authenticate", fmt.Sprintf(`Bearer realm="http://%s/token",service="myregistry.test",scope="repository:test/repo:pull"`, s.host))
128 w.WriteHeader(http.StatusUnauthorized)
129 return
130 }
131
132 // Blob requests fail after returning part of the response, requiring retries
133 // with Range header.
134 if strings.Contains(req.URL.Path, "/blobs/") {
135 w = &limitResponseWriter{ResponseWriter: w, remaining: s.blobLimit}
136 }
137
138 s.handler.ServeHTTP(w, req)
139}
140
141type limitResponseWriter struct {
142 http.ResponseWriter
143 remaining int64
144}
145
146func (w *limitResponseWriter) Write(b []byte) (n int, err error) {
147 if w.remaining <= 0 {
148 return 0, fmt.Errorf("limit reached")
149 }
150 if int64(len(b)) > w.remaining {
151 n, _ = w.ResponseWriter.Write(b[:w.remaining])
152 err = fmt.Errorf("limit reached")
153 w.remaining = 0
154 return
155 }
156 w.remaining -= int64(len(b))
157 return w.ResponseWriter.Write(b)
158}