cuelang.org/go@v0.13.0/mod/modregistry/client.go (about) 1 // Copyright 2023 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package modregistry provides functionality for reading and writing 16 // CUE modules from an OCI registry. 17 // 18 // WARNING: THIS PACKAGE IS EXPERIMENTAL. 19 // ITS API MAY CHANGE AT ANY TIME. 20 package modregistry 21 22 import ( 23 "archive/zip" 24 "bytes" 25 "context" 26 "encoding/json" 27 "errors" 28 "fmt" 29 "io" 30 "net/http" 31 "strings" 32 33 "cuelabs.dev/go/oci/ociregistry" 34 "cuelabs.dev/go/oci/ociregistry/ociref" 35 "cuelang.org/go/internal/mod/semver" 36 digest "github.com/opencontainers/go-digest" 37 specs "github.com/opencontainers/image-spec/specs-go" 38 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 39 "golang.org/x/sync/errgroup" 40 41 "cuelang.org/go/cue/ast" 42 "cuelang.org/go/mod/modfile" 43 "cuelang.org/go/mod/module" 44 "cuelang.org/go/mod/modzip" 45 ) 46 47 var ErrNotFound = fmt.Errorf("module not found") 48 49 // Client represents a OCI-registry-backed client that 50 // provides a store for CUE modules. 51 type Client struct { 52 resolver Resolver 53 } 54 55 // Resolver resolves module paths to a registry and a location 56 // within that registry. 57 type Resolver interface { 58 // ResolveToRegistry resolves a base module path (without a version) 59 // and optional version to the location for that path. 60 // 61 // If the version is empty, the Tag in the returned Location 62 // will hold the prefix that all versions of the module in its 63 // repository have. That prefix will be followed by the version 64 // itself. 65 // 66 // If there is no registry configured for the module, it returns 67 // an [ErrRegistryNotFound] error. 68 ResolveToRegistry(mpath, vers string) (RegistryLocation, error) 69 } 70 71 // ErrRegistryNotFound is returned by [Resolver.ResolveToRegistry] 72 // when there is no registry configured for a module. 73 var ErrRegistryNotFound = fmt.Errorf("no registry configured for module") 74 75 // RegistryLocation holds a registry and a location within it 76 // that a specific module (or set of versions for a module) 77 // will be stored. 78 type RegistryLocation struct { 79 // Registry holds the registry to use to access the module. 80 Registry ociregistry.Interface 81 // Repository holds the repository where the module is located. 82 Repository string 83 // Tag holds the tag for the module version. If an empty version 84 // was passed to Resolve, it holds the prefix shared by all 85 // version tags for the module. 86 Tag string 87 } 88 89 const ( 90 moduleArtifactType = "application/vnd.cue.module.v1+json" 91 moduleFileMediaType = "application/vnd.cue.modulefile.v1" 92 moduleAnnotation = "works.cue.module" 93 ) 94 95 // NewClient returns a new client that talks to the registry at the given 96 // hostname. 97 func NewClient(registry ociregistry.Interface) *Client { 98 return &Client{ 99 resolver: singleResolver{registry}, 100 } 101 } 102 103 // NewClientWithResolver returns a new client that uses the given 104 // resolver to decide which registries to fetch from or push to. 105 func NewClientWithResolver(resolver Resolver) *Client { 106 return &Client{ 107 resolver: resolver, 108 } 109 } 110 111 // Mirror ensures that the given module and its component parts 112 // are present and identical in both src and dst. 113 func (src *Client) Mirror(ctx context.Context, dst *Client, mv module.Version) error { 114 m, err := src.GetModule(ctx, mv) 115 if err != nil { 116 return err 117 } 118 dstLoc, err := dst.resolve(mv) 119 if err != nil { 120 return fmt.Errorf("cannot resolve module in destination: %w", err) 121 } 122 123 // TODO ideally this parallelism would respect parallelism limits 124 // on whatever is calling the function too rather than just doing 125 // all uploads in parallel. 126 var g errgroup.Group 127 // TODO for desc := range manifestRefs(m.manifest) 128 manifestRefs(m.manifest)(func(desc ociregistry.Descriptor) bool { 129 g.Go(func() error { 130 return mirrorBlob(ctx, m.loc, dstLoc, desc) 131 }) 132 return true 133 }) 134 if err := g.Wait(); err != nil { 135 return err 136 } 137 // We've uploaded all the blobs referenced by the manifest; now 138 // we can upload the manifest itself. 139 if _, err := dstLoc.Registry.ResolveManifest(ctx, dstLoc.Repository, m.manifestDigest); err == nil { 140 return nil 141 } 142 if _, err := dstLoc.Registry.PushManifest(ctx, dstLoc.Repository, dstLoc.Tag, m.manifestContents, ocispec.MediaTypeImageManifest); err != nil { 143 return nil 144 } 145 return nil 146 } 147 148 func mirrorBlob(ctx context.Context, srcLoc, dstLoc RegistryLocation, desc ocispec.Descriptor) error { 149 desc1, err := dstLoc.Registry.ResolveBlob(ctx, dstLoc.Repository, desc.Digest) 150 if err == nil { 151 // Blob already exists in destination. Check that its size agrees. 152 if desc1.Size != desc.Size { 153 return fmt.Errorf("destination size (%d) does not agree with source size (%d) in blob %v", desc1.Size, desc.Size, desc.Digest) 154 } 155 return nil 156 } 157 r, err := srcLoc.Registry.GetBlob(ctx, srcLoc.Repository, desc.Digest) 158 if err != nil { 159 return err 160 } 161 defer r.Close() 162 if _, err := dstLoc.Registry.PushBlob(ctx, dstLoc.Repository, desc, r); err != nil { 163 return err 164 } 165 return nil 166 } 167 168 // GetModule returns the module instance for the given version. 169 // It returns an error that satisfies [errors.Is]([ErrNotFound]) if the 170 // module is not present in the store at this version. 171 func (c *Client) GetModule(ctx context.Context, m module.Version) (*Module, error) { 172 loc, err := c.resolve(m) 173 if err != nil { 174 if errors.Is(err, ErrRegistryNotFound) { 175 return nil, ErrNotFound 176 } 177 return nil, err 178 } 179 rd, err := loc.Registry.GetTag(ctx, loc.Repository, loc.Tag) 180 if err != nil { 181 if isNotExist(err) { 182 return nil, fmt.Errorf("module %v: %w", m, ErrNotFound) 183 } 184 return nil, fmt.Errorf("module %v: %w", m, err) 185 } 186 defer rd.Close() 187 data, err := io.ReadAll(rd) 188 if err != nil { 189 return nil, err 190 } 191 192 return c.GetModuleWithManifest(m, data, rd.Descriptor().MediaType) 193 } 194 195 // GetModuleWithManifest returns a module instance given 196 // the top level manifest contents, without querying its tag. 197 // It assumes that the module will be tagged with the given version. 198 func (c *Client) GetModuleWithManifest(m module.Version, contents []byte, mediaType string) (*Module, error) { 199 loc, err := c.resolve(m) 200 if err != nil { 201 // Note: don't return [ErrNotFound] here because if we've got the 202 // manifest we should be pretty sure that the module actually 203 // exists, so it's a harder error than if we're getting the module 204 // by tag. 205 return nil, err 206 } 207 208 manifest, err := unmarshalManifest(contents, mediaType) 209 if err != nil { 210 return nil, fmt.Errorf("module %v: %v", m, err) 211 } 212 if !isModule(manifest) { 213 return nil, fmt.Errorf("%v does not resolve to a manifest (media type is %q)", m, mediaType) 214 } 215 // TODO check type of manifest too. 216 if n := len(manifest.Layers); n != 2 { 217 return nil, fmt.Errorf("module manifest should refer to exactly two blobs, but got %d", n) 218 } 219 if !isModuleFile(manifest.Layers[1]) { 220 return nil, fmt.Errorf("unexpected media type %q for module file blob", manifest.Layers[1].MediaType) 221 } 222 // TODO check that the other blobs are of the expected type (application/zip). 223 return &Module{ 224 client: c, 225 loc: loc, 226 version: m, 227 manifest: *manifest, 228 manifestContents: contents, 229 manifestDigest: digest.FromBytes(contents), 230 }, nil 231 } 232 233 // ModuleVersions returns all the versions for the module with the given path 234 // sorted in semver order. 235 // If m has a major version suffix, only versions with that major version will 236 // be returned. 237 func (c *Client) ModuleVersions(ctx context.Context, m string) (_req []string, _err0 error) { 238 mpath, major, hasMajor := ast.SplitPackageVersion(m) 239 loc, err := c.resolver.ResolveToRegistry(mpath, "") 240 if err != nil { 241 if errors.Is(err, ErrRegistryNotFound) { 242 return nil, nil 243 } 244 return nil, err 245 } 246 versions := []string{} 247 if !ociref.IsValidRepository(loc.Repository) { 248 // If it's not a valid repository, it can't be used in an OCI 249 // request, so return an empty slice rather than the 250 // "invalid OCI request" error that a registry can return. 251 return nil, nil 252 } 253 // Note: do not use c.repoName because that always expects 254 // a module path with a major version. 255 iter := loc.Registry.Tags(ctx, loc.Repository, "") 256 var _err error 257 iter(func(tag string, err error) bool { 258 if err != nil { 259 _err = err 260 return false 261 } 262 vers, ok := strings.CutPrefix(tag, loc.Tag) 263 if !ok || !semver.IsValid(vers) { 264 return true 265 } 266 if !hasMajor || semver.Major(vers) == major { 267 versions = append(versions, vers) 268 } 269 return true 270 }) 271 if _err != nil && !isNotExist(_err) { 272 return nil, fmt.Errorf("module %v: %w", m, _err) 273 } 274 semver.Sort(versions) 275 return versions, nil 276 } 277 278 // checkedModule represents module content that has passed the same 279 // checks made by [Client.PutModule]. The caller should not mutate 280 // any of the values returned by its methods. 281 type checkedModule struct { 282 mv module.Version 283 blobr io.ReaderAt 284 size int64 285 zipr *zip.Reader 286 modFile *modfile.File 287 modFileContent []byte 288 } 289 290 // putCheckedModule is like [Client.PutModule] except that it allows the 291 // caller to do some additional checks (see [CheckModule] for more info). 292 func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule, meta *Metadata) error { 293 var annotations map[string]string 294 if meta != nil { 295 annotations0, err := meta.annotations() 296 if err != nil { 297 return fmt.Errorf("invalid metadata: %v", err) 298 } 299 annotations = annotations0 300 } 301 loc, err := c.resolve(m.mv) 302 if err != nil { 303 return err 304 } 305 selfDigest, err := digest.FromReader(io.NewSectionReader(m.blobr, 0, m.size)) 306 if err != nil { 307 return fmt.Errorf("cannot read module zip file: %v", err) 308 } 309 // Upload the actual module's content 310 // TODO should we use a custom media type for this? 311 configDesc, err := c.scratchConfig(ctx, loc, moduleArtifactType) 312 if err != nil { 313 return fmt.Errorf("cannot make scratch config: %v", err) 314 } 315 manifest := &ocispec.Manifest{ 316 Versioned: specs.Versioned{ 317 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 318 }, 319 MediaType: ocispec.MediaTypeImageManifest, 320 Config: configDesc, 321 // One for self, one for module file. 322 Layers: []ocispec.Descriptor{{ 323 Digest: selfDigest, 324 MediaType: "application/zip", 325 Size: m.size, 326 }, { 327 Digest: digest.FromBytes(m.modFileContent), 328 MediaType: moduleFileMediaType, 329 Size: int64(len(m.modFileContent)), 330 }}, 331 Annotations: annotations, 332 } 333 334 if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[0], io.NewSectionReader(m.blobr, 0, m.size)); err != nil { 335 return fmt.Errorf("cannot push module contents: %v", err) 336 } 337 if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[1], bytes.NewReader(m.modFileContent)); err != nil { 338 return fmt.Errorf("cannot push cue.mod/module.cue contents: %v", err) 339 } 340 manifestData, err := json.Marshal(manifest) 341 if err != nil { 342 return fmt.Errorf("cannot marshal manifest: %v", err) 343 } 344 if _, err := loc.Registry.PushManifest(ctx, loc.Repository, loc.Tag, manifestData, ocispec.MediaTypeImageManifest); err != nil { 345 return fmt.Errorf("cannot tag %v: %v", m.mv, err) 346 } 347 return nil 348 } 349 350 // PutModule puts a module whose contents are held as a zip archive inside f. 351 // It assumes all the module dependencies are correctly resolved and present 352 // inside the cue.mod/module.cue file. 353 func (c *Client) PutModule(ctx context.Context, m module.Version, r io.ReaderAt, size int64) error { 354 return c.PutModuleWithMetadata(ctx, m, r, size, nil) 355 } 356 357 // PutModuleWithMetadata is like [Client.PutModule] except that it also 358 // includes the given metadata inside the module's manifest. 359 // If meta is nil, no metadata will be included, otherwise 360 // all fields in meta must be valid and non-empty. 361 func (c *Client) PutModuleWithMetadata(ctx context.Context, m module.Version, r io.ReaderAt, size int64, meta *Metadata) error { 362 cm, err := checkModule(m, r, size) 363 if err != nil { 364 return err 365 } 366 return c.putCheckedModule(ctx, cm, meta) 367 } 368 369 // checkModule checks a module's zip file before uploading it. 370 // This does the same checks that [Client.PutModule] does, so 371 // can be used to avoid doing duplicate work when an uploader 372 // wishes to do more checks that are implemented by that method. 373 // 374 // Note that the returned [CheckedModule] value contains r, so will 375 // be invalidated if r is closed. 376 func checkModule(m module.Version, blobr io.ReaderAt, size int64) (*checkedModule, error) { 377 zipr, modf, _, err := modzip.CheckZip(m, blobr, size) 378 if err != nil { 379 return nil, fmt.Errorf("module zip file check failed: %v", err) 380 } 381 modFileContent, mf, err := checkModFile(m, modf) 382 if err != nil { 383 return nil, fmt.Errorf("module.cue file check failed: %v", err) 384 } 385 return &checkedModule{ 386 mv: m, 387 blobr: blobr, 388 size: size, 389 zipr: zipr, 390 modFile: mf, 391 modFileContent: modFileContent, 392 }, nil 393 } 394 395 func checkModFile(m module.Version, f *zip.File) ([]byte, *modfile.File, error) { 396 r, err := f.Open() 397 if err != nil { 398 return nil, nil, err 399 } 400 defer r.Close() 401 // TODO check max size? 402 data, err := io.ReadAll(r) 403 if err != nil { 404 return nil, nil, err 405 } 406 mf, err := modfile.Parse(data, f.Name) 407 if err != nil { 408 return nil, nil, err 409 } 410 if mf.QualifiedModule() != m.Path() { 411 return nil, nil, fmt.Errorf("module path %q found in %s does not match module path being published %q", mf.QualifiedModule(), f.Name, m.Path()) 412 } 413 wantMajor := semver.Major(m.Version()) 414 if major := mf.MajorVersion(); major != wantMajor { 415 // This can't actually happen because the zip checker checks the major version 416 // that's being published to, so the above path check also implicitly checks that. 417 return nil, nil, fmt.Errorf("major version %q found in %s does not match version being published %q", major, f.Name, m.Version()) 418 } 419 // Check that all dependency versions look valid. 420 for modPath, dep := range mf.Deps { 421 _, err := module.NewVersion(modPath, dep.Version) 422 if err != nil { 423 return nil, nil, fmt.Errorf("invalid dependency: %v @ %v", modPath, dep.Version) 424 } 425 } 426 return data, mf, nil 427 } 428 429 // Module represents a CUE module instance. 430 type Module struct { 431 client *Client 432 loc RegistryLocation 433 version module.Version 434 manifest ocispec.Manifest 435 manifestContents []byte 436 manifestDigest ociregistry.Digest 437 } 438 439 func (m *Module) Version() module.Version { 440 return m.version 441 } 442 443 // ModuleFile returns the contents of the cue.mod/module.cue file. 444 func (m *Module) ModuleFile(ctx context.Context) ([]byte, error) { 445 r, err := m.loc.Registry.GetBlob(ctx, m.loc.Repository, m.manifest.Layers[1].Digest) 446 if err != nil { 447 return nil, err 448 } 449 defer r.Close() 450 return io.ReadAll(r) 451 } 452 453 // Metadata returns the metadata associated with the module. 454 // If there is none, it returns (nil, nil). 455 func (m *Module) Metadata() (*Metadata, error) { 456 return newMetadataFromAnnotations(m.manifest.Annotations) 457 } 458 459 // GetZip returns a reader that can be used to read the contents of the zip 460 // archive containing the module files. The reader should be closed after use, 461 // and the contents should not be assumed to be correct until the close 462 // error has been checked. 463 func (m *Module) GetZip(ctx context.Context) (io.ReadCloser, error) { 464 return m.loc.Registry.GetBlob(ctx, m.loc.Repository, m.manifest.Layers[0].Digest) 465 } 466 467 // ManifestDigest returns the digest of the manifest representing 468 // the module. 469 func (m *Module) ManifestDigest() ociregistry.Digest { 470 return m.manifestDigest 471 } 472 473 func (c *Client) resolve(m module.Version) (RegistryLocation, error) { 474 loc, err := c.resolver.ResolveToRegistry(m.BasePath(), m.Version()) 475 if err != nil { 476 return RegistryLocation{}, err 477 } 478 if loc.Registry == nil { 479 return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to nil registry", m) 480 } 481 if loc.Repository == "" { 482 return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to empty location", m) 483 } 484 if loc.Tag == "" { 485 return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to empty tag", m) 486 } 487 return loc, nil 488 } 489 490 func unmarshalManifest(data []byte, mediaType string) (*ociregistry.Manifest, error) { 491 if !isJSON(mediaType) { 492 return nil, fmt.Errorf("expected JSON media type but %q does not look like JSON", mediaType) 493 } 494 var m ociregistry.Manifest 495 if err := json.Unmarshal(data, &m); err != nil { 496 return nil, fmt.Errorf("cannot decode %s content as manifest: %v", mediaType, err) 497 } 498 return &m, nil 499 } 500 501 func isNotExist(err error) bool { 502 if errors.Is(err, ociregistry.ErrNameUnknown) || 503 errors.Is(err, ociregistry.ErrNameInvalid) { 504 return true 505 } 506 // A 403 error might have been sent as a response 507 // without explicitly including a "denied" error code. 508 // We treat this as a "not found" error because there's 509 // nothing the user can do about it. 510 // 511 // Also, some registries return an invalid error code with a 404 512 // response (see https://cuelang.org/issue/2982), so it 513 // seems reasonable to treat that as a non-found error too. 514 if herr := ociregistry.HTTPError(nil); errors.As(err, &herr) { 515 statusCode := herr.StatusCode() 516 return statusCode == http.StatusForbidden || 517 statusCode == http.StatusNotFound 518 } 519 return false 520 } 521 522 func isModule(m *ocispec.Manifest) bool { 523 // TODO check m.ArtifactType too when that's defined? 524 // See https://github.com/opencontainers/image-spec/blob/main/manifest.md#image-manifest-property-descriptions 525 return m.Config.MediaType == moduleArtifactType 526 } 527 528 func isModuleFile(desc ocispec.Descriptor) bool { 529 return desc.ArtifactType == moduleFileMediaType || 530 desc.MediaType == moduleFileMediaType 531 } 532 533 // isJSON reports whether the given media type has JSON as an underlying encoding. 534 // TODO this is a guess. There's probably a more correct way to do it. 535 func isJSON(mediaType string) bool { 536 return strings.HasSuffix(mediaType, "+json") || strings.HasSuffix(mediaType, "/json") 537 } 538 539 // scratchConfig returns a dummy configuration consisting only of the 540 // two-byte configuration {}. 541 // https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-of-a-scratch-config-or-layer-descriptor 542 func (c *Client) scratchConfig(ctx context.Context, loc RegistryLocation, mediaType string) (ocispec.Descriptor, error) { 543 // TODO check if it exists already to avoid push? 544 content := []byte("{}") 545 desc := ocispec.Descriptor{ 546 Digest: digest.FromBytes(content), 547 MediaType: mediaType, 548 Size: int64(len(content)), 549 } 550 if _, err := loc.Registry.PushBlob(ctx, loc.Repository, desc, bytes.NewReader(content)); err != nil { 551 return ocispec.Descriptor{}, err 552 } 553 return desc, nil 554 } 555 556 // singleResolver implements Resolver by always returning R, 557 // and mapping module paths directly to repository paths in 558 // the registry. 559 type singleResolver struct { 560 R ociregistry.Interface 561 } 562 563 func (r singleResolver) ResolveToRegistry(mpath, vers string) (RegistryLocation, error) { 564 return RegistryLocation{ 565 Registry: r.R, 566 Repository: mpath, 567 Tag: vers, 568 }, nil 569 } 570 571 // manifestRefs returns an iterator that produces all the references 572 // contained in m. 573 func manifestRefs(m ocispec.Manifest) func(func(ociregistry.Descriptor) bool) { 574 return func(yield func(ociregistry.Descriptor) bool) { 575 if !yield(m.Config) { 576 return 577 } 578 for _, desc := range m.Layers { 579 if !yield(desc) { 580 return 581 } 582 } 583 // For completeness, although we shouldn't actually use this 584 // logic. 585 if m.Subject != nil { 586 yield(*m.Subject) 587 } 588 } 589 }