Skip to content

Commit d5c0745

Browse files
committed
cmd/cue/cmd: support CUE_REGISTRY
The format is similar to a standard docker repository reference, with a couple of differences: - a host name is allowed on its own (a Docker reference always requires a repository name) - a `+insecure` or `+secure` suffix is supported to choose whether the connection is https or http. The default for this is similar to the heuristic used by docker, but a little less involved. The value of this environment variable is ignored unless the modules experimental mode is enabled with `CUE_EXPERIMENT=modules`. For #2330. Signed-off-by: Roger Peppe <[email protected]> Change-Id: I56d57e5bf0d7f8a334538b4093acea817f55b1f5 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1168510 Reviewed-by: Daniel Martí <[email protected]> Unity-Result: CUE porcuepine <[email protected]> TryBot-Result: CUEcueckoo <[email protected]>
1 parent a43792c commit d5c0745

18 files changed

+520
-29
lines changed

cmd/cue/cmd/common.go

+36-21
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,34 @@ import (
4343

4444
var requestedVersion = os.Getenv("CUE_SYNTAX_OVERRIDE")
4545

46-
var defaultConfig = config{
47-
loadCfg: &load.Config{
48-
ParseFile: func(name string, src interface{}) (*ast.File, error) {
49-
version := internal.APIVersionSupported
50-
if requestedVersion != "" {
51-
switch {
52-
case strings.HasPrefix(requestedVersion, "v0.1"):
53-
version = -1000 + 100
46+
func defaultConfig() (*config, error) {
47+
reg, err := getRegistry()
48+
if err != nil {
49+
return nil, err
50+
}
51+
return &config{
52+
loadCfg: &load.Config{
53+
ParseFile: func(name string, src interface{}) (*ast.File, error) {
54+
version := internal.APIVersionSupported
55+
if requestedVersion != "" {
56+
switch {
57+
case strings.HasPrefix(requestedVersion, "v0.1"):
58+
version = -1000 + 100
59+
}
5460
}
55-
}
56-
options := []parser.Option{
57-
parser.FromVersion(version),
58-
parser.ParseComments,
59-
}
60-
// TODO: consolidate all options into a single CUE_DEBUG variable.
61-
if os.Getenv("CUE_DEBUG_PARSER_TRACE") != "" {
62-
options = append(options, parser.Trace)
63-
}
64-
return parser.ParseFile(name, src, options...)
61+
options := []parser.Option{
62+
parser.FromVersion(version),
63+
parser.ParseComments,
64+
}
65+
// TODO: consolidate all options into a single CUE_DEBUG variable.
66+
if os.Getenv("CUE_DEBUG_PARSER_TRACE") != "" {
67+
options = append(options, parser.Trace)
68+
}
69+
return parser.ParseFile(name, src, options...)
70+
},
71+
Registry: reg,
6572
},
66-
},
73+
}, nil
6774
}
6875

6976
var inTest = false
@@ -373,11 +380,19 @@ type config struct {
373380
}
374381

375382
func newBuildPlan(cmd *Command, cfg *config) (p *buildPlan, err error) {
383+
var defCfg *config
384+
if cfg == nil || cfg.loadCfg == nil {
385+
var err error
386+
defCfg, err = defaultConfig()
387+
if err != nil {
388+
return nil, err
389+
}
390+
}
376391
if cfg == nil {
377-
cfg = &defaultConfig
392+
cfg = defCfg
378393
}
379394
if cfg.loadCfg == nil {
380-
cfg.loadCfg = defaultConfig.loadCfg
395+
cfg.loadCfg = defCfg.loadCfg
381396
}
382397
cfg.loadCfg.Stdin = cmd.InOrStdin()
383398

cmd/cue/cmd/registry.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os"
7+
"strings"
8+
9+
"cuelabs.dev/go/oci/ociregistry"
10+
"cuelabs.dev/go/oci/ociregistry/ociclient"
11+
"cuelabs.dev/go/oci/ociregistry/ocifilter"
12+
"cuelabs.dev/go/oci/ociregistry/ociref"
13+
14+
"cuelang.org/go/internal/cueexperiment"
15+
)
16+
17+
func getRegistry() (ociregistry.Interface, error) {
18+
// TODO document CUE_REGISTRY via a new "cue help environment" subcommand.
19+
env := os.Getenv("CUE_REGISTRY")
20+
if !cueexperiment.Flags.Modules {
21+
if env != "" {
22+
fmt.Fprintf(os.Stderr, "warning: ignoring CUE_REGISTRY because modules experiment is not enabled. Set CUE_EXPERIMENT=modules to enable it.\n")
23+
}
24+
return nil, nil
25+
}
26+
if env == "" {
27+
env = "registry.cuelabs.dev"
28+
}
29+
30+
host, prefix, insecure, err := parseRegistry(env)
31+
if err != nil {
32+
return nil, err
33+
}
34+
r, err := ociclient.New(host, &ociclient.Options{
35+
Insecure: insecure,
36+
})
37+
if err != nil {
38+
return nil, fmt.Errorf("cannot make OCI client: %v", err)
39+
}
40+
if prefix != "" {
41+
r = ocifilter.Sub(r, prefix)
42+
}
43+
return r, nil
44+
}
45+
46+
func parseRegistry(env string) (hostPort, prefix string, insecure bool, err error) {
47+
var suffix string
48+
if i := strings.LastIndex(env, "+"); i > 0 {
49+
suffix = env[i:]
50+
env = env[:i]
51+
}
52+
var r ociref.Reference
53+
if !strings.Contains(env, "/") {
54+
// OCI references don't allow a host name on its own without a repo,
55+
// but we do.
56+
r.Host = env
57+
if !ociref.IsValidHost(r.Host) {
58+
return "", "", false, fmt.Errorf("$CUE_REGISTRY %q is not a valid host name", r.Host)
59+
}
60+
} else {
61+
var err error
62+
r, err = ociref.Parse(env)
63+
if err != nil {
64+
return "", "", false, fmt.Errorf("cannot parse $CUE_REGISTRY: %v", err)
65+
}
66+
if r.Tag != "" || r.Digest != "" {
67+
return "", "", false, fmt.Errorf("$CUE_REGISTRY %q cannot have an associated tag or digest", env)
68+
}
69+
}
70+
if suffix == "" {
71+
if isInsecureHost(r.Host) {
72+
suffix = "+insecure"
73+
} else {
74+
suffix = "+secure"
75+
}
76+
}
77+
switch suffix {
78+
case "+insecure":
79+
insecure = true
80+
case "+secure":
81+
default:
82+
return "", "", false, fmt.Errorf("unknown suffix (%q) to CUE_REGISTRY (need +insecure or +secure)", suffix)
83+
}
84+
return r.Host, r.Repository, insecure, nil
85+
}
86+
87+
func isInsecureHost(hostPort string) bool {
88+
host, _, err := net.SplitHostPort(hostPort)
89+
if err != nil {
90+
host = hostPort
91+
}
92+
switch host {
93+
case "localhost",
94+
"127.0.0.1",
95+
"::1":
96+
return true
97+
}
98+
// TODO other clients have logic for RFC1918 too, amongst other
99+
// things. Maybe we should do that too.
100+
return false
101+
}

cmd/cue/cmd/registry_test.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/go-quicktest/qt"
7+
8+
"cuelang.org/go/internal/tdtest"
9+
)
10+
11+
type registryTest struct {
12+
registry string
13+
wantHost string
14+
wantPrefix string
15+
wantInsecure bool
16+
wantError string
17+
}
18+
19+
var parseRegistryTests = []registryTest{{
20+
registry: "registry.cuelabs.dev",
21+
wantHost: "registry.cuelabs.dev",
22+
}, {
23+
registry: "registry.cuelabs.dev+insecure",
24+
wantHost: "registry.cuelabs.dev",
25+
wantInsecure: true,
26+
}, {
27+
registry: "foo.com/bar/baz",
28+
wantHost: "foo.com",
29+
wantPrefix: "bar/baz",
30+
wantInsecure: false,
31+
}, {
32+
registry: "localhost:8080/blah",
33+
wantHost: "localhost:8080",
34+
wantPrefix: "blah",
35+
wantInsecure: true,
36+
}, {
37+
registry: "localhost/blah",
38+
wantError: `cannot parse \$CUE_REGISTRY: reference does not contain host name`,
39+
}, {
40+
registry: "127.0.0.1/blah",
41+
wantHost: "127.0.0.1",
42+
wantPrefix: "blah",
43+
wantInsecure: true,
44+
}, {
45+
registry: "localhost:1324",
46+
wantHost: "localhost:1324",
47+
wantInsecure: true,
48+
}, {
49+
registry: "foo.com/bar:1324",
50+
wantError: `\$CUE_REGISTRY "foo.com/bar:1324" cannot have an associated tag or digest`,
51+
}, {
52+
registry: "foo.com/bar@sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
53+
wantError: `\$CUE_REGISTRY "foo.com/bar@sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" cannot have an associated tag or digest`,
54+
}, {
55+
registry: "foo.com/bar:blah@sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
56+
wantError: `\$CUE_REGISTRY "foo.com/bar:blah@sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" cannot have an associated tag or digest`,
57+
}, {
58+
registry: "foo.com/bar+baz",
59+
wantError: `unknown suffix \("\+baz"\) to CUE_REGISTRY \(need \+insecure or \+secure\)`,
60+
}, {
61+
registry: "badhost",
62+
wantError: `\$CUE_REGISTRY "badhost" is not a valid host name`,
63+
}}
64+
65+
func TestParseRegistry(t *testing.T) {
66+
tdtest.Run(t, parseRegistryTests, func(t *tdtest.T, test *registryTest) {
67+
host, prefix, insecure, err := parseRegistry(test.registry)
68+
if test.wantError != "" {
69+
qt.Assert(t, qt.ErrorMatches(err, test.wantError))
70+
return
71+
}
72+
qt.Assert(t, qt.IsNil(err))
73+
t.Equal(host, test.wantHost)
74+
t.Equal(prefix, test.wantPrefix)
75+
t.Equal(insecure, test.wantInsecure)
76+
})
77+
}

cmd/cue/cmd/script_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"cuelang.org/go/cue/errors"
3838
"cuelang.org/go/cue/parser"
3939
"cuelang.org/go/internal/cuetest"
40+
"cuelang.org/go/internal/registrytest"
4041
)
4142

4243
const (
@@ -107,6 +108,29 @@ func TestScript(t *testing.T) {
107108
"GONOSUMDB=*", // GOPROXY is a private proxy
108109
homeEnvName()+"="+home,
109110
)
111+
if info, err := os.Stat(filepath.Join(e.WorkDir, "_registry")); err == nil && info.IsDir() {
112+
prefix := ""
113+
if data, err := os.ReadFile(filepath.Join(e.WorkDir, "_registry_prefix")); err == nil {
114+
prefix = strings.TrimSpace(string(data))
115+
}
116+
// There's a _registry directory. Start a fake registry server to serve
117+
// the modules in it.
118+
reg, err := registrytest.New(os.DirFS(e.WorkDir), prefix)
119+
if err != nil {
120+
return fmt.Errorf("cannot start test registry server: %v", err)
121+
}
122+
if prefix != "" {
123+
prefix = "/" + prefix
124+
}
125+
e.Vars = append(e.Vars,
126+
"CUE_REGISTRY="+reg.Host()+prefix+"+insecure",
127+
// This enables some tests to construct their own malformed
128+
// CUE_REGISTRY values that still refer to the test registry.
129+
"DEBUG_REGISTRY_HOST="+reg.Host(),
130+
"CUE_EXPERIMENT=modules",
131+
)
132+
e.Defer(reg.Close)
133+
}
110134
return nil
111135
},
112136
Condition: cuetest.Condition,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# When CUE_EXPERIMENT is empty, the CUE_REGISTRY
2+
# variable is ignored, as are the new fields in module.cue
3+
4+
env CUE_EXPERIMENT=
5+
exec cue export .
6+
cmp stdout expect-stdout
7+
8+
-- expect-stdout --
9+
"cue.mod/pkg source"
10+
-- main.cue --
11+
package main
12+
import "example.com/e"
13+
14+
e.foo
15+
16+
-- cue.mod/module.cue --
17+
module: "test.org"
18+
// This should be ignored.
19+
deps: "example.com/e": v: "v0.0.1"
20+
-- cue.mod/pkg/example.com/e/cue.mod/module.cue --
21+
module: "example.com/e"
22+
-- cue.mod/pkg/example.com/e/main.cue --
23+
package e
24+
foo: "cue.mod/pkg source"
25+
26+
-- _registry/example.com_e_v0.0.1/cue.mod/module.cue --
27+
module: "example.com/e@v0"
28+
29+
-- _registry/example.com_e_v0.0.1/main.cue --
30+
package e
31+
32+
foo: "registry source"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
env CUE_EXPERIMENT=modules
2+
env CUE_REGISTRY=malformed!registry@url
3+
! exec cue eval .
4+
cmp stderr expect-stderr
5+
6+
-- expect-stderr --
7+
$CUE_REGISTRY "malformed!registry@url" is not a valid host name
8+
-- main.cue --
9+
package main
10+
import "example.com/e"
11+
12+
e.foo
13+
14+
-- cue.mod/module.cue --
15+
module: "test.org"
16+
deps: "example.com/e": v: "v0.0.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
! exec cue eval .
2+
# TODO this error message could use improvement:
3+
# - the "error response: 404 Not Found: " part is redundant
4+
# - the module path is also repeated redundantly.
5+
cmp stderr expect-stderr
6+
7+
-- expect-stderr --
8+
instance: cannot resolve dependencies: example.com/[email protected]: module example.com/[email protected]: error response: 404 Not Found: repository name not known to registry
9+
-- main.cue --
10+
package main
11+
import "example.com/e"
12+
13+
e.foo
14+
15+
-- cue.mod/module.cue --
16+
module: "test.org"
17+
deps: "example.com/e": v: "v0.0.2"
18+
-- _registry/example.com_e_v0.0.1/cue.mod/module.cue --
19+
module: "example.com/e@v0"
20+
21+
-- main.cue --
22+
package e
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
env CUE_EXPERIMENT=
2+
env CUE_REGISTRY=ignored.example.com
3+
exec cue export .
4+
cmp stdout expect-stdout
5+
cmp stderr expect-stderr
6+
7+
-- expect-stdout --
8+
"ok"
9+
-- expect-stderr --
10+
warning: ignoring CUE_REGISTRY because modules experiment is not enabled. Set CUE_EXPERIMENT=modules to enable it.
11+
-- main.cue --
12+
package main
13+
14+
"ok"

0 commit comments

Comments
 (0)