...

Source file src/cuelang.org/go/mod/modconfig/modconfig.go

Documentation: cuelang.org/go/mod/modconfig

     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  	"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  // Registry is used to access CUE modules from external sources.
    30  type Registry interface {
    31  	// Requirements returns a list of the modules required by the given module
    32  	// version.
    33  	Requirements(ctx context.Context, m module.Version) ([]module.Version, error)
    34  
    35  	// Fetch returns the location of the contents for the given module
    36  	// version, downloading it if necessary.
    37  	Fetch(ctx context.Context, m module.Version) (module.SourceLoc, error)
    38  
    39  	// ModuleVersions returns all the versions for the module with the
    40  	// given path, which should contain a major version.
    41  	ModuleVersions(ctx context.Context, mpath string) ([]string, error)
    42  }
    43  
    44  // We don't want to make modload part of the cue/load API,
    45  // so we define the above type independently, but we want
    46  // it to be interchangeable, so check that statically here.
    47  var (
    48  	_ Registry         = modload.Registry(nil)
    49  	_ modload.Registry = Registry(nil)
    50  )
    51  
    52  // DefaultRegistry is the default registry host.
    53  const DefaultRegistry = "registry.cue.works"
    54  
    55  // Resolver implements [modregistry.Resolver] in terms of the
    56  // CUE registry configuration file and auth configuration.
    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  // Config provides the starting point for the configuration.
    66  type Config struct {
    67  	// TODO allow for a custom resolver to be passed in.
    68  
    69  	// Transport is used to make the underlying HTTP requests.
    70  	// If it's nil, [http.DefaultTransport] will be used.
    71  	Transport http.RoundTripper
    72  
    73  	// Env provides environment variable values. If this is nil,
    74  	// the current process's environment will be used.
    75  	Env []string
    76  
    77  	// ClientType is used as part of the User-Agent header
    78  	// that's added in each outgoing HTTP request.
    79  	// If it's empty, it defaults to "cuelang.org/go".
    80  	ClientType string
    81  }
    82  
    83  // NewResolver returns an implementation of [modregistry.Resolver]
    84  // that uses cfg to guide registry resolution. If cfg is nil, it's
    85  // equivalent to passing pointer to a zero Config struct.
    86  //
    87  // It consults the same environment variables used by the
    88  // cue command.
    89  //
    90  // The contents of the configuration will not be mutated.
    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  // Host represents a registry host name and whether
   137  // it should be accessed via a secure connection or not.
   138  type Host = modresolve.Host
   139  
   140  // AllHosts returns all the registry hosts that the resolver might resolve to,
   141  // ordered lexically by hostname.
   142  func (r *Resolver) AllHosts() []Host {
   143  	return r.resolver.AllHosts()
   144  }
   145  
   146  // HostLocation represents a registry host and a location with it.
   147  type HostLocation = modresolve.Location
   148  
   149  // ResolveToLocation returns the host location for the given module path and version
   150  // without creating a Registry instance for it.
   151  func (r *Resolver) ResolveToLocation(mpath string, version string) (HostLocation, bool) {
   152  	return r.resolver.ResolveToLocation(mpath, version)
   153  }
   154  
   155  // Resolve implements modregistry.Resolver.Resolve.
   156  func (r *Resolver) ResolveToRegistry(mpath string, version string) (modregistry.RegistryLocation, error) {
   157  	loc, ok := r.resolver.ResolveToLocation(mpath, version)
   158  	if !ok {
   159  		// This can only happen when mpath is invalid, which should not
   160  		// happen in practice, as the only caller is modregistry which
   161  		// vets module paths before calling Resolve.
   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  // cueLoginsTransport implements [http.RoundTripper] by using
   183  // tokens from the CUE login information when available, falling
   184  // back to using the standard [ociauth] transport implementation.
   185  type cueLoginsTransport struct {
   186  	cfg    *Config
   187  	getenv func(string) string
   188  
   189  	// initOnce guards initErr, logins, and transport.
   190  	initOnce sync.Once
   191  	initErr  error
   192  	logins   *cueconfig.Logins
   193  	// transport holds the underlying transport. This wraps
   194  	// t.cfg.Transport.
   195  	transport http.RoundTripper
   196  
   197  	// mu guards the fields below.
   198  	mu sync.Mutex
   199  
   200  	// cachedTransports holds a transport per host.
   201  	// This is needed because the oauth2 API requires a
   202  	// different client for each host. Each of these transports
   203  	// wraps the transport above.
   204  	cachedTransports map[string]http.RoundTripper
   205  }
   206  
   207  func (t *cueLoginsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   208  	// Return an error lazily on the first request because if the
   209  	// user isn't doing anything that requires a registry, we
   210  	// shouldn't complain about reading a bad configuration file.
   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  	// TODO: note that a CUE registry may include a path prefix,
   218  	// so using solely the host will not work with such a path.
   219  	// Can we do better here, perhaps keeping the path prefix up to "/v2/"?
   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  		// TODO: When this client refreshes an access token,
   235  		// we should store the refreshed token on disk.
   236  
   237  		// Make the oauth client use the transport that was set up
   238  		// in init.
   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  	// Unlock immediately so we don't hold the lock for the entire
   246  	// request, which would preclude any concurrency when
   247  	// making HTTP requests.
   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  	// If a registry was authenticated via `cue login`, use that.
   261  	// If not, fall back to authentication via Docker's config.json.
   262  	// Note that the order below is backwards, since we layer interfaces.
   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  	// If we can't locate a logins.json file at all, then we'll
   274  	// We only refuse to continue if we find an invalid logins.json file.
   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  // NewRegistry returns an implementation of the Registry
   292  // interface suitable for passing to [load.Instances].
   293  // It uses the standard CUE cache directory.
   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