Skip to content

Commit

Permalink
internal/mod/modresolve: new package
Browse files Browse the repository at this point in the history
This provides the heart of the CUE_REGISTRY routing logic
and will later be plumbed into an OCI client implementation.

Signed-off-by: Roger Peppe <[email protected]>
Change-Id: Ia7e9a30efff4cd5e5878d27dc482a06fcc6521ed
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1170811
Unity-Result: CUE porcuepine <[email protected]>
Reviewed-by: Daniel Martí <[email protected]>
TryBot-Result: CUEcueckoo <[email protected]>
  • Loading branch information
rogpeppe committed Oct 18, 2023
1 parent c6da768 commit 2a7d1d6
Show file tree
Hide file tree
Showing 2 changed files with 396 additions and 0 deletions.
211 changes: 211 additions & 0 deletions internal/mod/modresolve/resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package modresolve

import (
"fmt"
"net"
"strings"

"cuelabs.dev/go/oci/ociregistry/ociref"

"cuelang.org/go/internal/mod/module"
)

// Resolve resolves a module path (a.k.a. OCI repository name) to the
// location for that path. Invalid paths will map to the default location.
type Resolver interface {
Resolve(path string) Location
}

// Location represents the location for a given path.
type Location struct {
// Host holds the host or host:port of the registry.
Host string
// Prefix holds a prefix to be added to the path.
Prefix string
// Insecure holds whether an insecure connection
// should be used when connecting to the registry.
Insecure bool
}

// ParseCUERegistry parses a registry routing specification that
// maps module prefixes to the registry that should be used to
// fetch that module.
//
// The specification consists of an order-independent, comma-separated list.
//
// Each element either maps a module prefix to the registry that will be used
// for all modules that have that prefix (prefix=registry), or a catch-all registry to be used
// for modules that do not match any prefix (registry).
//
// For example:
//
// myorg.com=myregistry.com/m,catchallregistry.example.org
//
// Any module with a matching prefix will be routed to the given registry.
// A prefix only matches whole path elements.
// In the above example, module myorg.com/foo/bar@v0 will be looked up
// in myregistry.com in the repository m/myorg.com/foo/bar,
// whereas github.com/x/y will be looked up in catchallregistry.example.com.
//
// The registry part is syntactically similar to a [docker reference]
// except that the repository is optional and no tag or digest is allowed.
// Additionally, a +secure or +insecure suffix may be used to indicate
// whether to use a secure or insecure connection. Without that,
// localhost, 127.0.0.1 and [::1] will default to insecure, and anything
// else to secure.
//
// If s does not declare a catch-all registry location, catchAllDefault is
// used. It is an error if s fails to declares a catch-all registry location
// and no catchAllDefault is provided.
//
// [docker reference]: https://pkg.go.dev/github.com/distribution/reference
func ParseCUERegistry(s string, catchAllDefault string) (Resolver, error) {
if s == "" && catchAllDefault == "" {
return nil, fmt.Errorf("no catch-all registry or default")
}
locs := make(map[string]Location)
if s == "" {
s = catchAllDefault
}
parts := strings.Split(s, ",")
for _, part := range parts {
key, val, ok := strings.Cut(part, "=")
if !ok {
if part == "" {
// TODO or just ignore it?
return nil, fmt.Errorf("empty registry part")
}
if _, ok := locs[""]; ok {
return nil, fmt.Errorf("duplicate catch-all registry")
}
key, val = "", part
} else {
if key == "" {
return nil, fmt.Errorf("empty module prefix")
}
if val == "" {
return nil, fmt.Errorf("empty registry reference")
}
if err := module.CheckPathWithoutVersion(key); err != nil {
return nil, fmt.Errorf("invalid module path %q: %v", key, err)
}
if _, ok := locs[key]; ok {
return nil, fmt.Errorf("duplicate module prefix %q", key)
}
}
loc, err := parseRegistry(val)
if err != nil {
return nil, fmt.Errorf("invalid registry %q: %v", val, err)
}
locs[key] = loc
}
if _, ok := locs[""]; !ok {
if catchAllDefault == "" {
return nil, fmt.Errorf("no default catch-all registry provided")
}
loc, err := parseRegistry(catchAllDefault)
if err != nil {
return nil, fmt.Errorf("invalid catch-all registry %q: %v", catchAllDefault, err)
}
locs[""] = loc
}
return &resolver{
locs: locs,
}, nil
}

