Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand All @@ -34,6 +35,7 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ github.com/danielgtaylor/huma/v2 v2.34.1/go.mod h1:ynwJgLk8iGVgoaipi5tgwIQ5yoFNm
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand Down Expand Up @@ -44,6 +46,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
Expand Down
2 changes: 1 addition & 1 deletion internal/validators/registries/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func ValidateNPM(ctx context.Context, pkg model.Package, serverName string) erro

// Validate that MCPB-specific fields are not present
if pkg.FileSHA256 != "" {
return fmt.Errorf("NPM packages must not have 'fileSha256' field - this is only for MCPB packages")
return fmt.Errorf("NPM packages must not have 'fileSha256' field")
}

// Validate that the registry base URL matches NPM exactly
Expand Down
2 changes: 1 addition & 1 deletion internal/validators/registries/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func ValidateOCI(ctx context.Context, pkg model.Package, serverName string) erro
return fmt.Errorf("OCI packages must not have 'version' field - include version in 'identifier' instead (e.g., 'docker.io/owner/image:1.0.0')")
}
if pkg.FileSHA256 != "" {
return fmt.Errorf("OCI packages must not have 'fileSha256' field - this is only for MCPB packages")
return fmt.Errorf("OCI packages must not have 'fileSha256' field")
}

// Parse the canonical OCI reference from the identifier
Expand Down
102 changes: 30 additions & 72 deletions internal/validators/registries/oci_ref_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ package registries

import (
"fmt"
"regexp"
"strings"

"github.com/distribution/reference"
)

const (
// defaultOCIRegistry is the default registry when none is specified
defaultOCIRegistry = "docker.io"
// defaultOCINamespace is the default namespace for official images
defaultOCINamespace = "library"
)
Expand All @@ -22,7 +21,7 @@ type OCIReference struct {
Digest string // e.g., "sha256:abc..." (optional)
}

// ParseOCIReference parses a canonical OCI image reference.
// ParseOCIReference parses a canonical OCI image reference using github.com/distribution/reference.
// Supported formats:
// - registry/namespace/image:tag
// - registry/namespace/image@digest
Expand All @@ -34,84 +33,43 @@ func ParseOCIReference(ref string) (*OCIReference, error) {
return nil, fmt.Errorf("OCI reference cannot be empty")
}

result := &OCIReference{}

// Split by @ to separate digest
var mainPart string
if idx := strings.Index(ref, "@"); idx >= 0 {
mainPart = ref[:idx]
result.Digest = ref[idx+1:]

// Validate digest format
if !strings.HasPrefix(result.Digest, "sha256:") {
return nil, fmt.Errorf("invalid digest format: must start with 'sha256:'")
}
digestPattern := regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$`)
if !digestPattern.MatchString(result.Digest) {
return nil, fmt.Errorf("invalid digest format: must be sha256 followed by 64 hex characters")
}
} else {
mainPart = ref
// 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)
}

// Split by : to separate tag
var pathPart string
if idx := strings.LastIndex(mainPart, ":"); idx >= 0 {
// Check if this looks like a registry with port (e.g., localhost:5000)
// or a tag. If there's a / after the :, it's likely a port.
if idx > 0 && !strings.Contains(mainPart[idx:], "/") {
pathPart = mainPart[:idx]
result.Tag = mainPart[idx+1:]
} else {
pathPart = mainPart
}
} else {
pathPart = mainPart
}
result := &OCIReference{}

// Extract registry (domain)
result.Registry = reference.Domain(named)

// Parse the path (registry/namespace/image or namespace/image or image)
parts := strings.Split(pathPart, "/")
// Extract path (namespace/image or just image)
path := reference.Path(named)
parts := strings.Split(path, "/")

switch len(parts) {
case 1:
// Just image name: "postgres:16" -> docker.io/library/postgres:16
result.Registry = defaultOCIRegistry
// 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]
}

case 2:
// namespace/image: "owner/repo:tag" -> docker.io/owner/repo:tag
// OR registry/image: "ghcr.io/image" -> ghcr.io/library/image
// Heuristic: if first part looks like a domain (contains . or :), treat as registry
if strings.Contains(parts[0], ".") || strings.Contains(parts[0], ":") {
result.Registry = parts[0]
result.Namespace = defaultOCINamespace
result.Image = parts[1]
} else {
result.Registry = defaultOCIRegistry
result.Namespace = parts[0]
result.Image = parts[1]
}

case 3:
// registry/namespace/image: "ghcr.io/owner/repo:tag"
result.Registry = parts[0]
result.Namespace = parts[1]
result.Image = parts[2]
// Extract tag if present
if tagged, ok := named.(reference.Tagged); ok {
result.Tag = tagged.Tag()
}

default:
// More than 3 parts could be multi-level namespace (e.g., ghcr.io/org/team/repo)
// Take first as registry, last as image, everything in between as namespace
if len(parts) > 3 {
result.Registry = parts[0]
result.Namespace = strings.Join(parts[1:len(parts)-1], "/")
result.Image = parts[len(parts)-1]
} else {
return nil, fmt.Errorf("invalid OCI reference format: %s", ref)
}
// Extract digest if present
if digested, ok := named.(reference.Digested); ok {
result.Digest = digested.Digest().String()
}

// Validate we have either a tag or digest
// 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)
}
Expand Down