registry/internal/validators/registries/oci_ref_parser.go
Radoslav Dimitrov 6d110bf99e
Use distribution/reference for parsing OCI references (#651)
<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
The following PR addresses the feedback from #634 around:
* Use a proper library - https://github.com/distribution/reference,
instead of the implementing the OCI parsing ourselves (note: left the
tests so we can ensure we are not breaking any existing behavior)
* Updating one of the error messages to now assume MCPB.

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
2025-10-14 15:27:57 +03:00

119 lines
3.2 KiB
Go

package registries
import (
"fmt"
"strings"
"github.com/distribution/reference"
)
const (
// defaultOCINamespace is the default namespace for official images
defaultOCINamespace = "library"
)
// OCIReference represents a parsed OCI image reference
type OCIReference struct {
Registry string // e.g., "ghcr.io", "docker.io"
Namespace string // e.g., "owner", "library"
Image string // e.g., "repo"
Tag string // e.g., "v1.0.0", "latest" (optional)
Digest string // e.g., "sha256:abc..." (optional)
}
// ParseOCIReference parses a canonical OCI image reference using github.com/distribution/reference.
// Supported formats:
// - registry/namespace/image:tag
// - registry/namespace/image@digest
// - registry/namespace/image:tag@digest
// - namespace/image:tag (defaults to docker.io)
// - image:tag (defaults to docker.io/library)
func ParseOCIReference(ref string) (*OCIReference, error) {
if ref == "" {
return nil, fmt.Errorf("OCI reference cannot be empty")
}
// Parse using distribution/reference - normalizes short forms to canonical
named, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return nil, fmt.Errorf("invalid OCI reference format: %w", err)
}
result := &OCIReference{}
// Extract registry (domain)
result.Registry = reference.Domain(named)
// Extract path (namespace/image or just image)
path := reference.Path(named)
parts := strings.Split(path, "/")
// Parse namespace and image from path
if len(parts) == 1 {
// Single part: library/image (docker.io default namespace)
result.Namespace = defaultOCINamespace
result.Image = parts[0]
} else {
// Multiple parts: namespace/image or org/team/image
result.Namespace = strings.Join(parts[:len(parts)-1], "/")
result.Image = parts[len(parts)-1]
}
// Extract tag if present
if tagged, ok := named.(reference.Tagged); ok {
result.Tag = tagged.Tag()
}
// Extract digest if present
if digested, ok := named.(reference.Digested); ok {
result.Digest = digested.Digest().String()
}
// Validate we have either a tag or digest (required for MCP registry)
if result.Tag == "" && result.Digest == "" {
return nil, fmt.Errorf("OCI reference must include either a tag or digest: %s", ref)
}
// Default tag to "latest" if only digest is provided (for display purposes)
// Note: when pulling by digest, the tag is ignored by registries
if result.Tag == "" && result.Digest != "" {
result.Tag = "latest"
}
return result, nil
}
// String returns the canonical string representation of the OCI reference
func (r *OCIReference) String() string {
var sb strings.Builder
sb.WriteString(r.Registry)
sb.WriteString("/")
sb.WriteString(r.Namespace)
sb.WriteString("/")
sb.WriteString(r.Image)
if r.Tag != "" {
sb.WriteString(":")
sb.WriteString(r.Tag)
}
if r.Digest != "" {
sb.WriteString("@")
sb.WriteString(r.Digest)
}
return sb.String()
}
// GetRegistryBaseURL returns the full registry URL (e.g., "https://docker.io" or "https://ghcr.io")
func (r *OCIReference) GetRegistryBaseURL() string {
switch r.Registry {
case "docker.io", "registry.docker.io", "index.docker.io":
return "https://docker.io"
case "ghcr.io":
return "https://ghcr.io"
default:
return "https://" + r.Registry
}
}