type resolver struct {
locs map[string]Location
}

func (r *resolver) Resolve(path string) Location {
if path == "" {
return r.locs[""]
}
bestMatch := ""
// Note: there's always a wildcard match.
bestMatchLoc := r.locs[""]
for pat, loc := range r.locs {
if pat == path {
return loc
}
if !strings.HasPrefix(path, pat) {
continue
}
if len(bestMatch) > len(pat) {
// We've already found a more specific match.
continue
}
if path[len(pat)] != '/' {
// The path doesn't have a separator at the end of
// the prefix, which means that it doesn't match.
// For example, foo.com/bar does not match foo.com/ba.
continue
}
// It's a possible match but not necessarily the longest one.
bestMatch, bestMatchLoc = pat, loc
}
return bestMatchLoc
}

func parseRegistry(env string) (Location, error) {
var suffix string
if i := strings.LastIndex(env, "+"); i > 0 {
suffix = env[i:]
env = env[:i]
}
var r ociref.Reference
if !strings.Contains(env, "/") {
// OCI references don't allow a host name on its own without a repo,
// but we do.
r.Host = env
if !ociref.IsValidHost(r.Host) {
return Location{}, fmt.Errorf("invalid host name %q in registry", r.Host)
}
} else {
var err error
r, err = ociref.Parse(env)
if err != nil {
return Location{}, err
}
if r.Tag != "" || r.Digest != "" {
return Location{}, fmt.Errorf("cannot have an associated tag or digest")
}
}
if suffix == "" {
if isInsecureHost(r.Host) {
suffix = "+insecure"
} else {
suffix = "+secure"
}
}
insecure := false
switch suffix {
case "+insecure":
insecure = true
case "+secure":
default:
return Location{}, fmt.Errorf("unknown suffix (%q), need +insecure, +secure or no suffix)", suffix)
}
return Location{
Host: r.Host,
Prefix: r.Repository,
Insecure: insecure,
}, nil
}

func isInsecureHost(hostPort string) bool {
host, _, err := net.SplitHostPort(hostPort)
if err != nil {
host = hostPort
}
switch host {
case "localhost",
"127.0.0.1",
"::1", "[::1]":
return true
}
// TODO other clients have logic for RFC1918 too, amongst other
// things. Maybe we should do that too.
return false
}
185 changes: 185 additions & 0 deletions internal/mod/modresolve/resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package modresolve

import (
"testing"

"github.com/go-quicktest/qt"
)

