|
@@ -24,6 +24,7 @@ import (
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
"runtime"
|
|
|
+ "slices"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"sync/atomic"
|
|
@@ -53,7 +54,7 @@ var (
|
|
|
|
|
|
// ErrMissingModel is returned when the model part of a name is missing
|
|
|
// or invalid.
|
|
|
- ErrNameInvalid = errors.New("invalid name; must be in the form {scheme://}{host/}{namespace/}[model]{:tag}{@digest}")
|
|
|
+ ErrNameInvalid = errors.New("invalid or missing name")
|
|
|
|
|
|
// ErrCached is passed to [Trace.PushUpdate] when a layer already
|
|
|
// exists. It is a non-fatal error and is never returned by [Registry.Push].
|
|
@@ -205,10 +206,18 @@ type Registry struct {
|
|
|
// It is only used when a layer is larger than [MaxChunkingThreshold].
|
|
|
MaxChunkSize int64
|
|
|
|
|
|
- // NameMask, if set, is the name used to convert non-fully qualified
|
|
|
+ // Mask, if set, is the name used to convert non-fully qualified
|
|
|
// names to fully qualified names. If empty, the default mask
|
|
|
// ("registry.ollama.ai/library/_:latest") is used.
|
|
|
- NameMask string
|
|
|
+ Mask string
|
|
|
+}
|
|
|
+
|
|
|
+func (r *Registry) completeName(name string) names.Name {
|
|
|
+ mask := defaultMask
|
|
|
+ if r.Mask != "" {
|
|
|
+ mask = names.Parse(r.Mask)
|
|
|
+ }
|
|
|
+ return names.Merge(names.Parse(name), mask)
|
|
|
}
|
|
|
|
|
|
// DefaultRegistry returns a new Registry configured from the environment. The
|
|
@@ -243,52 +252,6 @@ func DefaultRegistry() (*Registry, error) {
|
|
|
return &rc, nil
|
|
|
}
|
|
|
|
|
|
-type PushParams struct {
|
|
|
- // From is an optional destination name for the model. If empty, the
|
|
|
- // destination name is the same as the source name.
|
|
|
- From string
|
|
|
-}
|
|
|
-
|
|
|
-// parseName parses name using [names.ParseExtended] and then merges the name with the
|
|
|
-// default name, and checks that the name is fully qualified. If a digest is
|
|
|
-// present, it parse and returns it with the other fields as their zero values.
|
|
|
-//
|
|
|
-// It returns an error if the name is not fully qualified, or if the digest, if
|
|
|
-// any, is invalid.
|
|
|
-//
|
|
|
-// The scheme is returned as provided by [names.ParseExtended].
|
|
|
-func parseName(s, mask string) (scheme string, n names.Name, d blob.Digest, err error) {
|
|
|
- maskName := defaultMask
|
|
|
- if mask != "" {
|
|
|
- maskName = names.Parse(mask)
|
|
|
- if !maskName.IsFullyQualified() {
|
|
|
- return "", names.Name{}, blob.Digest{}, fmt.Errorf("invalid name mask: %s", mask)
|
|
|
- }
|
|
|
- }
|
|
|
- scheme, n, ds := names.ParseExtended(s)
|
|
|
- if !n.IsValid() {
|
|
|
- return "", names.Name{}, blob.Digest{}, fmt.Errorf("%w: %q", ErrNameInvalid, s)
|
|
|
- }
|
|
|
- n = names.Merge(n, maskName)
|
|
|
- if ds != "" {
|
|
|
- // Digest is present. Validate it.
|
|
|
- d, err = blob.ParseDigest(ds)
|
|
|
- if err != nil {
|
|
|
- return "", names.Name{}, blob.Digest{}, err
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // The name check is deferred until after the digest check because we
|
|
|
- // say that digests take precedence over names, and so should there
|
|
|
- // errors when being parsed.
|
|
|
- if !n.IsFullyQualified() {
|
|
|
- return "", names.Name{}, blob.Digest{}, fmt.Errorf("%w: %q", ErrNameInvalid, s)
|
|
|
- }
|
|
|
-
|
|
|
- scheme = cmp.Or(scheme, "https")
|
|
|
- return scheme, n, d, nil
|
|
|
-}
|
|
|
-
|
|
|
func (r *Registry) maxStreams() int {
|
|
|
n := cmp.Or(r.MaxStreams, runtime.GOMAXPROCS(0))
|
|
|
|
|
@@ -308,6 +271,12 @@ func (r *Registry) maxChunkSize() int64 {
|
|
|
return cmp.Or(r.MaxChunkSize, DefaultMaxChunkSize)
|
|
|
}
|
|
|
|
|
|
+type PushParams struct {
|
|
|
+ // From is an optional destination name for the model. If empty, the
|
|
|
+ // destination name is the same as the source name.
|
|
|
+ From string
|
|
|
+}
|
|
|
+
|
|
|
// Push pushes the model with the name in the cache to the remote registry.
|
|
|
func (r *Registry) Push(ctx context.Context, c *blob.DiskCache, name string, p *PushParams) error {
|
|
|
if p == nil {
|
|
@@ -337,7 +306,7 @@ func (r *Registry) Push(ctx context.Context, c *blob.DiskCache, name string, p *
|
|
|
|
|
|
t := traceFromContext(ctx)
|
|
|
|
|
|
- scheme, n, _, err := parseName(name, r.NameMask)
|
|
|
+ scheme, n, _, err := parseName(name, r.Mask)
|
|
|
if err != nil {
|
|
|
// This should never happen since ResolveLocal should have
|
|
|
// already validated the name.
|
|
@@ -431,7 +400,7 @@ func canRetry(err error) bool {
|
|
|
// typically slower than splitting the model up across layers, and is mostly
|
|
|
// utilized for layers of type equal to "application/vnd.ollama.image".
|
|
|
func (r *Registry) Pull(ctx context.Context, c *blob.DiskCache, name string) error {
|
|
|
- scheme, n, _, err := parseName(name, r.NameMask)
|
|
|
+ scheme, n, _, err := parseName(name, r.Mask)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
@@ -582,9 +551,9 @@ func (r *Registry) Pull(ctx context.Context, c *blob.DiskCache, name string) err
|
|
|
// Unlink is like [blob.DiskCache.Unlink], but makes name fully qualified
|
|
|
// before attempting to unlink the model.
|
|
|
func (r *Registry) Unlink(c *blob.DiskCache, name string) (ok bool, _ error) {
|
|
|
- _, n, _, err := parseName(name, r.NameMask)
|
|
|
- if err != nil {
|
|
|
- return false, err
|
|
|
+ n := r.completeName(name)
|
|
|
+ if !n.IsFullyQualified() {
|
|
|
+ return false, fmt.Errorf("%w: %q", ErrNameInvalid, name)
|
|
|
}
|
|
|
return c.Unlink(n.String())
|
|
|
}
|
|
@@ -658,9 +627,9 @@ type Layer struct {
|
|
|
}
|
|
|
|
|
|
// ResolveLocal resolves a name to a Manifest in the local cache. The name is
|
|
|
-// parsed using [names.ParseExtended] but the scheme is ignored.
|
|
|
+// parsed using [names.Split] but the scheme is ignored.
|
|
|
func (r *Registry) ResolveLocal(c *blob.DiskCache, name string) (*Manifest, error) {
|
|
|
- _, n, d, err := parseName(name, r.NameMask)
|
|
|
+ _, n, d, err := parseName(name, r.Mask)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
@@ -686,7 +655,7 @@ func (r *Registry) ResolveLocal(c *blob.DiskCache, name string) (*Manifest, erro
|
|
|
|
|
|
// Resolve resolves a name to a Manifest in the remote registry.
|
|
|
func (r *Registry) Resolve(ctx context.Context, name string) (*Manifest, error) {
|
|
|
- scheme, n, d, err := parseName(name, r.NameMask)
|
|
|
+ scheme, n, d, err := parseName(name, r.Mask)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
@@ -869,3 +838,69 @@ func maybeUnexpectedEOF(err error) error {
|
|
|
}
|
|
|
return err
|
|
|
}
|
|
|
+
|
|
|
+type publicError struct {
|
|
|
+ wrapped error
|
|
|
+ message string
|
|
|
+}
|
|
|
+
|
|
|
+func withPublicMessagef(err error, message string, args ...any) error {
|
|
|
+ return publicError{wrapped: err, message: fmt.Sprintf(message, args...)}
|
|
|
+}
|
|
|
+
|
|
|
+func (e publicError) Error() string { return e.message }
|
|
|
+func (e publicError) Unwrap() error { return e.wrapped }
|
|
|
+
|
|
|
+var supportedSchemes = []string{
|
|
|
+ "http",
|
|
|
+ "https",
|
|
|
+ "https+insecure",
|
|
|
+}
|
|
|
+
|
|
|
+var supportedSchemesMessage = fmt.Sprintf("supported schemes are %v", strings.Join(supportedSchemes, ", "))
|
|
|
+
|
|
|
+// parseName parses and validates an extended name, returning the scheme, name,
|
|
|
+// and digest.
|
|
|
+//
|
|
|
+// If the scheme is empty, scheme will be "https". If an unsupported scheme is
|
|
|
+// given, [ErrNameInvalid] wrapped with a display friendly message is returned.
|
|
|
+//
|
|
|
+// If the digest is invalid, [ErrNameInvalid] wrapped with a display friendly
|
|
|
+// message is returned.
|
|
|
+//
|
|
|
+// If the name is not, once merged with the mask, fully qualified,
|
|
|
+// [ErrNameInvalid] wrapped with a display friendly message is returned.
|
|
|
+func parseName(s string, mask string) (scheme string, _ names.Name, _ blob.Digest, _ error) {
|
|
|
+ scheme, name, digest := names.Split(s)
|
|
|
+ scheme = cmp.Or(scheme, "https")
|
|
|
+ if !slices.Contains(supportedSchemes, scheme) {
|
|
|
+ err := withPublicMessagef(ErrNameInvalid, "unsupported scheme: %q: %s", scheme, supportedSchemesMessage)
|
|
|
+ return "", names.Name{}, blob.Digest{}, err
|
|
|
+ }
|
|
|
+
|
|
|
+ var d blob.Digest
|
|
|
+ if digest != "" {
|
|
|
+ var err error
|
|
|
+ d, err = blob.ParseDigest(digest)
|
|
|
+ if err != nil {
|
|
|
+ err = withPublicMessagef(ErrNameInvalid, "invalid digest: %q", digest)
|
|
|
+ return "", names.Name{}, blob.Digest{}, err
|
|
|
+ }
|
|
|
+ if name == "" {
|
|
|
+ // We have can resolve a manifest from a digest only,
|
|
|
+ // so skip name validation and return the scheme and
|
|
|
+ // digest.
|
|
|
+ return scheme, names.Name{}, d, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ maskName := defaultMask
|
|
|
+ if mask != "" {
|
|
|
+ maskName = names.Parse(mask)
|
|
|
+ }
|
|
|
+ n := names.Merge(names.Parse(name), maskName)
|
|
|
+ if !n.IsFullyQualified() {
|
|
|
+ return "", names.Name{}, blob.Digest{}, fmt.Errorf("%w: %q", ErrNameInvalid, s)
|
|
|
+ }
|
|
|
+ return scheme, n, d, nil
|
|
|
+}
|