cuelang.org/go@v0.13.0/mod/modconfig/modconfig.go (about) 1 // Package modconfig provides access to the standard CUE 2 // module configuration, including registry access and authorization. 3 package modconfig 4 5 import ( 6 "context" 7 "errors" 8 "fmt" 9 "io/fs" 10 "net/http" 11 "os" 12 "slices" 13 "strings" 14 "sync" 15 16 "cuelabs.dev/go/oci/ociregistry" 17 "cuelabs.dev/go/oci/ociregistry/ociauth" 18 "cuelabs.dev/go/oci/ociregistry/ociclient" 19 "golang.org/x/oauth2" 20 21 "cuelang.org/go/internal/cueconfig" 22 "cuelang.org/go/internal/cueversion" 23 "cuelang.org/go/internal/mod/modload" 24 "cuelang.org/go/internal/mod/modpkgload" 25 "cuelang.org/go/internal/mod/modresolve" 26 "cuelang.org/go/mod/modcache" 27 "cuelang.org/go/mod/modregistry" 28 "cuelang.org/go/mod/module" 29 ) 30 31 // Registry is used to access CUE modules from external sources. 32 type Registry interface { 33 // Requirements returns a list of the modules required by the given module 34 // version. 35 Requirements(ctx context.Context, m module.Version) ([]module.Version, error) 36 37 // Fetch returns the location of the contents for the given module 38 // version, downloading it if necessary. 39 Fetch(ctx context.Context, m module.Version) (module.SourceLoc, error) 40 41 // ModuleVersions returns all the versions for the module with the 42 // given path, which should contain a major version. 43 ModuleVersions(ctx context.Context, mpath string) ([]string, error) 44 } 45 46 // CachedRegistry is optionally implemented by a registry that 47 // contains a cache. 48 type CachedRegistry interface { 49 // FetchFromCache looks up the given module in the cache. 50 // It returns an error that satisfies [errors.Is]([modregistry.ErrNotFound]) if the 51 // module is not present in the cache at this version or if there 52 // is no cache. 53 FetchFromCache(mv module.Version) (module.SourceLoc, error) 54 } 55 56 // We don't want to make modload part of the cue/load API, 57 // so we define the above type independently, but we want 58 // it to be interchangeable, so check that statically here. 59 var ( 60 _ Registry = modload.Registry(nil) 61 _ modload.Registry = Registry(nil) 62 _ CachedRegistry = modpkgload.CachedRegistry(nil) 63 _ modpkgload.CachedRegistry = CachedRegistry(nil) 64 ) 65 66 // DefaultRegistry is the default registry host. 67 const DefaultRegistry = "registry.cue.works" 68 69 // Resolver implements [modregistry.Resolver] in terms of the 70 // CUE registry configuration file and auth configuration. 71 type Resolver struct { 72 resolver modresolve.LocationResolver 73 newRegistry func(host string, insecure bool) (ociregistry.Interface, error) 74 75 mu sync.Mutex 76 registries map[string]ociregistry.Interface 77 } 78 79 // Config provides the starting point for the configuration. 80 type Config struct { 81 // TODO allow for a custom resolver to be passed in. 82 83 // Transport is used to make the underlying HTTP requests. 84 // If it's nil, [http.DefaultTransport] will be used. 85 Transport http.RoundTripper 86 87 // Env provides environment variable values. If this is nil, 88 // the current process's environment will be used. 89 Env []string 90 91 // CUERegistry specifies the registry or registries to use 92 // to resolve modules. If it is empty, $CUE_REGISTRY 93 // is used. 94 // Experimental: this field might go away in a future version. 95 CUERegistry string 96 97 // ClientType is used as part of the User-Agent header 98 // that's added in each outgoing HTTP request. 99 // If it's empty, it defaults to "cuelang.org/go". 100 ClientType string 101 } 102 103 // NewResolver returns an implementation of [modregistry.Resolver] 104 // that uses cfg to guide registry resolution. If cfg is nil, it's 105 // equivalent to passing pointer to a zero Config struct. 106 // 107 // It consults the same environment variables used by the 108 // cue command. 109 // 110 // The contents of the configuration will not be mutated. 111 func NewResolver(cfg *Config) (*Resolver, error) { 112 cfg = newRef(cfg) 113 cfg.Transport = cueversion.NewTransport(cfg.ClientType, cfg.Transport) 114 getenv := getenvFunc(cfg.Env) 115 var configData []byte 116 var configPath string 117 cueRegistry := cfg.CUERegistry 118 if cueRegistry == "" { 119 cueRegistry = getenv("CUE_REGISTRY") 120 } 121 kind, rest, _ := strings.Cut(cueRegistry, ":") 122 switch kind { 123 case "file": 124 data, err := os.ReadFile(rest) 125 if err != nil { 126 return nil, err 127 } 128 configData, configPath = data, rest 129 case "inline": 130 configData, configPath = []byte(rest), "inline" 131 case "simple": 132 cueRegistry = rest 133 } 134 var resolver modresolve.LocationResolver 135 var err error 136 if configPath != "" { 137 resolver, err = modresolve.ParseConfig(configData, configPath, DefaultRegistry) 138 } else { 139 resolver, err = modresolve.ParseCUERegistry(cueRegistry, DefaultRegistry) 140 } 141 if err != nil { 142 return nil, fmt.Errorf("bad value for registry: %v", err) 143 } 144 return &Resolver{ 145 resolver: resolver, 146 newRegistry: func(host string, insecure bool) (ociregistry.Interface, error) { 147 return ociclient.New(host, &ociclient.Options{ 148 Insecure: insecure, 149 Transport: &cueLoginsTransport{ 150 getenv: getenv, 151 cfg: cfg, 152 }, 153 }) 154 }, 155 registries: make(map[string]ociregistry.Interface), 156 }, nil 157 } 158 159 // Host represents a registry host name and whether 160 // it should be accessed via a secure connection or not. 161 type Host = modresolve.Host 162 163 // AllHosts returns all the registry hosts that the resolver might resolve to, 164 // ordered lexically by hostname. 165 func (r *Resolver) AllHosts() []Host { 166 return r.resolver.AllHosts() 167 } 168 169 // HostLocation represents a registry host and a location with it. 170 type HostLocation = modresolve.Location 171 172 // ResolveToLocation returns the host location for the given module path and version 173 // without creating a Registry instance for it. 174 func (r *Resolver) ResolveToLocation(mpath string, version string) (HostLocation, bool) { 175 return r.resolver.ResolveToLocation(mpath, version) 176 } 177 178 // ResolveToRegistry implements [modregistry.Resolver.ResolveToRegistry]. 179 func (r *Resolver) ResolveToRegistry(mpath string, version string) (modregistry.RegistryLocation, error) { 180 loc, ok := r.resolver.ResolveToLocation(mpath, version) 181 if !ok { 182 // This can happen when mpath is invalid, which should not 183 // happen in practice, as the only caller is modregistry which 184 // vets module paths before calling Resolve. 185 // 186 // It can also happen when the user has explicitly configured a "none" 187 // registry to avoid falling back to a default registry. 188 return modregistry.RegistryLocation{}, fmt.Errorf("cannot resolve %s (version %q) to registry: %w", mpath, version, modregistry.ErrRegistryNotFound) 189 } 190 r.mu.Lock() 191 defer r.mu.Unlock() 192 reg := r.registries[loc.Host] 193 if reg == nil { 194 reg1, err := r.newRegistry(loc.Host, loc.Insecure) 195 if err != nil { 196 return modregistry.RegistryLocation{}, fmt.Errorf("cannot make client: %v", err) 197 } 198 r.registries[loc.Host] = reg1 199 reg = reg1 200 } 201 return modregistry.RegistryLocation{ 202 Registry: reg, 203 Repository: loc.Repository, 204 Tag: loc.Tag, 205 }, nil 206 } 207 208 // cueLoginsTransport implements [http.RoundTripper] by using 209 // tokens from the CUE login information when available, falling 210 // back to using the standard [ociauth] transport implementation. 211 type cueLoginsTransport struct { 212 cfg *Config 213 getenv func(string) string 214 215 // initOnce guards initErr, logins, and transport. 216 initOnce sync.Once 217 initErr error 218 // loginsMu guards the logins pointer below. 219 // Note that an instance of cueconfig.Logins is read-only and 220 // does not have to be guarded. 221 loginsMu sync.Mutex 222 logins *cueconfig.Logins 223 // transport holds the underlying transport. This wraps 224 // t.cfg.Transport. 225 transport http.RoundTripper 226 227 // mu guards the fields below. 228 mu sync.Mutex 229 230 // cachedTransports holds a transport per host. 231 // This is needed because the oauth2 API requires a 232 // different client for each host. Each of these transports 233 // wraps the transport above. 234 cachedTransports map[string]http.RoundTripper 235 } 236 237 func (t *cueLoginsTransport) RoundTrip(req *http.Request) (*http.Response, error) { 238 // Return an error lazily on the first request because if the 239 // user isn't doing anything that requires a registry, we 240 // shouldn't complain about reading a bad configuration file. 241 if err := t.init(); err != nil { 242 return nil, err 243 } 244 245 t.loginsMu.Lock() 246 logins := t.logins 247 t.loginsMu.Unlock() 248 249 if logins == nil { 250 return t.transport.RoundTrip(req) 251 } 252 // TODO: note that a CUE registry may include a path prefix, 253 // so using solely the host will not work with such a path. 254 // Can we do better here, perhaps keeping the path prefix up to "/v2/"? 255 host := req.URL.Host 256 login, ok := logins.Registries[host] 257 if !ok { 258 return t.transport.RoundTrip(req) 259 } 260 261 t.mu.Lock() 262 transport := t.cachedTransports[host] 263 if transport == nil { 264 tok := cueconfig.TokenFromLogin(login) 265 oauthCfg := cueconfig.RegistryOAuthConfig(Host{ 266 Name: host, 267 Insecure: req.URL.Scheme == "http", 268 }) 269 270 // Make the oauth client use the transport that was set up 271 // in init. 272 ctx := context.WithValue(req.Context(), oauth2.HTTPClient, &http.Client{ 273 Transport: t.transport, 274 }) 275 transport = oauth2.NewClient(ctx, 276 &cachingTokenSource{ 277 updateFunc: func(tok *oauth2.Token) error { 278 return t.updateLogin(host, tok) 279 }, 280 base: oauthCfg.TokenSource(ctx, tok), 281 t: tok, 282 }, 283 ).Transport 284 t.cachedTransports[host] = transport 285 } 286 // Unlock immediately so we don't hold the lock for the entire 287 // request, which would preclude any concurrency when 288 // making HTTP requests. 289 t.mu.Unlock() 290 return transport.RoundTrip(req) 291 } 292 293 func (t *cueLoginsTransport) updateLogin(host string, new *oauth2.Token) error { 294 // Reload the logins file in case another process changed it in the meantime. 295 loginsPath, err := cueconfig.LoginConfigPath(t.getenv) 296 if err != nil { 297 // TODO: this should never fail. Log a warning. 298 return nil 299 } 300 301 // Lock the logins for the entire duration of the update to avoid races 302 t.loginsMu.Lock() 303 defer t.loginsMu.Unlock() 304 305 logins, err := cueconfig.UpdateRegistryLogin(loginsPath, host, new) 306 if err != nil { 307 return err 308 } 309 310 t.logins = logins 311 312 return nil 313 } 314 315 func (t *cueLoginsTransport) init() error { 316 t.initOnce.Do(func() { 317 t.initErr = t._init() 318 }) 319 return t.initErr 320 } 321 322 func (t *cueLoginsTransport) _init() error { 323 // If a registry was authenticated via `cue login`, use that. 324 // If not, fall back to authentication via Docker's config.json. 325 // Note that the order below is backwards, since we layer interfaces. 326 327 config, err := ociauth.LoadWithEnv(nil, t.cfg.Env) 328 if err != nil { 329 return fmt.Errorf("cannot load OCI auth configuration: %v", err) 330 } 331 t.transport = ociauth.NewStdTransport(ociauth.StdTransportParams{ 332 Config: config, 333 Transport: t.cfg.Transport, 334 }) 335 336 // If we can't locate a logins.json file at all, then we'll continue. 337 // We only refuse to continue if we find an invalid logins.json file. 338 loginsPath, err := cueconfig.LoginConfigPath(t.getenv) 339 if err != nil { 340 // TODO: this should never fail. Log a warning. 341 return nil 342 } 343 logins, err := cueconfig.ReadLogins(loginsPath) 344 if errors.Is(err, fs.ErrNotExist) { 345 return nil 346 } 347 if err != nil { 348 return fmt.Errorf("cannot load CUE registry logins: %v", err) 349 } 350 t.logins = logins 351 t.cachedTransports = make(map[string]http.RoundTripper) 352 return nil 353 } 354 355 // NewRegistry returns an implementation of the Registry 356 // interface suitable for passing to [load.Instances]. 357 // It uses the standard CUE cache directory. 358 func NewRegistry(cfg *Config) (Registry, error) { 359 cfg = newRef(cfg) 360 resolver, err := NewResolver(cfg) 361 if err != nil { 362 return nil, err 363 } 364 cacheDir, err := cueconfig.CacheDir(getenvFunc(cfg.Env)) 365 if err != nil { 366 return nil, err 367 } 368 return modcache.New(modregistry.NewClientWithResolver(resolver), cacheDir) 369 } 370 371 func getenvFunc(env []string) func(string) string { 372 if env == nil { 373 return os.Getenv 374 } 375 return func(key string) string { 376 for _, e := range slices.Backward(env) { 377 if len(e) >= len(key)+1 && e[len(key)] == '=' && e[:len(key)] == key { 378 return e[len(key)+1:] 379 } 380 } 381 return "" 382 } 383 } 384 385 func newRef[T any](x *T) *T { 386 var x1 T 387 if x != nil { 388 x1 = *x 389 } 390 return &x1 391 } 392 393 // cachingTokenSource works similar to oauth2.ReuseTokenSource, except that it 394 // also exposes a hook to get a hold of the refreshed token, so that it can be 395 // stored in persistent storage. 396 type cachingTokenSource struct { 397 updateFunc func(tok *oauth2.Token) error 398 base oauth2.TokenSource // called when t is expired 399 400 mu sync.Mutex // guards t 401 t *oauth2.Token 402 } 403 404 func (s *cachingTokenSource) Token() (*oauth2.Token, error) { 405 s.mu.Lock() 406 t := s.t 407 408 if t.Valid() { 409 s.mu.Unlock() 410 return t, nil 411 } 412 413 t, err := s.base.Token() 414 if err != nil { 415 s.mu.Unlock() 416 return nil, err 417 } 418 419 s.t = t 420 s.mu.Unlock() 421 422 err = s.updateFunc(t) 423 if err != nil { 424 return nil, err 425 } 426 427 return t, nil 428 }