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  }