werkdock import ARCHIVE [REPOSITORY[:TAG]] creates an image from a plain rootfs archive (docker import semantics; the store method is renamed Import). load is docker load's slot for saved docker/OCI image archives, not implemented yet: it refuses without --name and keeps accepting a rootfs archive with --name for callers that predate import (Werkator), with a note. Import recognizes an unpacked docker save or OCI layout and refuses it, so the two do not blur. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
79 lines
2.6 KiB
Go
79 lines
2.6 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Reference names an image the docker/OCI way: [registry/]repository[:tag].
|
|
// The tag defaults to "latest", exactly as docker resolves a bare name.
|
|
type Reference struct {
|
|
Repository string
|
|
Tag string
|
|
}
|
|
|
|
const defaultTag = "latest"
|
|
|
|
var (
|
|
// componentRe is one path component of a repository (docker's
|
|
// grammar, with the separator runs simplified).
|
|
componentRe = regexp.MustCompile(`^[a-z0-9]+(?:[._-]+[a-z0-9]+)*$`)
|
|
// registryRe is a host name with an optional port, allowed as the
|
|
// first component only.
|
|
registryRe = regexp.MustCompile(`^[a-z0-9]+(?:[.-][a-z0-9]+)*(?::[0-9]+)?$`)
|
|
tagRe = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$`)
|
|
)
|
|
|
|
// ParseReference validates and normalizes a reference string.
|
|
func ParseReference(s string) (Reference, error) {
|
|
if strings.Contains(s, "@") {
|
|
return Reference{}, fmt.Errorf("invalid image reference %q: digests are not supported, use a tag", s)
|
|
}
|
|
repo, tag := s, defaultTag
|
|
// A colon after the last slash separates the tag; one before it is a
|
|
// registry port.
|
|
if i := strings.LastIndex(s, ":"); i > strings.LastIndex(s, "/") {
|
|
repo, tag = s[:i], s[i+1:]
|
|
}
|
|
if repo == "" {
|
|
return Reference{}, fmt.Errorf("invalid image reference %q: empty repository", s)
|
|
}
|
|
components := strings.Split(repo, "/")
|
|
for i, c := range components {
|
|
if componentRe.MatchString(c) {
|
|
continue
|
|
}
|
|
if i == 0 && len(components) > 1 && registryRe.MatchString(c) {
|
|
continue
|
|
}
|
|
return Reference{}, fmt.Errorf("invalid image reference %q: repository component %q (allowed: lowercase letters, digits, '.', '_', '-' between them)", s, c)
|
|
}
|
|
if !tagRe.MatchString(tag) {
|
|
return Reference{}, fmt.Errorf("invalid image reference %q: tag %q (allowed: letters, digits, '_', '.', '-')", s, tag)
|
|
}
|
|
// ".tmp" is the staging suffix of Import — a tag ending in it would
|
|
// collide with interrupted imports.
|
|
if strings.HasSuffix(tag, ".tmp") {
|
|
return Reference{}, fmt.Errorf("invalid image reference %q: the .tmp tag suffix is reserved for staging", s)
|
|
}
|
|
return Reference{Repository: repo, Tag: tag}, nil
|
|
}
|
|
|
|
// String is the full form, repository:tag.
|
|
func (r Reference) String() string { return r.Repository + ":" + r.Tag }
|
|
|
|
// Short omits the default tag, the way a user typically writes it.
|
|
func (r Reference) Short() string {
|
|
if r.Tag == defaultTag {
|
|
return r.Repository
|
|
}
|
|
return r.String()
|
|
}
|
|
|
|
// legacyName reports whether the reference could have been an image of
|
|
// the flat pre-tag store layout (a bare name, no registry or namespace).
|
|
func (r Reference) legacyName() bool {
|
|
return r.Tag == defaultTag && !strings.Contains(r.Repository, "/")
|
|
}
|