A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1package main
2
3import (
4 "os"
5 "strings"
6)
7
8// ImageRef is a parsed container image reference
9type ImageRef struct {
10 Host string
11 Identity string
12 Repo string
13 Tag string
14 Raw string
15}
16
17// detectImageRef walks the process tree looking for an image reference
18// that matches the given registry host. It starts from the parent process
19// and walks up to 5 ancestors to handle wrapper scripts (make, bash -c, etc.).
20//
21// Returns nil if no matching image reference is found — callers should
22// fall back to the active account.
23func detectImageRef(registryHost string) *ImageRef {
24 // Normalize the registry host for matching
25 matchHost := strings.TrimPrefix(registryHost, "https://")
26 matchHost = strings.TrimPrefix(matchHost, "http://")
27 matchHost = strings.TrimSuffix(matchHost, "/")
28
29 pid := os.Getppid()
30 for depth := 0; depth < 5; depth++ {
31 args, err := getProcessArgs(pid)
32 if err != nil {
33 break
34 }
35
36 for _, arg := range args {
37 if ref := parseImageRef(arg, matchHost); ref != nil {
38 return ref
39 }
40 }
41
42 ppid, err := getParentPID(pid)
43 if err != nil || ppid == pid || ppid <= 1 {
44 break
45 }
46 pid = ppid
47 }
48
49 return nil
50}
51
52// parseImageRef tries to parse a string as a container image reference.
53// Expected format: host/identity/repo:tag or host/identity/repo
54//
55// Handles:
56// - docker:// and oci:// transport prefixes (skopeo)
57// - Flags (- prefix), paths (/ or . prefix), shell artifacts (|, &, ;)
58// - Optional tag (defaults to "latest")
59// - Host must look like a domain (contains ., or is localhost, or has :port)
60// - If matchHost is non-empty, only returns refs matching that host
61func parseImageRef(s string, matchHost string) *ImageRef {
62 // Skip flags, absolute paths, relative paths
63 if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "/") || strings.HasPrefix(s, ".") {
64 return nil
65 }
66
67 // Strip docker:// or oci:// transport prefixes (skopeo)
68 s = strings.TrimPrefix(s, "docker://")
69 s = strings.TrimPrefix(s, "oci://")
70
71 // Skip other transport schemes
72 if strings.Contains(s, "://") {
73 return nil
74 }
75 // Must contain at least one slash
76 if !strings.Contains(s, "/") {
77 return nil
78 }
79 // Skip things that look like shell commands
80 if strings.ContainsAny(s, " |&;") {
81 return nil
82 }
83
84 // Split off tag
85 tag := "latest"
86 refPart := s
87 if atIdx := strings.LastIndex(s, ":"); atIdx != -1 {
88 lastSlash := strings.LastIndex(s, "/")
89 if atIdx > lastSlash {
90 tag = s[atIdx+1:]
91 refPart = s[:atIdx]
92 }
93 }
94
95 parts := strings.Split(refPart, "/")
96
97 // ATCR pattern requires host/identity/repo (3+ parts)
98 if len(parts) < 3 {
99 return nil
100 }
101
102 host := parts[0]
103 identity := parts[1]
104 repo := strings.Join(parts[2:], "/")
105
106 // Host must look like a domain
107 if !strings.Contains(host, ".") && host != "localhost" && !strings.Contains(host, ":") {
108 return nil
109 }
110
111 // If a specific host was requested, enforce it
112 if matchHost != "" && host != matchHost {
113 return nil
114 }
115
116 return &ImageRef{
117 Host: host,
118 Identity: identity,
119 Repo: repo,
120 Tag: tag,
121 Raw: s,
122 }
123}