Files
at-container-registry/cmd/credential-helper/detect.go

124 lines
3.0 KiB
Go

package main
import (
"os"
"strings"
)
// ImageRef is a parsed container image reference
type ImageRef struct {
Host string
Identity string
Repo string
Tag string
Raw string
}
// detectImageRef walks the process tree looking for an image reference
// that matches the given registry host. It starts from the parent process
// and walks up to 5 ancestors to handle wrapper scripts (make, bash -c, etc.).
//
// Returns nil if no matching image reference is found — callers should
// fall back to the active account.
func detectImageRef(registryHost string) *ImageRef {
// Normalize the registry host for matching
matchHost := strings.TrimPrefix(registryHost, "https://")
matchHost = strings.TrimPrefix(matchHost, "http://")
matchHost = strings.TrimSuffix(matchHost, "/")
pid := os.Getppid()
for depth := 0; depth < 5; depth++ {
args, err := getProcessArgs(pid)
if err != nil {
break
}
for _, arg := range args {
if ref := parseImageRef(arg, matchHost); ref != nil {
return ref
}
}
ppid, err := getParentPID(pid)
if err != nil || ppid == pid || ppid <= 1 {
break
}
pid = ppid
}
return nil
}
// parseImageRef tries to parse a string as a container image reference.
// Expected format: host/identity/repo:tag or host/identity/repo
//
// Handles:
// - docker:// and oci:// transport prefixes (skopeo)
// - Flags (- prefix), paths (/ or . prefix), shell artifacts (|, &, ;)
// - Optional tag (defaults to "latest")
// - Host must look like a domain (contains ., or is localhost, or has :port)
// - If matchHost is non-empty, only returns refs matching that host
func parseImageRef(s string, matchHost string) *ImageRef {
// Skip flags, absolute paths, relative paths
if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "/") || strings.HasPrefix(s, ".") {
return nil
}
// Strip docker:// or oci:// transport prefixes (skopeo)
s = strings.TrimPrefix(s, "docker://")
s = strings.TrimPrefix(s, "oci://")
// Skip other transport schemes
if strings.Contains(s, "://") {
return nil
}
// Must contain at least one slash
if !strings.Contains(s, "/") {
return nil
}
// Skip things that look like shell commands
if strings.ContainsAny(s, " |&;") {
return nil
}
// Split off tag
tag := "latest"
refPart := s
if atIdx := strings.LastIndex(s, ":"); atIdx != -1 {
lastSlash := strings.LastIndex(s, "/")
if atIdx > lastSlash {
tag = s[atIdx+1:]
refPart = s[:atIdx]
}
}
parts := strings.Split(refPart, "/")
// ATCR pattern requires host/identity/repo (3+ parts)
if len(parts) < 3 {
return nil
}
host := parts[0]
identity := parts[1]
repo := strings.Join(parts[2:], "/")
// Host must look like a domain
if !strings.Contains(host, ".") && host != "localhost" && !strings.Contains(host, ":") {
return nil
}
// If a specific host was requested, enforce it
if matchHost != "" && host != matchHost {
return nil
}
return &ImageRef{
Host: host,
Identity: identity,
Repo: repo,
Tag: tag,
Raw: s,
}
}