<!-- 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>
119 lines
3.2 KiB
Go
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
|
|
}
|
|
}
|