Skip to content

Commit ca20656

Browse files
committed
cmd/cue: cue mod upload command
This adds an experimental (and perhaps temporary) command that can be used to upload a module to an OCI registry. To repeat: this command is EXPERIMENTAL; usage and name will change in the future. Signed-off-by: Roger Peppe <[email protected]> Change-Id: I5aa976b528a1d7c9ae50335adb15921cdf8e5465 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1171801 TryBot-Result: CUEcueckoo <[email protected]> Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Daniel Martí <[email protected]>
1 parent 2ae1a9e commit ca20656

File tree

6 files changed

+321
-12
lines changed

6 files changed

+321
-12
lines changed

cmd/cue/cmd/mod.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,21 @@ func newModCmd(c *Command) *cobra.Command {
4545
}
4646

4747
cmd.AddCommand(newModInitCmd(c))
48+
cmd.AddCommand(newModUploadCmd(c))
4849
return cmd
4950
}
5051

5152
func newModInitCmd(c *Command) *cobra.Command {
5253
cmd := &cobra.Command{
5354
Use: "init [module]",
5455
Short: "initialize new module in current directory",
55-
Long: `Init initializes a cue.mod directory in the current directory,
56-
in effect creating a new module rooted at the current directory.
57-
The cue.mod directory must not already exist.
58-
A legacy cue.mod file in the current directory is moved
59-
to the new subdirectory.
60-
61-
A module name is optional, but if it is not given a packages
62-
within the module cannot imported another package defined
56+
Long: `Init initializes a cue.mod directory in the current directory, in effect
57+
creating a new module rooted at the current directory. The cue.mod
58+
directory must not already exist. A legacy cue.mod file in the current
59+
directory is moved to the new subdirectory.
60+
61+
A module name is optional, but if it is not given, a package
62+
within the module cannot import another package defined
6363
in the module.
6464
`,
6565
RunE: mkRunE(c, runModInit),

cmd/cue/cmd/modupload.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2023 The CUE Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
22+
"github.com/spf13/cobra"
23+
24+
"cuelang.org/go/internal/mod/modfile"
25+
"cuelang.org/go/internal/mod/modregistry"
26+
"cuelang.org/go/internal/mod/module"
27+
"cuelang.org/go/internal/mod/modzip"
28+
)
29+
30+
func newModUploadCmd(c *Command) *cobra.Command {
31+
cmd := &cobra.Command{
32+
// TODO: this command is still experimental, don't show it in
33+
// the documentation just yet.
34+
Hidden: true,
35+
36+
Use: "upload <version>",
37+
Short: "upload the current module to a registry",
38+
Long: `WARNING: THIS COMMAND IS EXPERIMENTAL.
39+
40+
Upload the current module to an OCI registry.
41+
Currently this command must be run in the module's root directory.
42+
Also note that this command does no dependency or other checks at the moment.
43+
`,
44+
RunE: mkRunE(c, runModUpload),
45+
Args: cobra.ExactArgs(1),
46+
}
47+
48+
return cmd
49+
}
50+
51+
func runModUpload(cmd *Command, args []string) error {
52+
reg, err := getRegistry()
53+
if err != nil {
54+
return err
55+
}
56+
if reg == nil {
57+
return fmt.Errorf("no registry configured to upload to")
58+
}
59+
modfileData, err := os.ReadFile("cue.mod/module.cue")
60+
if err != nil {
61+
if os.IsNotExist(err) {
62+
return fmt.Errorf("no cue.mod/module.cue file found; cue mod upload must be run in the module's root directory")
63+
}
64+
return err
65+
}
66+
mf, err := modfile.Parse(modfileData, "cue.mod/module.cue")
67+
if err != nil {
68+
return err
69+
}
70+
mv, err := module.NewVersion(mf.Module, args[0])
71+
if err != nil {
72+
return fmt.Errorf("cannot form module version: %v", err)
73+
}
74+
zf, err := os.CreateTemp("", "cue-upload-")
75+
if err != nil {
76+
return err
77+
}
78+
defer os.Remove(zf.Name())
79+
defer zf.Close()
80+
81+
// TODO verify that all dependencies exist in the registry.
82+
if err := modzip.CreateFromDir(zf, mv, "."); err != nil {
83+
return err
84+
}
85+
info, err := zf.Stat()
86+
if err != nil {
87+
return err
88+
}
89+
90+
rclient := modregistry.NewClient(reg)
91+
if err := rclient.PutModule(context.Background(), mv, zf, info.Size()); err != nil {
92+
return fmt.Errorf("cannot put module: %v", err)
93+
}
94+
fmt.Printf("uploaded %s\n", mv)
95+
return nil
96+
}

cmd/cue/cmd/script_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"context"
2121
"fmt"
2222
"io/fs"
23+
"net/http/httptest"
24+
"net/url"
2325
"os"
2426
"path"
2527
"path/filepath"
@@ -28,6 +30,8 @@ import (
2830
"testing"
2931
"time"
3032

33+
"cuelabs.dev/go/oci/ociregistry/ocimem"
34+
"cuelabs.dev/go/oci/ociregistry/ociserver"
3135
"github.com/google/shlex"
3236
"github.com/rogpeppe/go-internal/goproxytest"
3337
"github.com/rogpeppe/go-internal/gotooltest"
@@ -109,6 +113,40 @@ func TestScript(t *testing.T) {
109113
ts.Check(os.WriteFile(path, []byte(data), 0o666))
110114
}
111115
},
116+
// memregistry starts an in-memory OCI server and sets the argument
117+
// environment variable name to its hostname.
118+
"memregistry": func(ts *testscript.TestScript, neg bool, args []string) {
119+
usage := func() {
120+
ts.Fatalf("usage: memregistry [-auth=username:password] <envvar-name>")
121+
}
122+
if neg {
123+
usage()
124+
}
125+
var auth *registrytest.AuthConfig
126+
if len(args) > 0 && strings.HasPrefix(args[0], "-") {
127+
userPass, ok := strings.CutPrefix(args[0], "-auth=")
128+
if !ok {
129+
usage()
130+
}
131+
user, pass, ok := strings.Cut(userPass, ":")
132+
if !ok {
133+
usage()
134+
}
135+
auth = &registrytest.AuthConfig{
136+
Username: user,
137+
Password: pass,
138+
}
139+
args = args[1:]
140+
}
141+
if len(args) != 1 {
142+
usage()
143+
}
144+
145+
srv := httptest.NewServer(registrytest.AuthHandler(ociserver.New(ocimem.New(), nil), auth))
146+
u, _ := url.Parse(srv.URL)
147+
ts.Setenv(args[0], u.Host)
148+
ts.Defer(srv.Close)
149+
},
112150
},
113151
Setup: func(e *testscript.Env) error {
114152
// Set up a home dir within work dir with a . prefix so that the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Check that we can use the cue mod upload command to upload to a registry.
2+
memregistry MEMREGISTRY
3+
env ORIG_CUE_REGISTRY=$CUE_REGISTRY
4+
env CUE_REGISTRY=example.com=$MEMREGISTRY+insecure,$CUE_REGISTRY
5+
cd example
6+
exec cue mod upload v0.0.1
7+
cmp stdout ../expect-upload-stdout
8+
cd ../main
9+
exec cue eval .
10+
cmp stdout ../expect-eval-stdout
11+
12+
# Sanity check that the module isn't present in the fallback registry.
13+
env CUE_REGISTRY=$ORIG_CUE_REGISTRY
14+
! exec cue eval
15+
stderr 'repository name not known to registry'
16+
17+
-- expect-upload-stdout --
18+
19+
-- expect-eval-stdout --
20+
main: "main"
21+
"foo.com/bar/hello@v0": "v0.2.3"
22+
"bar.com@v0": "v0.5.0"
23+
"baz.org@v0": "v0.10.1"
24+
"example.com@v0": "v0.0.1"
25+
-- main/cue.mod/module.cue --
26+
module: "main.org"
27+
28+
deps: "example.com@v0": v: "v0.0.1"
29+
30+
-- main/main.cue --
31+
package main
32+
import "example.com@v0:main"
33+
34+
main
35+
36+
-- example/cue.mod/module.cue --
37+
module: "example.com@v0"
38+
deps: {
39+
"foo.com/bar/hello@v0": v: "v0.2.3"
40+
"bar.com@v0": v: "v0.5.0"
41+
}
42+
43+
-- example/top.cue --
44+
package main
45+
46+
// Note: import without a major version takes
47+
// the major version from the module.cue file.
48+
import a "foo.com/bar/hello"
49+
a
50+
main: "main"
51+
"example.com@v0": "v0.0.1"
52+
53+
-- _registry/foo.com_bar_hello_v0.2.3/cue.mod/module.cue --
54+
module: "foo.com/bar/hello@v0"
55+
deps: {
56+
"bar.com@v0": v: "v0.0.2"
57+
"baz.org@v0": v: "v0.10.1"
58+
}
59+
60+
-- _registry/foo.com_bar_hello_v0.2.3/x.cue --
61+
package hello
62+
import (
63+
a "bar.com/bar@v0"
64+
b "baz.org@v0:baz"
65+
)
66+
"foo.com/bar/hello@v0": "v0.2.3"
67+
a
68+
b
69+
70+
71+
-- _registry/bar.com_v0.0.2/cue.mod/module.cue --
72+
module: "bar.com@v0"
73+
deps: "baz.org@v0": v: "v0.0.2"
74+
75+
-- _registry/bar.com_v0.0.2/bar/x.cue --
76+
package bar
77+
import a "baz.org@v0:baz"
78+
"bar.com@v0": "v0.0.2"
79+
a
80+
81+
82+
-- _registry/bar.com_v0.5.0/cue.mod/module.cue --
83+
module: "bar.com@v0"
84+
deps: "baz.org@v0": v: "v0.5.0"
85+
86+
-- _registry/bar.com_v0.5.0/bar/x.cue --
87+
package bar
88+
import a "baz.org@v0:baz"
89+
"bar.com@v0": "v0.5.0"
90+
a
91+
92+
93+
-- _registry/baz.org_v0.0.2/cue.mod/module.cue --
94+
module: "baz.org@v0"
95+
96+
-- _registry/baz.org_v0.0.2/baz.cue --
97+
package baz
98+
"baz.org@v0": "v0.0.2"
99+
100+
101+
-- _registry/baz.org_v0.1.2/cue.mod/module.cue --
102+
module: "baz.org@v0"
103+
104+
-- _registry/baz.org_v0.1.2/baz.cue --
105+
package baz
106+
"baz.org@v0": "v0.1.2"
107+
108+
109+
-- _registry/baz.org_v0.5.0/cue.mod/module.cue --
110+
module: "baz.org@v0"
111+
112+
-- _registry/baz.org_v0.5.0/baz.cue --
113+
package baz
114+
"baz.org@v0": "v0.5.0"
115+
116+
117+
-- _registry/baz.org_v0.10.1/cue.mod/module.cue --
118+
module: "baz.org@v0"
119+
120+
-- _registry/baz.org_v0.10.1/baz.cue --
121+
package baz
122+
"baz.org@v0": "v0.10.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Check that we can use the cue mod upload command to upload to a registry
2+
# that's protected by authorization.
3+
4+
memregistry -auth=foo:bar MEMREGISTRY
5+
env CUE_EXPERIMENT=modules
6+
env CUE_REGISTRY=$MEMREGISTRY+insecure
7+
env DOCKER_CONFIG=$WORK/dockerconfig
8+
env-fill $DOCKER_CONFIG/config.json
9+
10+
cd example
11+
exec cue mod upload v0.0.1
12+
cmp stdout ../expect-upload-stdout
13+
cd ../main
14+
exec cue eval .
15+
cmp stdout ../expect-eval-stdout
16+
17+
-- dockerconfig/config.json --
18+
{
19+
"auths": {
20+
"${MEMREGISTRY}": {
21+
"username": "foo",
22+
"password": "bar"
23+
}
24+
}
25+
}
26+
27+
-- expect-upload-stdout --
28+
29+
-- expect-eval-stdout --
30+
main: "main"
31+
"example.com@v0": "v0.0.1"
32+
-- main/cue.mod/module.cue --
33+
module: "main.org"
34+
deps: "example.com@v0": v: "v0.0.1"
35+
36+
-- main/main.cue --
37+
package main
38+
import "example.com@v0:main"
39+
40+
main
41+
"main": "main"
42+
43+
-- example/cue.mod/module.cue --
44+
module: "example.com@v0"
45+
46+
-- example/top.cue --
47+
package main
48+
49+
"example.com@v0": "v0.0.1"

internal/registrytest/registry.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ func New(fsys fs.FS, prefix string) (*Registry, error) {
6060
if err := pushContent(client, mods); err != nil {
6161
return nil, fmt.Errorf("cannot push modules: %v", err)
6262
}
63-
var handler http.Handler = ociserver.New(r, nil)
63+
var handler http.Handler = ociserver.New(ocifilter.ReadOnly(r), nil)
6464
if authConfigData != nil {
6565
var cfg AuthConfig
6666
if err := json.Unmarshal(authConfigData, &cfg); err != nil {
6767
return nil, fmt.Errorf("invalid auth.json: %v", err)
6868
}
69-
handler = authMiddleware(handler, &cfg)
69+
handler = AuthHandler(handler, &cfg)
7070
}
7171
srv := httptest.NewServer(handler)
7272
u, err := url.Parse(srv.URL)
@@ -79,8 +79,12 @@ func New(fsys fs.FS, prefix string) (*Registry, error) {
7979
}, nil
8080
}
8181

82-
func authMiddleware(handler http.Handler, cfg *AuthConfig) http.Handler {
83-
if cfg.Username == "" {
82+
// AuthHandler wraps the given handler with logic that checks
83+
// that the incoming requests fulfil the auth requirements defined
84+
// in cfg. If cfg is nil or there are no auth requirements, it returns handler
85+
// unchanged.
86+
func AuthHandler(handler http.Handler, cfg *AuthConfig) http.Handler {
87+
if cfg == nil || cfg.Username == "" {
8488
return handler
8589
}
8690
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

0 commit comments

Comments
 (0)