func TestResolver(t *testing.T) {
testCases := []struct {
testName string
in string
catchAllDefault string
err string
lookups map[string]Location
}{{
testName: "MultipleFallbacks",
in: "registry.somewhere,registry.other",
err: "duplicate catch-all registry",
}, {
testName: "NoRegistryOrDefault",
catchAllDefault: "",
err: "no catch-all registry or default",
}, {
testName: "InvalidRegistry",
in: "$#foo",
err: `invalid registry "\$#foo": invalid host name "\$#foo" in registry`,
}, {
testName: "InvalidSecuritySuffix",
in: "foo.com+bogus",
err: `invalid registry "foo.com\+bogus": unknown suffix \("\+bogus"\), need \+insecure, \+secure or no suffix\)`,
}, {
testName: "IPV6AddrWithoutBrackets",
in: "::1",
err: `invalid registry "::1": invalid host name "::1" in registry`,
}, {
testName: "EmptyElement",
in: "foo.com,",
err: `empty registry part`,
}, {
testName: "MissingPrefix",
in: "=foo.com",
err: `empty module prefix`,
}, {
testName: "MissingRegistry",
in: "x.com=",
err: `empty registry reference`,
}, {
testName: "InvalidModulePrefix",
in: "foo#=foo.com",
err: `invalid module path "foo#": invalid char '#'`,
}, {
testName: "DuplicateModulePrefix",
in: "x.com=r.org,x.com=q.org",
err: `duplicate module prefix "x.com"`,
}, {
testName: "NoDefaultCatchAll",
in: "x.com=r.org",
err: `no default catch-all registry provided`,
}, {
testName: "InvalidCatchAll",
in: "x.com=r.org",
catchAllDefault: "bogus",
err: `invalid catch-all registry "bogus": invalid host name "bogus" in registry`,
}, {
testName: "InvalidRegistryRef",
in: "foo.com//bar",
err: `invalid registry "foo.com//bar": invalid reference syntax \("foo.com//bar"\)`,
}, {
testName: "RegistryRefWithDigest",
in: "foo.com/bar@sha256:f3c16f525a1b7c204fc953d6d7db7168d84ebf4902f83c3a37d113b18c28981f",
err: `invalid registry "foo.com/bar@sha256:f3c16f525a1b7c204fc953d6d7db7168d84ebf4902f83c3a37d113b18c28981f": cannot have an associated tag or digest`,
}, {
testName: "RegistryRefWithTag",
in: "foo.com/bar:sometag",
err: `invalid registry "foo.com/bar:sometag": cannot have an associated tag or digest`,
}, {
testName: "SingleCatchAll",
catchAllDefault: "registry.somewhere",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "registry.somewhere",
},
},
}, {
testName: "CatchAllWithNoDefault",
in: "registry.somewhere",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "registry.somewhere",
},
},
}, {
testName: "CatchAllWithDefault",
in: "registry.somewhere",
catchAllDefault: "other.cue.somewhere",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "registry.somewhere",
},
"": {
Host: "registry.somewhere",
},
},
}, {
testName: "PrefixWithCatchAllNoDefault",
in: "example.com=registry.example.com/offset,registry.somewhere",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "registry.somewhere",
},
"example.com/blah": {
Host: "registry.example.com",
Prefix: "offset",
},
"example.com": {
Host: "registry.example.com",
Prefix: "offset",
},
},
}, {
testName: "PrefixWithCatchAllDefault",
in: "example.com=registry.example.com/offset",
catchAllDefault: "registry.somewhere",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "registry.somewhere",
},
"example.com/blah": {
Host: "registry.example.com",
Prefix: "offset",
},
},
}, {
testName: "LocalhostIsInsecure",
in: "localhost:5000",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "localhost:5000",
Insecure: true,
},
},
}, {
testName: "SecureLocalhost",
in: "localhost:1234+secure",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "localhost:1234",
},
},
}, {
testName: "127.0.0.1IsInsecure",
in: "127.0.0.1",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "127.0.0.1",
Insecure: true,
},
},
}, {
testName: "[::1]IsInsecure",
in: "[::1]",
lookups: map[string]Location{
"fruit.com/apple": {
Host: "[::1]",
Insecure: true,
},
},
}}

for _, tc := range testCases {
t.Run(tc.testName, func(t *testing.T) {
r, err := ParseCUERegistry(tc.in, tc.catchAllDefault)
if tc.err != "" {
qt.Assert(t, qt.ErrorMatches(err, tc.err))
return
}
qt.Assert(t, qt.IsNil(err))
for prefix, want := range tc.lookups {
got := r.Resolve(prefix)
qt.Assert(t, qt.Equals(got, want), qt.Commentf("prefix %q", prefix))
}
})
}
}

0 comments on commit 2a7d1d6

Please sign in to comment.