1
2
3 package modconfig
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "io/fs"
10 "net/http"
11 "os"
12 "strings"
13 "sync"
14
15 "cuelabs.dev/go/oci/ociregistry"
16 "cuelabs.dev/go/oci/ociregistry/ociauth"
17 "cuelabs.dev/go/oci/ociregistry/ociclient"
18 "golang.org/x/oauth2"
19
20 "cuelang.org/go/internal/cueconfig"
21 "cuelang.org/go/internal/cueversion"
22 "cuelang.org/go/internal/mod/modload"
23 "cuelang.org/go/internal/mod/modresolve"
24 "cuelang.org/go/mod/modcache"
25 "cuelang.org/go/mod/modregistry"
26 "cuelang.org/go/mod/module"
27 )
28
29
30 type Registry interface {
31
32
33 Requirements(ctx context.Context, m module.Version) ([]module.Version, error)
34
35
36
37 Fetch(ctx context.Context, m module.Version) (module.SourceLoc, error)
38
39
40
41 ModuleVersions(ctx context.Context, mpath string) ([]string, error)
42 }
43
44
45
46
47 var (
48 _ Registry = modload.Registry(nil)
49 _ modload.Registry = Registry(nil)
50 )
51
52
53 const DefaultRegistry = "registry.cue.works"
54
55
56
57 type Resolver struct {
58 resolver modresolve.LocationResolver
59 newRegistry func(host string, insecure bool) (ociregistry.Interface, error)
60
61 mu sync.Mutex
62 registries map[string]ociregistry.Interface
63 }
64
65
66 type Config struct {
67
68
69
70
71 Transport http.RoundTripper
72
73
74
75 Env []string
76
77
78
79
80 ClientType string
81 }
82
83
84
85
86
87
88
89
90
91 func NewResolver(cfg *Config) (*Resolver, error) {
92 cfg = newRef(cfg)
93 cfg.Transport = cueversion.NewTransport(cfg.ClientType, cfg.Transport)
94 getenv := getenvFunc(cfg.Env)
95 var configData []byte
96 var configPath string
97 cueRegistry := getenv("CUE_REGISTRY")
98 kind, rest, _ := strings.Cut(cueRegistry, ":")
99 switch kind {
100 case "file":
101 data, err := os.ReadFile(rest)
102 if err != nil {
103 return nil, err
104 }
105 configData, configPath = data, rest
106 case "inline":
107 configData, configPath = []byte(rest), "$CUE_REGISTRY"
108 case "simple":
109 cueRegistry = rest
110 }
111 var resolver modresolve.LocationResolver
112 var err error
113 if configPath != "" {
114 resolver, err = modresolve.ParseConfig(configData, configPath, DefaultRegistry)
115 } else {
116 resolver, err = modresolve.ParseCUERegistry(cueRegistry, DefaultRegistry)
117 }
118 if err != nil {
119 return nil, fmt.Errorf("bad value for $CUE_REGISTRY: %v", err)
120 }
121 return &Resolver{
122 resolver: resolver,
123 newRegistry: func(host string, insecure bool) (ociregistry.Interface, error) {
124 return ociclient.New(host, &ociclient.Options{
125 Insecure: insecure,
126 Transport: &cueLoginsTransport{
127 getenv: getenv,
128 cfg: cfg,
129 },
130 })
131 },
132 registries: make(map[string]ociregistry.Interface),
133 }, nil
134 }
135
136
137
138 type Host = modresolve.Host
139
140
141
142 func (r *Resolver) AllHosts() []Host {
143 return r.resolver.AllHosts()
144 }
145
146
147 type HostLocation = modresolve.Location
148
149
150
151 func (r *Resolver) ResolveToLocation(mpath string, version string) (HostLocation, bool) {
152 return r.resolver.ResolveToLocation(mpath, version)
153 }
154
155
156 func (r *Resolver) ResolveToRegistry(mpath string, version string) (modregistry.RegistryLocation, error) {
157 loc, ok := r.resolver.ResolveToLocation(mpath, version)
158 if !ok {
159
160
161
162 return modregistry.RegistryLocation{}, fmt.Errorf("cannot resolve %s (version %s) to registry", mpath, version)
163 }
164 r.mu.Lock()
165 defer r.mu.Unlock()
166 reg := r.registries[loc.Host]
167 if reg == nil {
168 reg1, err := r.newRegistry(loc.Host, loc.Insecure)
169 if err != nil {
170 return modregistry.RegistryLocation{}, fmt.Errorf("cannot make client: %v", err)
171 }
172 r.registries[loc.Host] = reg1
173 reg = reg1
174 }
175 return modregistry.RegistryLocation{
176 Registry: reg,
177 Repository: loc.Repository,
178 Tag: loc.Tag,
179 }, nil
180 }
181
182
183
184
185 type cueLoginsTransport struct {
186 cfg *Config
187 getenv func(string) string
188
189
190 initOnce sync.Once
191 initErr error
192 logins *cueconfig.Logins
193
194
195 transport http.RoundTripper
196
197
198 mu sync.Mutex
199
200
201
202
203
204 cachedTransports map[string]http.RoundTripper
205 }
206
207 func (t *cueLoginsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
208
209
210
211 if err := t.init(); err != nil {
212 return nil, err
213 }
214 if t.logins == nil {
215 return t.transport.RoundTrip(req)
216 }
217
218
219
220 host := req.URL.Host
221 login, ok := t.logins.Registries[host]
222 if !ok {
223 return t.transport.RoundTrip(req)
224 }
225
226 t.mu.Lock()
227 transport := t.cachedTransports[host]
228 if transport == nil {
229 tok := cueconfig.TokenFromLogin(login)
230 oauthCfg := cueconfig.RegistryOAuthConfig(Host{
231 Name: host,
232 Insecure: req.URL.Scheme == "http",
233 })
234
235
236
237
238
239 ctx := context.WithValue(req.Context(), oauth2.HTTPClient, &http.Client{
240 Transport: t.transport,
241 })
242 transport = oauthCfg.Client(ctx, tok).Transport
243 t.cachedTransports[host] = transport
244 }
245
246
247
248 t.mu.Unlock()
249 return transport.RoundTrip(req)
250 }
251
252 func (t *cueLoginsTransport) init() error {
253 t.initOnce.Do(func() {
254 t.initErr = t._init()
255 })
256 return t.initErr
257 }
258
259 func (t *cueLoginsTransport) _init() error {
260
261
262
263
264 config, err := ociauth.LoadWithEnv(nil, t.cfg.Env)
265 if err != nil {
266 return fmt.Errorf("cannot load OCI auth configuration: %v", err)
267 }
268 t.transport = ociauth.NewStdTransport(ociauth.StdTransportParams{
269 Config: config,
270 Transport: t.cfg.Transport,
271 })
272
273
274
275 loginsPath, err := cueconfig.LoginConfigPath(t.getenv)
276 if err != nil {
277 return nil
278 }
279 logins, err := cueconfig.ReadLogins(loginsPath)
280 if errors.Is(err, fs.ErrNotExist) {
281 return nil
282 }
283 if err != nil {
284 return fmt.Errorf("cannot load CUE registry logins: %v", err)
285 }
286 t.logins = logins
287 t.cachedTransports = make(map[string]http.RoundTripper)
288 return nil
289 }
290
291
292
293
294 func NewRegistry(cfg *Config) (Registry, error) {
295 cfg = newRef(cfg)
296 resolver, err := NewResolver(cfg)
297 if err != nil {
298 return nil, err
299 }
300 cacheDir, err := cueconfig.CacheDir(getenvFunc(cfg.Env))
301 if err != nil {
302 return nil, err
303 }
304 return modcache.New(modregistry.NewClientWithResolver(resolver), cacheDir)
305 }
306
307 func getenvFunc(env []string) func(string) string {
308 if env == nil {
309 return os.Getenv
310 }
311 return func(key string) string {
312 for i := len(env) - 1; i >= 0; i-- {
313 if e := env[i]; len(e) >= len(key)+1 && e[len(key)] == '=' && e[:len(key)] == key {
314 return e[len(key)+1:]
315 }
316 }
317 return ""
318 }
319 }
320
321 func newRef[T any](x *T) *T {
322 var x1 T
323 if x != nil {
324 x1 = *x
325 }
326 return &x1
327 }
328
View as plain text