1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package registry
16
17 import (
18 "bytes"
19 "encoding/json"
20 "fmt"
21 "io"
22 "log"
23 "net/http"
24 "sort"
25 "strconv"
26 "strings"
27 "sync"
28
29 v1 "github.com/google/go-containerregistry/pkg/v1"
30 "github.com/google/go-containerregistry/pkg/v1/types"
31 )
32
33 type catalog struct {
34 Repos []string `json:"repositories"`
35 }
36
37 type listTags struct {
38 Name string `json:"name"`
39 Tags []string `json:"tags"`
40 }
41
42 type manifest struct {
43 contentType string
44 blob []byte
45 }
46
47 type manifests struct {
48
49 manifests map[string]map[string]manifest
50 lock sync.RWMutex
51 log *log.Logger
52 }
53
54 func isManifest(req *http.Request) bool {
55 elems := strings.Split(req.URL.Path, "/")
56 elems = elems[1:]
57 if len(elems) < 4 {
58 return false
59 }
60 return elems[len(elems)-2] == "manifests"
61 }
62
63 func isTags(req *http.Request) bool {
64 elems := strings.Split(req.URL.Path, "/")
65 elems = elems[1:]
66 if len(elems) < 4 {
67 return false
68 }
69 return elems[len(elems)-2] == "tags"
70 }
71
72 func isCatalog(req *http.Request) bool {
73 elems := strings.Split(req.URL.Path, "/")
74 elems = elems[1:]
75 if len(elems) < 2 {
76 return false
77 }
78
79 return elems[len(elems)-1] == "_catalog"
80 }
81
82
83 func isReferrers(req *http.Request) bool {
84 elems := strings.Split(req.URL.Path, "/")
85 elems = elems[1:]
86 if len(elems) < 4 {
87 return false
88 }
89 return elems[len(elems)-2] == "referrers"
90 }
91
92
93
94 func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
95 elem := strings.Split(req.URL.Path, "/")
96 elem = elem[1:]
97 target := elem[len(elem)-1]
98 repo := strings.Join(elem[1:len(elem)-2], "/")
99
100 switch req.Method {
101 case http.MethodGet:
102 m.lock.RLock()
103 defer m.lock.RUnlock()
104
105 c, ok := m.manifests[repo]
106 if !ok {
107 return ®Error{
108 Status: http.StatusNotFound,
109 Code: "NAME_UNKNOWN",
110 Message: "Unknown name",
111 }
112 }
113 m, ok := c[target]
114 if !ok {
115 return ®Error{
116 Status: http.StatusNotFound,
117 Code: "MANIFEST_UNKNOWN",
118 Message: "Unknown manifest",
119 }
120 }
121
122 h, _, _ := v1.SHA256(bytes.NewReader(m.blob))
123 resp.Header().Set("Docker-Content-Digest", h.String())
124 resp.Header().Set("Content-Type", m.contentType)
125 resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
126 resp.WriteHeader(http.StatusOK)
127 io.Copy(resp, bytes.NewReader(m.blob))
128 return nil
129
130 case http.MethodHead:
131 m.lock.RLock()
132 defer m.lock.RUnlock()
133
134 if _, ok := m.manifests[repo]; !ok {
135 return ®Error{
136 Status: http.StatusNotFound,
137 Code: "NAME_UNKNOWN",
138 Message: "Unknown name",
139 }
140 }
141 m, ok := m.manifests[repo][target]
142 if !ok {
143 return ®Error{
144 Status: http.StatusNotFound,
145 Code: "MANIFEST_UNKNOWN",
146 Message: "Unknown manifest",
147 }
148 }
149
150 h, _, _ := v1.SHA256(bytes.NewReader(m.blob))
151 resp.Header().Set("Docker-Content-Digest", h.String())
152 resp.Header().Set("Content-Type", m.contentType)
153 resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
154 resp.WriteHeader(http.StatusOK)
155 return nil
156
157 case http.MethodPut:
158 b := &bytes.Buffer{}
159 io.Copy(b, req.Body)
160 h, _, _ := v1.SHA256(bytes.NewReader(b.Bytes()))
161 digest := h.String()
162 mf := manifest{
163 blob: b.Bytes(),
164 contentType: req.Header.Get("Content-Type"),
165 }
166
167
168
169
170
171 if types.MediaType(mf.contentType).IsIndex() {
172 if err := func() *regError {
173 m.lock.RLock()
174 defer m.lock.RUnlock()
175
176 im, err := v1.ParseIndexManifest(b)
177 if err != nil {
178 return ®Error{
179 Status: http.StatusBadRequest,
180 Code: "MANIFEST_INVALID",
181 Message: err.Error(),
182 }
183 }
184 for _, desc := range im.Manifests {
185 if !desc.MediaType.IsDistributable() {
186 continue
187 }
188 if desc.MediaType.IsIndex() || desc.MediaType.IsImage() {
189 if _, found := m.manifests[repo][desc.Digest.String()]; !found {
190 return ®Error{
191 Status: http.StatusNotFound,
192 Code: "MANIFEST_UNKNOWN",
193 Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest),
194 }
195 }
196 } else {
197
198 m.log.Printf("TODO: Check blobs for %q", desc.Digest)
199 }
200 }
201 return nil
202 }(); err != nil {
203 return err
204 }
205 }
206
207 m.lock.Lock()
208 defer m.lock.Unlock()
209
210 if _, ok := m.manifests[repo]; !ok {
211 m.manifests[repo] = make(map[string]manifest, 2)
212 }
213
214
215
216 m.manifests[repo][digest] = mf
217 m.manifests[repo][target] = mf
218 resp.Header().Set("Docker-Content-Digest", digest)
219 resp.WriteHeader(http.StatusCreated)
220 return nil
221
222 case http.MethodDelete:
223 m.lock.Lock()
224 defer m.lock.Unlock()
225 if _, ok := m.manifests[repo]; !ok {
226 return ®Error{
227 Status: http.StatusNotFound,
228 Code: "NAME_UNKNOWN",
229 Message: "Unknown name",
230 }
231 }
232
233 _, ok := m.manifests[repo][target]
234 if !ok {
235 return ®Error{
236 Status: http.StatusNotFound,
237 Code: "MANIFEST_UNKNOWN",
238 Message: "Unknown manifest",
239 }
240 }
241
242 delete(m.manifests[repo], target)
243 resp.WriteHeader(http.StatusAccepted)
244 return nil
245
246 default:
247 return ®Error{
248 Status: http.StatusBadRequest,
249 Code: "METHOD_UNKNOWN",
250 Message: "We don't understand your method + url",
251 }
252 }
253 }
254
255 func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *regError {
256 elem := strings.Split(req.URL.Path, "/")
257 elem = elem[1:]
258 repo := strings.Join(elem[1:len(elem)-2], "/")
259
260 if req.Method == "GET" {
261 m.lock.RLock()
262 defer m.lock.RUnlock()
263
264 c, ok := m.manifests[repo]
265 if !ok {
266 return ®Error{
267 Status: http.StatusNotFound,
268 Code: "NAME_UNKNOWN",
269 Message: "Unknown name",
270 }
271 }
272
273 var tags []string
274 for tag := range c {
275 if !strings.Contains(tag, "sha256:") {
276 tags = append(tags, tag)
277 }
278 }
279 sort.Strings(tags)
280
281
282
283 if last := req.URL.Query().Get("last"); last != "" {
284 for i, t := range tags {
285 if t > last {
286 tags = tags[i:]
287 break
288 }
289 }
290 }
291
292
293 if ns := req.URL.Query().Get("n"); ns != "" {
294 if n, err := strconv.Atoi(ns); err != nil {
295 return ®Error{
296 Status: http.StatusBadRequest,
297 Code: "BAD_REQUEST",
298 Message: fmt.Sprintf("parsing n: %v", err),
299 }
300 } else if n < len(tags) {
301 tags = tags[:n]
302 }
303 }
304
305 tagsToList := listTags{
306 Name: repo,
307 Tags: tags,
308 }
309
310 msg, _ := json.Marshal(tagsToList)
311 resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
312 resp.WriteHeader(http.StatusOK)
313 io.Copy(resp, bytes.NewReader([]byte(msg)))
314 return nil
315 }
316
317 return ®Error{
318 Status: http.StatusBadRequest,
319 Code: "METHOD_UNKNOWN",
320 Message: "We don't understand your method + url",
321 }
322 }
323
324 func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError {
325 query := req.URL.Query()
326 nStr := query.Get("n")
327 n := 10000
328 if nStr != "" {
329 n, _ = strconv.Atoi(nStr)
330 }
331
332 if req.Method == "GET" {
333 m.lock.RLock()
334 defer m.lock.RUnlock()
335
336 var repos []string
337 countRepos := 0
338
339 for key := range m.manifests {
340 if countRepos >= n {
341 break
342 }
343 countRepos++
344
345 repos = append(repos, key)
346 }
347
348 repositoriesToList := catalog{
349 Repos: repos,
350 }
351
352 msg, _ := json.Marshal(repositoriesToList)
353 resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
354 resp.WriteHeader(http.StatusOK)
355 io.Copy(resp, bytes.NewReader([]byte(msg)))
356 return nil
357 }
358
359 return ®Error{
360 Status: http.StatusBadRequest,
361 Code: "METHOD_UNKNOWN",
362 Message: "We don't understand your method + url",
363 }
364 }
365
366
367 func (m *manifests) handleReferrers(resp http.ResponseWriter, req *http.Request) *regError {
368
369 if req.Method != "GET" {
370 return ®Error{
371 Status: http.StatusBadRequest,
372 Code: "METHOD_UNKNOWN",
373 Message: "We don't understand your method + url",
374 }
375 }
376
377 elem := strings.Split(req.URL.Path, "/")
378 elem = elem[1:]
379 target := elem[len(elem)-1]
380 repo := strings.Join(elem[1:len(elem)-2], "/")
381
382
383 if _, err := v1.NewHash(target); err != nil {
384 return ®Error{
385 Status: http.StatusBadRequest,
386 Code: "UNSUPPORTED",
387 Message: "Target must be a valid digest",
388 }
389 }
390
391 m.lock.RLock()
392 defer m.lock.RUnlock()
393
394 digestToManifestMap, repoExists := m.manifests[repo]
395 if !repoExists {
396 return ®Error{
397 Status: http.StatusNotFound,
398 Code: "NAME_UNKNOWN",
399 Message: "Unknown name",
400 }
401 }
402
403 im := v1.IndexManifest{
404 SchemaVersion: 2,
405 MediaType: types.OCIImageIndex,
406 Manifests: []v1.Descriptor{},
407 }
408 for digest, manifest := range digestToManifestMap {
409 h, err := v1.NewHash(digest)
410 if err != nil {
411 continue
412 }
413 var refPointer struct {
414 Subject *v1.Descriptor `json:"subject"`
415 }
416 json.Unmarshal(manifest.blob, &refPointer)
417 if refPointer.Subject == nil {
418 continue
419 }
420 referenceDigest := refPointer.Subject.Digest
421 if referenceDigest.String() != target {
422 continue
423 }
424
425 var imageAsArtifact struct {
426 Config struct {
427 MediaType string `json:"mediaType"`
428 } `json:"config"`
429 }
430 json.Unmarshal(manifest.blob, &imageAsArtifact)
431 im.Manifests = append(im.Manifests, v1.Descriptor{
432 MediaType: types.MediaType(manifest.contentType),
433 Size: int64(len(manifest.blob)),
434 Digest: h,
435 ArtifactType: imageAsArtifact.Config.MediaType,
436 })
437 }
438 msg, _ := json.Marshal(&im)
439 resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
440 resp.Header().Set("Content-Type", string(types.OCIImageIndex))
441 resp.WriteHeader(http.StatusOK)
442 io.Copy(resp, bytes.NewReader([]byte(msg)))
443 return nil
444 }
445
View as plain text