1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package modregistry
21
22 import (
23 "archive/zip"
24 "bytes"
25 "context"
26 "encoding/json"
27 "errors"
28 "fmt"
29 "io"
30 "strings"
31
32 "cuelabs.dev/go/oci/ociregistry"
33 "cuelang.org/go/internal/mod/semver"
34 digest "github.com/opencontainers/go-digest"
35 specs "github.com/opencontainers/image-spec/specs-go"
36 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
37
38 "cuelang.org/go/mod/modfile"
39 "cuelang.org/go/mod/module"
40 "cuelang.org/go/mod/modzip"
41 )
42
43 var ErrNotFound = fmt.Errorf("module not found")
44
45
46
47 type Client struct {
48 resolver Resolver
49 }
50
51
52
53 type Resolver interface {
54
55
56
57
58
59
60
61 ResolveToRegistry(mpath, vers string) (RegistryLocation, error)
62 }
63
64
65
66
67 type RegistryLocation struct {
68
69 Registry ociregistry.Interface
70
71 Repository string
72
73
74
75 Tag string
76 }
77
78 const (
79 moduleArtifactType = "application/vnd.cue.module.v1+json"
80 moduleFileMediaType = "application/vnd.cue.modulefile.v1"
81 moduleAnnotation = "works.cue.module"
82 )
83
84
85
86 func NewClient(registry ociregistry.Interface) *Client {
87 return &Client{
88 resolver: singleResolver{registry},
89 }
90 }
91
92
93
94 func NewClientWithResolver(resolver Resolver) *Client {
95 return &Client{
96 resolver: resolver,
97 }
98 }
99
100
101
102
103 func (c *Client) GetModule(ctx context.Context, m module.Version) (*Module, error) {
104 loc, err := c.resolve(m)
105 if err != nil {
106 return nil, err
107 }
108 rd, err := loc.Registry.GetTag(ctx, loc.Repository, loc.Tag)
109 if err != nil {
110 if errors.Is(err, ociregistry.ErrManifestUnknown) {
111 return nil, fmt.Errorf("module %v: %w", m, ErrNotFound)
112 }
113 return nil, fmt.Errorf("module %v: %v", m, err)
114 }
115 defer rd.Close()
116 data, err := io.ReadAll(rd)
117 if err != nil {
118 return nil, err
119 }
120
121 return c.GetModuleWithManifest(ctx, m, data, rd.Descriptor().MediaType)
122 }
123
124
125
126
127
128 func (c *Client) GetModuleWithManifest(ctx context.Context, m module.Version, contents []byte, mediaType string) (*Module, error) {
129 loc, err := c.resolve(m)
130 if err != nil {
131 return nil, err
132 }
133
134 manifest, err := unmarshalManifest(ctx, contents, mediaType)
135 if err != nil {
136 return nil, fmt.Errorf("module %v: %v", m, err)
137 }
138 if !isModule(manifest) {
139 return nil, fmt.Errorf("%v does not resolve to a manifest (media type is %q)", m, mediaType)
140 }
141
142 if n := len(manifest.Layers); n != 2 {
143 return nil, fmt.Errorf("module manifest should refer to exactly two blobs, but got %d", n)
144 }
145 if !isModuleFile(manifest.Layers[1]) {
146 return nil, fmt.Errorf("unexpected media type %q for module file blob", manifest.Layers[1].MediaType)
147 }
148
149 return &Module{
150 client: c,
151 loc: loc,
152 version: m,
153 manifest: *manifest,
154 manifestDigest: digest.FromBytes(contents),
155 }, nil
156 }
157
158
159
160
161
162 func (c *Client) ModuleVersions(ctx context.Context, m string) ([]string, error) {
163 mpath, major, hasMajor := module.SplitPathVersion(m)
164 if !hasMajor {
165 mpath = m
166 }
167 loc, err := c.resolver.ResolveToRegistry(mpath, "")
168 if err != nil {
169 return nil, err
170 }
171 versions := []string{}
172
173
174 iter := loc.Registry.Tags(ctx, loc.Repository, "")
175 var _err error
176 iter(func(tag string, err error) bool {
177 if err != nil {
178 _err = err
179 return false
180 }
181 vers, ok := strings.CutPrefix(tag, loc.Tag)
182 if !ok || !semver.IsValid(vers) {
183 return true
184 }
185 if !hasMajor || semver.Major(vers) == major {
186 versions = append(versions, vers)
187 }
188 return true
189 })
190 if _err != nil && !isNotExist(_err) {
191 return nil, _err
192 }
193 semver.Sort(versions)
194 return versions, nil
195 }
196
197
198
199
200 type checkedModule struct {
201 mv module.Version
202 blobr io.ReaderAt
203 size int64
204 zipr *zip.Reader
205 modFile *modfile.File
206 modFileContent []byte
207 }
208
209
210
211 func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule) error {
212 loc, err := c.resolve(m.mv)
213 if err != nil {
214 return err
215 }
216 selfDigest, err := digest.FromReader(io.NewSectionReader(m.blobr, 0, m.size))
217 if err != nil {
218 return fmt.Errorf("cannot read module zip file: %v", err)
219 }
220
221
222 configDesc, err := c.scratchConfig(ctx, loc, moduleArtifactType)
223 if err != nil {
224 return fmt.Errorf("cannot make scratch config: %v", err)
225 }
226 manifest := &ocispec.Manifest{
227 Versioned: specs.Versioned{
228 SchemaVersion: 2,
229 },
230 MediaType: ocispec.MediaTypeImageManifest,
231 Config: configDesc,
232
233 Layers: []ocispec.Descriptor{{
234 Digest: selfDigest,
235 MediaType: "application/zip",
236 Size: m.size,
237 }, {
238 Digest: digest.FromBytes(m.modFileContent),
239 MediaType: moduleFileMediaType,
240 Size: int64(len(m.modFileContent)),
241 }},
242 }
243
244 if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[0], io.NewSectionReader(m.blobr, 0, m.size)); err != nil {
245 return fmt.Errorf("cannot push module contents: %v", err)
246 }
247 if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[1], bytes.NewReader(m.modFileContent)); err != nil {
248 return fmt.Errorf("cannot push cue.mod/module.cue contents: %v", err)
249 }
250 manifestData, err := json.Marshal(manifest)
251 if err != nil {
252 return fmt.Errorf("cannot marshal manifest: %v", err)
253 }
254 if _, err := loc.Registry.PushManifest(ctx, loc.Repository, loc.Tag, manifestData, ocispec.MediaTypeImageManifest); err != nil {
255 return fmt.Errorf("cannot tag %v: %v", m.mv, err)
256 }
257 return nil
258 }
259
260
261
262
263
264
265
266 func (c *Client) PutModule(ctx context.Context, m module.Version, r io.ReaderAt, size int64) error {
267 cm, err := checkModule(m, r, size)
268 if err != nil {
269 return err
270 }
271 return c.putCheckedModule(ctx, cm)
272 }
273
274
275
276
277
278
279
280
281 func checkModule(m module.Version, blobr io.ReaderAt, size int64) (*checkedModule, error) {
282 zipr, modf, _, err := modzip.CheckZip(m, blobr, size)
283 if err != nil {
284 return nil, fmt.Errorf("module zip file check failed: %v", err)
285 }
286 modFileContent, mf, err := checkModFile(m, modf)
287 if err != nil {
288 return nil, fmt.Errorf("module.cue file check failed: %v", err)
289 }
290 return &checkedModule{
291 mv: m,
292 blobr: blobr,
293 size: size,
294 zipr: zipr,
295 modFile: mf,
296 modFileContent: modFileContent,
297 }, nil
298 }
299
300 func checkModFile(m module.Version, f *zip.File) ([]byte, *modfile.File, error) {
301 r, err := f.Open()
302 if err != nil {
303 return nil, nil, err
304 }
305 defer r.Close()
306
307 data, err := io.ReadAll(r)
308 if err != nil {
309 return nil, nil, err
310 }
311 mf, err := modfile.Parse(data, f.Name)
312 if err != nil {
313 return nil, nil, err
314 }
315 if mf.Module != m.Path() {
316 return nil, nil, fmt.Errorf("module path %q found in %s does not match module path being published %q", mf.Module, f.Name, m.Path())
317 }
318 _, major, ok := module.SplitPathVersion(mf.Module)
319 if !ok {
320
321
322 return nil, nil, fmt.Errorf("invalid module path %q", mf.Module)
323 }
324 wantMajor := semver.Major(m.Version())
325 if major != wantMajor {
326
327
328 return nil, nil, fmt.Errorf("major version %q found in %s does not match version being published %q", major, f.Name, m.Version())
329 }
330
331 for modPath, dep := range mf.Deps {
332 _, err := module.NewVersion(modPath, dep.Version)
333 if err != nil {
334 return nil, nil, fmt.Errorf("invalid dependency: %v @ %v", modPath, dep.Version)
335 }
336 }
337 return data, mf, nil
338 }
339
340
341 type Module struct {
342 client *Client
343 loc RegistryLocation
344 version module.Version
345 manifest ocispec.Manifest
346 manifestDigest ociregistry.Digest
347 }
348
349 func (m *Module) Version() module.Version {
350 return m.version
351 }
352
353
354 func (m *Module) ModuleFile(ctx context.Context) ([]byte, error) {
355 r, err := m.loc.Registry.GetBlob(ctx, m.loc.Repository, m.manifest.Layers[1].Digest)
356 if err != nil {
357 return nil, err
358 }
359 defer r.Close()
360 return io.ReadAll(r)
361 }
362
363
364
365
366
367 func (m *Module) GetZip(ctx context.Context) (io.ReadCloser, error) {
368 return m.loc.Registry.GetBlob(ctx, m.loc.Repository, m.manifest.Layers[0].Digest)
369 }
370
371
372
373 func (m *Module) ManifestDigest() ociregistry.Digest {
374 return m.manifestDigest
375 }
376
377 func (c *Client) resolve(m module.Version) (RegistryLocation, error) {
378 loc, err := c.resolver.ResolveToRegistry(m.BasePath(), m.Version())
379 if err != nil {
380 return RegistryLocation{}, err
381 }
382 if loc.Registry == nil {
383 return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to nil registry", m)
384 }
385 if loc.Repository == "" {
386 return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to empty location", m)
387 }
388 if loc.Tag == "" {
389 return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to empty tag", m)
390 }
391 return loc, nil
392 }
393
394 func unmarshalManifest(ctx context.Context, data []byte, mediaType string) (*ociregistry.Manifest, error) {
395 if !isJSON(mediaType) {
396 return nil, fmt.Errorf("expected JSON media type but %q does not look like JSON", mediaType)
397 }
398 var m ociregistry.Manifest
399 if err := json.Unmarshal(data, &m); err != nil {
400 return nil, fmt.Errorf("cannot decode %s content as manifest: %v", mediaType, err)
401 }
402 return &m, nil
403 }
404
405 func isNotExist(err error) bool {
406 return errors.Is(err, ociregistry.ErrNameUnknown) || errors.Is(err, ociregistry.ErrNameInvalid)
407 }
408
409 func isModule(m *ocispec.Manifest) bool {
410
411
412 return m.Config.MediaType == moduleArtifactType
413 }
414
415 func isModuleFile(desc ocispec.Descriptor) bool {
416 return desc.ArtifactType == moduleFileMediaType ||
417 desc.MediaType == moduleFileMediaType
418 }
419
420
421
422 func isJSON(mediaType string) bool {
423 return strings.HasSuffix(mediaType, "+json") || strings.HasSuffix(mediaType, "/json")
424 }
425
426
427
428
429 func (c *Client) scratchConfig(ctx context.Context, loc RegistryLocation, mediaType string) (ocispec.Descriptor, error) {
430
431 content := []byte("{}")
432 desc := ocispec.Descriptor{
433 Digest: digest.FromBytes(content),
434 MediaType: mediaType,
435 Size: int64(len(content)),
436 }
437 if _, err := loc.Registry.PushBlob(ctx, loc.Repository, desc, bytes.NewReader(content)); err != nil {
438 return ocispec.Descriptor{}, err
439 }
440 return desc, nil
441 }
442
443
444
445
446 type singleResolver struct {
447 R ociregistry.Interface
448 }
449
450 func (r singleResolver) ResolveToRegistry(mpath, vers string) (RegistryLocation, error) {
451 return RegistryLocation{
452 Registry: r.R,
453 Repository: mpath,
454 Tag: vers,
455 }, nil
456 }
457
View as plain text