161 lines
4.6 KiB
Go
161 lines
4.6 KiB
Go
package registries
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// defaultOCIRegistry is the default registry when none is specified
|
|
defaultOCIRegistry = "docker.io"
|
|
// 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.
|
|
// 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")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Parse the path (registry/namespace/image or namespace/image or image)
|
|
parts := strings.Split(pathPart, "/")
|
|
|
|
switch len(parts) {
|
|
case 1:
|
|
// Just image name: "postgres:16" -> docker.io/library/postgres:16
|
|
result.Registry = defaultOCIRegistry
|
|
result.Namespace = defaultOCINamespace
|
|
result.Image = parts[0]
|
|
|
|
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]
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Validate we have either a tag or digest
|
|
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
|
|
}
|
|
}
|