mirror of
https://github.com/regclient/regclient.git
synced 2025-04-18 22:44:00 +03:00
- Use "any" instead of an empty interface. - Use range over an integer for for loops. - Remove shadow variables in loops now that Go no longer reuses the variable. - Use "slices.Contains", "slices.Delete", "slices.Equal", "slices.Index", "slices.SortFunc". - Use "cmp.Or", "min", and "max". - Use "fmt.Appendf" instead of "Sprintf" for generating a byte slice. - Use "errors.Join" or "fmt.Errorf" with multiple "%w" for multiple errors. Additionally, use modern regclient features: - Use "ref.SetTag", "ref.SetDigest", and "ref.AddDigest". - Call "regclient.ManifestGet" using "WithManifestDesc" instead of setting the digest on the reference. Signed-off-by: Brandon Mitchell <git@bmitch.net>
306 lines
8.8 KiB
Go
306 lines
8.8 KiB
Go
// Package ref is used to define references.
|
|
// References default to remote registry references (registry:port/repo:tag).
|
|
// Schemes can be included in front of the reference for different reference types.
|
|
package ref
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/regclient/regclient/types/errs"
|
|
)
|
|
|
|
const (
|
|
dockerLibrary = "library"
|
|
// dockerRegistry is the name resolved in docker images on Hub.
|
|
dockerRegistry = "docker.io"
|
|
// dockerRegistryLegacy is the name resolved in docker images on Hub.
|
|
dockerRegistryLegacy = "index.docker.io"
|
|
// dockerRegistryDNS is the host to connect to for Hub.
|
|
dockerRegistryDNS = "registry-1.docker.io"
|
|
)
|
|
|
|
var (
|
|
hostPartS = `(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)`
|
|
hostPortS = `(?:` + hostPartS + `(?:` + regexp.QuoteMeta(`.`) + hostPartS + `)*` + regexp.QuoteMeta(`.`) + `?` + regexp.QuoteMeta(`:`) + `[0-9]+)`
|
|
hostDomainS = `(?:` + hostPartS + `(?:(?:` + regexp.QuoteMeta(`.`) + hostPartS + `)+` + regexp.QuoteMeta(`.`) + `?|` + regexp.QuoteMeta(`.`) + `))`
|
|
hostUpperS = `(?:[a-zA-Z0-9]*[A-Z][a-zA-Z0-9-]*[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[A-Z][a-zA-Z0-9]*)`
|
|
registryS = `(?:` + hostDomainS + `|` + hostPortS + `|` + hostUpperS + `|localhost(?:` + regexp.QuoteMeta(`:`) + `[0-9]+)?)`
|
|
repoPartS = `[a-z0-9]+(?:(?:\.|_|__|-+)[a-z0-9]+)*`
|
|
pathS = `[/a-zA-Z0-9_\-. ~\+]+`
|
|
tagS = `[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}`
|
|
digestS = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`
|
|
schemeRE = regexp.MustCompile(`^([a-z]+)://(.+)$`)
|
|
registryRE = regexp.MustCompile(`^(` + registryS + `)$`)
|
|
refRE = regexp.MustCompile(`^(?:(` + registryS + `)` + regexp.QuoteMeta(`/`) + `)?` +
|
|
`(` + repoPartS + `(?:` + regexp.QuoteMeta(`/`) + repoPartS + `)*)` +
|
|
`(?:` + regexp.QuoteMeta(`:`) + `(` + tagS + `))?` +
|
|
`(?:` + regexp.QuoteMeta(`@`) + `(` + digestS + `))?$`)
|
|
ocidirRE = regexp.MustCompile(`^(` + pathS + `)` +
|
|
`(?:` + regexp.QuoteMeta(`:`) + `(` + tagS + `))?` +
|
|
`(?:` + regexp.QuoteMeta(`@`) + `(` + digestS + `))?$`)
|
|
)
|
|
|
|
// Ref is a reference to a registry/repository.
|
|
// Direct access to the contents of this struct should not be assumed.
|
|
type Ref struct {
|
|
Scheme string // Scheme is the type of reference, "reg" or "ocidir".
|
|
Reference string // Reference is the unparsed string or common name.
|
|
Registry string // Registry is the server for the "reg" scheme.
|
|
Repository string // Repository is the path on the registry for the "reg" scheme.
|
|
Tag string // Tag is a mutable tag for a reference.
|
|
Digest string // Digest is an immutable hash for a reference.
|
|
Path string // Path is the directory of the OCI Layout for "ocidir".
|
|
}
|
|
|
|
// New returns a reference based on the scheme (defaulting to "reg").
|
|
func New(parse string) (Ref, error) {
|
|
scheme := ""
|
|
tail := parse
|
|
matchScheme := schemeRE.FindStringSubmatch(parse)
|
|
if len(matchScheme) == 3 {
|
|
scheme = matchScheme[1]
|
|
tail = matchScheme[2]
|
|
}
|
|
ret := Ref{
|
|
Scheme: scheme,
|
|
Reference: parse,
|
|
}
|
|
switch scheme {
|
|
case "":
|
|
ret.Scheme = "reg"
|
|
matchRef := refRE.FindStringSubmatch(tail)
|
|
if len(matchRef) < 5 {
|
|
if refRE.FindStringSubmatch(strings.ToLower(tail)) != nil {
|
|
return Ref{}, fmt.Errorf("%w \"%s\", repo must be lowercase", errs.ErrInvalidReference, tail)
|
|
}
|
|
return Ref{}, fmt.Errorf("%w \"%s\"", errs.ErrInvalidReference, tail)
|
|
}
|
|
ret.Registry = matchRef[1]
|
|
ret.Repository = matchRef[2]
|
|
ret.Tag = matchRef[3]
|
|
ret.Digest = matchRef[4]
|
|
|
|
// handle localhost use case since it matches the regex for a repo path entry
|
|
repoPath := strings.Split(ret.Repository, "/")
|
|
if ret.Registry == "" && repoPath[0] == "localhost" {
|
|
ret.Registry = repoPath[0]
|
|
ret.Repository = strings.Join(repoPath[1:], "/")
|
|
}
|
|
switch ret.Registry {
|
|
case "", dockerRegistryDNS, dockerRegistryLegacy:
|
|
ret.Registry = dockerRegistry
|
|
}
|
|
if ret.Registry == dockerRegistry && !strings.Contains(ret.Repository, "/") {
|
|
ret.Repository = dockerLibrary + "/" + ret.Repository
|
|
}
|
|
if ret.Tag == "" && ret.Digest == "" {
|
|
ret.Tag = "latest"
|
|
}
|
|
if ret.Repository == "" {
|
|
return Ref{}, fmt.Errorf("%w \"%s\"", errs.ErrInvalidReference, tail)
|
|
}
|
|
|
|
case "ocidir", "ocifile":
|
|
matchPath := ocidirRE.FindStringSubmatch(tail)
|
|
if len(matchPath) < 2 || matchPath[1] == "" {
|
|
return Ref{}, fmt.Errorf("%w, invalid path for scheme \"%s\": %s", errs.ErrInvalidReference, scheme, tail)
|
|
}
|
|
ret.Path = matchPath[1]
|
|
if len(matchPath) > 2 && matchPath[2] != "" {
|
|
ret.Tag = matchPath[2]
|
|
}
|
|
if len(matchPath) > 3 && matchPath[3] != "" {
|
|
ret.Digest = matchPath[3]
|
|
}
|
|
|
|
default:
|
|
return Ref{}, fmt.Errorf("%w, unknown scheme \"%s\" in \"%s\"", errs.ErrInvalidReference, scheme, parse)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// NewHost returns a Reg for a registry hostname or equivalent.
|
|
// The ocidir schema equivalent is the path.
|
|
func NewHost(parse string) (Ref, error) {
|
|
scheme := ""
|
|
tail := parse
|
|
matchScheme := schemeRE.FindStringSubmatch(parse)
|
|
if len(matchScheme) == 3 {
|
|
scheme = matchScheme[1]
|
|
tail = matchScheme[2]
|
|
}
|
|
ret := Ref{
|
|
Scheme: scheme,
|
|
}
|
|
|
|
switch scheme {
|
|
case "":
|
|
ret.Scheme = "reg"
|
|
matchReg := registryRE.FindStringSubmatch(tail)
|
|
if len(matchReg) < 2 {
|
|
return Ref{}, fmt.Errorf("%w \"%s\"", errs.ErrParsingFailed, tail)
|
|
}
|
|
ret.Registry = matchReg[1]
|
|
if ret.Registry == "" {
|
|
return Ref{}, fmt.Errorf("%w \"%s\"", errs.ErrParsingFailed, tail)
|
|
}
|
|
|
|
case "ocidir", "ocifile":
|
|
matchPath := ocidirRE.FindStringSubmatch(tail)
|
|
if len(matchPath) < 2 || matchPath[1] == "" {
|
|
return Ref{}, fmt.Errorf("%w, invalid path for scheme \"%s\": %s", errs.ErrParsingFailed, scheme, tail)
|
|
}
|
|
ret.Path = matchPath[1]
|
|
|
|
default:
|
|
return Ref{}, fmt.Errorf("%w, unknown scheme \"%s\" in \"%s\"", errs.ErrParsingFailed, scheme, parse)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// AddDigest returns a ref with the requested digest set.
|
|
// The tag will NOT be unset and the reference value will be reset.
|
|
func (r Ref) AddDigest(digest string) Ref {
|
|
r.Digest = digest
|
|
r.Reference = r.CommonName()
|
|
return r
|
|
}
|
|
|
|
// CommonName outputs a parsable name from a reference.
|
|
func (r Ref) CommonName() string {
|
|
cn := ""
|
|
switch r.Scheme {
|
|
case "reg":
|
|
if r.Registry != "" {
|
|
cn = r.Registry + "/"
|
|
}
|
|
if r.Repository == "" {
|
|
return ""
|
|
}
|
|
cn = cn + r.Repository
|
|
if r.Tag != "" {
|
|
cn = cn + ":" + r.Tag
|
|
}
|
|
if r.Digest != "" {
|
|
cn = cn + "@" + r.Digest
|
|
}
|
|
case "ocidir":
|
|
cn = fmt.Sprintf("ocidir://%s", r.Path)
|
|
if r.Tag != "" {
|
|
cn = cn + ":" + r.Tag
|
|
}
|
|
if r.Digest != "" {
|
|
cn = cn + "@" + r.Digest
|
|
}
|
|
}
|
|
return cn
|
|
}
|
|
|
|
// IsSet returns true if needed values are defined for a specific reference.
|
|
func (r Ref) IsSet() bool {
|
|
if !r.IsSetRepo() {
|
|
return false
|
|
}
|
|
// Registry requires a tag or digest, OCI Layout doesn't require these.
|
|
if r.Scheme == "reg" && r.Tag == "" && r.Digest == "" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IsSetRepo returns true when the ref includes values for a specific repository.
|
|
func (r Ref) IsSetRepo() bool {
|
|
switch r.Scheme {
|
|
case "reg":
|
|
if r.Registry != "" && r.Repository != "" {
|
|
return true
|
|
}
|
|
case "ocidir":
|
|
if r.Path != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsZero returns true if ref is unset.
|
|
func (r Ref) IsZero() bool {
|
|
if r.Scheme == "" && r.Registry == "" && r.Repository == "" && r.Path == "" && r.Tag == "" && r.Digest == "" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SetDigest returns a ref with the requested digest set.
|
|
// The tag will be unset and the reference value will be reset.
|
|
func (r Ref) SetDigest(digest string) Ref {
|
|
r.Digest = digest
|
|
r.Tag = ""
|
|
r.Reference = r.CommonName()
|
|
return r
|
|
}
|
|
|
|
// SetTag returns a ref with the requested tag set.
|
|
// The digest will be unset and the reference value will be reset.
|
|
func (r Ref) SetTag(tag string) Ref {
|
|
r.Tag = tag
|
|
r.Digest = ""
|
|
r.Reference = r.CommonName()
|
|
return r
|
|
}
|
|
|
|
// ToReg converts a reference to a registry like syntax.
|
|
func (r Ref) ToReg() Ref {
|
|
switch r.Scheme {
|
|
case "ocidir":
|
|
r.Scheme = "reg"
|
|
r.Registry = "localhost"
|
|
// clean the path to strip leading ".."
|
|
r.Repository = path.Clean("/" + r.Path)[1:]
|
|
r.Repository = strings.ToLower(r.Repository)
|
|
// convert any unsupported characters to "-" in the path
|
|
re := regexp.MustCompile(`[^/a-z0-9]+`)
|
|
r.Repository = string(re.ReplaceAll([]byte(r.Repository), []byte("-")))
|
|
}
|
|
return r
|
|
}
|
|
|
|
// EqualRegistry compares the registry between two references.
|
|
func EqualRegistry(a, b Ref) bool {
|
|
if a.Scheme != b.Scheme {
|
|
return false
|
|
}
|
|
switch a.Scheme {
|
|
case "reg":
|
|
return a.Registry == b.Registry
|
|
case "ocidir":
|
|
return a.Path == b.Path
|
|
case "":
|
|
// both undefined
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// EqualRepository compares the repository between two references.
|
|
func EqualRepository(a, b Ref) bool {
|
|
if a.Scheme != b.Scheme {
|
|
return false
|
|
}
|
|
switch a.Scheme {
|
|
case "reg":
|
|
return a.Registry == b.Registry && a.Repository == b.Repository
|
|
case "ocidir":
|
|
return a.Path == b.Path
|
|
case "":
|
|
// both undefined
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|