1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package ocitest
20
21 import (
22 "bytes"
23 "context"
24 "encoding/json"
25 "fmt"
26 "io"
27 "sort"
28 "strings"
29 "testing"
30
31 "github.com/go-quicktest/qt"
32 "github.com/opencontainers/go-digest"
33
34 "cuelabs.dev/go/oci/ociregistry"
35 )
36
37 type Registry struct {
38 T *testing.T
39 R ociregistry.Interface
40 }
41
42
43
44
45
46
47 func NewRegistry(t *testing.T, r ociregistry.Interface) Registry {
48 return Registry{t, r}
49 }
50
51
52
53 type RegistryContent map[string]RepoContent
54
55
56
57
58
59
60
61
62
63
64 type RepoContent struct {
65
66
67 Manifests map[string]ociregistry.Manifest
68
69
70 Blobs map[string]string
71
72
73 Tags map[string]string
74 }
75
76
77
78
79 type PushedRepoContent struct {
80
81
82 Manifests map[string]ociregistry.Descriptor
83
84
85 ManifestData map[string][]byte
86
87
88
89 Blobs map[string]ociregistry.Descriptor
90 }
91
92
93
94
95
96 func PushContent(r ociregistry.Interface, rc RegistryContent) (map[string]PushedRepoContent, error) {
97 regContent := make(map[string]PushedRepoContent)
98 for repo, repoc := range rc {
99 prc, err := PushRepoContent(r, repo, repoc)
100 if err != nil {
101 return nil, fmt.Errorf("cannot push content for repository %q: %v", repo, err)
102 }
103 regContent[repo] = prc
104 }
105 return regContent, nil
106 }
107
108
109 func PushRepoContent(r ociregistry.Interface, repo string, repoc RepoContent) (PushedRepoContent, error) {
110 ctx := context.Background()
111 prc := PushedRepoContent{
112 Manifests: make(map[string]ociregistry.Descriptor),
113 ManifestData: make(map[string][]byte),
114 Blobs: make(map[string]ociregistry.Descriptor),
115 }
116
117 for id, blob := range repoc.Blobs {
118 prc.Blobs[id] = ociregistry.Descriptor{
119 Digest: digest.FromString(blob),
120 Size: int64(len(blob)),
121 MediaType: "application/binary",
122 }
123 }
124 manifests, manifestSeq, err := completedManifests(repoc, prc.Blobs)
125 if err != nil {
126 return PushedRepoContent{}, err
127 }
128 for id, content := range manifests {
129 prc.Manifests[id] = content.desc
130 prc.ManifestData[id] = content.data
131 }
132
133 for id, content := range repoc.Blobs {
134 _, err := r.PushBlob(ctx, repo, prc.Blobs[id], strings.NewReader(content))
135 if err != nil {
136 return PushedRepoContent{}, fmt.Errorf("cannot push blob %q in repo %q: %v", id, repo, err)
137 }
138 }
139
140 for _, mc := range manifestSeq {
141 _, err := r.PushManifest(ctx, repo, "", mc.data, mc.desc.MediaType)
142 if err != nil {
143 return PushedRepoContent{}, fmt.Errorf("cannot push manifest %q in repo %q: %v", mc.id, repo, err)
144 }
145 }
146
147 for tag, id := range repoc.Tags {
148 mc, ok := manifests[id]
149 if !ok {
150 return PushedRepoContent{}, fmt.Errorf("tag %q refers to unknown manifest id %q", tag, id)
151 }
152 _, err := r.PushManifest(ctx, repo, tag, mc.data, mc.desc.MediaType)
153 if err != nil {
154 return PushedRepoContent{}, fmt.Errorf("cannot push tag %q in repo %q: %v", id, repo, err)
155 }
156 }
157 return prc, nil
158 }
159
160
161
162
163
164 func (r Registry) MustPushContent(rc RegistryContent) map[string]PushedRepoContent {
165 prc, err := PushContent(r.R, rc)
166 qt.Assert(r.T, qt.IsNil(err))
167 return prc
168 }
169
170 type manifestContent struct {
171 id string
172 data []byte
173 desc ociregistry.Descriptor
174 }
175
176
177
178
179 func completedManifests(repoc RepoContent, blobs map[string]ociregistry.Descriptor) (map[string]manifestContent, []manifestContent, error) {
180 manifests := make(map[string]manifestContent)
181 manifestSeq := make([]manifestContent, 0, len(repoc.Manifests))
182
183
184
185 required := make(map[string]bool)
186 for {
187 madeProgress := false
188 needMore := false
189 need := func(digest ociregistry.Digest) {
190 needMore = true
191 if !required[string(digest)] {
192 required[string(digest)] = true
193 madeProgress = true
194 }
195 }
196 for id, m := range repoc.Manifests {
197 if _, ok := manifests[id]; ok {
198 continue
199 }
200 m1 := m
201 if m1.Subject != nil {
202 mc, ok := manifests[string(m1.Subject.Digest)]
203 if !ok {
204 need(m1.Subject.Digest)
205 continue
206 }
207 m1.Subject = ref(*m1.Subject)
208 *m1.Subject = mc.desc
209 madeProgress = true
210 }
211 m1.Config = fillBlobDescriptor(m.Config, blobs)
212 m1.Layers = make([]ociregistry.Descriptor, len(m.Layers))
213 for i, desc := range m.Layers {
214 m1.Layers[i] = fillBlobDescriptor(desc, blobs)
215 }
216 data, err := json.Marshal(m1)
217 if err != nil {
218 panic(err)
219 }
220 mc := manifestContent{
221 id: id,
222 data: data,
223 desc: ociregistry.Descriptor{
224 Digest: digest.FromBytes(data),
225 Size: int64(len(data)),
226 MediaType: m.MediaType,
227 },
228 }
229 manifests[id] = mc
230 madeProgress = true
231 manifestSeq = append(manifestSeq, mc)
232 }
233 if !needMore {
234 return manifests, manifestSeq, nil
235 }
236 if !madeProgress {
237 for m := range required {
238 if _, ok := manifests[m]; ok {
239 delete(required, m)
240 }
241 }
242 return nil, nil, fmt.Errorf("no manifest found for ids %s", strings.Join(mapKeys(required), ", "))
243 }
244 }
245 }
246
247 func fillManifestDescriptors(m ociregistry.Manifest, blobs map[string]ociregistry.Descriptor) ociregistry.Manifest {
248 m.Config = fillBlobDescriptor(m.Config, blobs)
249 m.Layers = append([]ociregistry.Descriptor(nil), m.Layers...)
250 for i, desc := range m.Layers {
251 m.Layers[i] = fillBlobDescriptor(desc, blobs)
252 }
253 return m
254 }
255
256 func fillBlobDescriptor(d ociregistry.Descriptor, blobs map[string]ociregistry.Descriptor) ociregistry.Descriptor {
257 blobDesc, ok := blobs[string(d.Digest)]
258 if !ok {
259 panic(fmt.Errorf("no blob found with id %q", d.Digest))
260 }
261 d.Digest = blobDesc.Digest
262 d.Size = blobDesc.Size
263 if d.MediaType == "" {
264 d.MediaType = blobDesc.MediaType
265 }
266 return d
267 }
268
269 func (r Registry) MustPushBlob(repo string, data []byte) ociregistry.Descriptor {
270 desc := ociregistry.Descriptor{
271 Digest: digest.FromBytes(data),
272 Size: int64(len(data)),
273 MediaType: "application/octet-stream",
274 }
275 desc1, err := r.R.PushBlob(context.Background(), repo, desc, bytes.NewReader(data))
276 qt.Assert(r.T, qt.IsNil(err))
277 return desc1
278 }
279
280 func (r Registry) MustPushManifest(repo string, jsonObject any, tag string) ([]byte, ociregistry.Descriptor) {
281 data, err := json.Marshal(jsonObject)
282 qt.Assert(r.T, qt.IsNil(err))
283 var mt struct {
284 MediaType string `json:"mediaType,omitempty"`
285 }
286 err = json.Unmarshal(data, &mt)
287 qt.Assert(r.T, qt.IsNil(err))
288 qt.Assert(r.T, qt.Not(qt.Equals(mt.MediaType, "")))
289 desc := ociregistry.Descriptor{
290 Digest: digest.FromBytes(data),
291 Size: int64(len(data)),
292 MediaType: mt.MediaType,
293 }
294 desc1, err := r.R.PushManifest(context.Background(), repo, tag, data, mt.MediaType)
295 qt.Assert(r.T, qt.IsNil(err))
296 qt.Check(r.T, qt.Equals(desc1.Digest, desc.Digest))
297 qt.Check(r.T, qt.Equals(desc1.Size, desc.Size))
298 qt.Check(r.T, qt.Equals(desc1.MediaType, desc.MediaType))
299 return data, desc1
300 }
301
302 type Repo struct {
303 T *testing.T
304 Name string
305 R ociregistry.Interface
306 }
307
308
309
310
311 func HasContent(r ociregistry.BlobReader, wantData []byte, wantMediaType string) qt.Checker {
312 if wantMediaType == "" {
313 wantMediaType = "application/octet-stream"
314 }
315 return contentChecker{
316 r: r,
317 wantData: wantData,
318 wantMediaType: wantMediaType,
319 }
320 }
321
322 type contentChecker struct {
323 r ociregistry.BlobReader
324 wantData []byte
325 wantMediaType string
326 }
327
328 func (c contentChecker) Args() []qt.Arg {
329 return []qt.Arg{{
330 Name: "reader",
331 Value: c.r,
332 }, {
333 Name: "data",
334 Value: c.wantData,
335 }, {
336 Name: "mediaType",
337 Value: c.wantMediaType,
338 }}
339 }
340
341 func (c contentChecker) Check(note func(key string, value any)) error {
342 desc := c.r.Descriptor()
343 gotData, err := io.ReadAll(c.r)
344 if err != nil {
345 return qt.BadCheckf("error reading data: %v", err)
346 }
347 if got, want := desc.Size, int64(len(c.wantData)); got != want {
348 note("actual data", gotData)
349 return fmt.Errorf("mismatched content length (got %d want %d)", got, want)
350 }
351 if got, want := desc.Digest, digest.FromBytes(c.wantData); got != want {
352 note("actual data", gotData)
353 return fmt.Errorf("mismatched digest (got %v want %v)", got, want)
354 }
355 if !bytes.Equal(gotData, c.wantData) {
356 note("actual data", gotData)
357 return fmt.Errorf("mismatched content")
358 }
359 if got, want := desc.MediaType, c.wantMediaType; got != want {
360 note("actual media type", desc.MediaType)
361 return fmt.Errorf("media type mismatch")
362 }
363 return nil
364 }
365
366 func ref[T any](x T) *T {
367 return &x
368 }
369
370 func mapKeys[V any](m map[string]V) []string {
371 keys := make([]string, 0, len(m))
372 for k := range m {
373 keys = append(keys, k)
374 }
375 sort.Strings(keys)
376 return keys
377 }
378
View as plain text