// Package icons maps container image references to brand icons from the // Simple Icons set (CC0, see LICENSE-simple-icons.md). Only the master's UI // imports it, so the agent binary does not carry the data. package icons import ( "bytes" "compress/gzip" _ "embed" "encoding/json" "strings" "sync" "github.com/shcizo/package-updater/internal/imageref" ) //go:generate go run ./gen //go:embed simple-icons.json.gz var data []byte var ( once sync.Once paths map[string]string ) // aliases maps a normalised image name to the Simple Icons slug that differs // from it. var aliases = map[string]string{ "postgres": "postgresql", "goauthentik": "authentik", "traefik": "traefikproxy", "node": "nodedotjs", "mongo": "mongodb", } // skipNamespaces are registry namespaces that name a publisher, not a product. var skipNamespaces = map[string]bool{"library": true, "linuxserver": true, "bitnami": true, "hotio": true} // suffixes are stripped from the last path segment to find the product name // ("authentik-server" is authentik). var suffixes = []string{"-server", "-web", "-api", "-worker", "-agent"} func load() { once.Do(func() { paths = map[string]string{} zr, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return } _ = json.NewDecoder(zr).Decode(&paths) }) } // Path returns the SVG path data (24x24 viewBox) of the icon with this Simple // Icons slug. func Path(slug string) (string, bool) { load() p, ok := paths[slug] return p, ok } // Count returns how many icons are embedded. func Count() int { load() return len(paths) } // ForImage returns the slug of the icon that best matches the image // reference, or "" when there is none. It tries the last path segment, that // segment without a role suffix, and the namespace, in that order. func ForImage(ref string) string { segs := strings.Split(imageref.Parse(ref).Repo, "/") if len(segs) > 1 && isRegistryHost(segs[0]) { segs = segs[1:] } last := segs[len(segs)-1] candidates := []string{last} for _, s := range suffixes { if base, ok := strings.CutSuffix(last, s); ok && base != "" { candidates = append(candidates, base) break } } if len(segs) > 1 && !skipNamespaces[segs[len(segs)-2]] { candidates = append(candidates, segs[len(segs)-2]) } for _, c := range candidates { slug := normalise(c) if a, ok := aliases[slug]; ok { slug = a } if _, ok := Path(slug); ok { return slug } } return "" } func isRegistryHost(s string) bool { return strings.ContainsAny(s, ".:") || s == "localhost" } // normalise lowercases s and keeps only a-z and 0-9, the form of Simple Icons slugs. func normalise(s string) string { var b strings.Builder for _, r := range strings.ToLower(s) { if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' { b.WriteRune(r) } } return b.String() }