getmodules: Add new "oci" module source address scheme

This connects up various work done in earlier commits so that it's now
possible to install modules from source addresses that start with "oci:",
which will each get interpreted as a reference to an artifacts in an OCI
Distribution repository.

For the first time we now have a getter that needs to be configured
dynamically based on the CLI configuration, so this slightly reworks the
"reusingGetter" type so that each instance has its own map of getters
that's based on the statically-configured one. Currently "oci" is the only
getter that needs this dynamic configuration, but perhaps in future we'll
adopt a similar dependency inversion style for some of the other getters
so that we can centralize concerns such as allowing operators to configure
additional TLS certificates for OpenTofu to trust.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-04-16 10:03:32 -07:00
parent fbe7e48a5d
commit 0f9a88bed7
2 changed files with 33 additions and 4 deletions

View File

@@ -91,14 +91,22 @@ var goGetterDecompressorMediaTypes = map[string]string{
"archive/zip": "zip",
}
// goGetterGetters is an initial table of getters that we use as a starting
// point when building a _real_ table of getters to pass into a
// [reusingGetter] instance.
//
// The elements mapped to nil here are those which are populated dynamically
// based on arguments to [NewPackageFetcher], included here only so it's
// easier to refer to the entire list of supported getter keys in one place.
var goGetterGetters = map[string]getter.Getter{
"file": new(getter.FileGetter),
"gcs": new(getter.GCSGetter),
"git": new(getter.GitGetter),
"hg": new(getter.HgGetter),
"s3": new(getter.S3Getter),
"http": getterHTTPGetter,
"https": getterHTTPGetter,
"oci": nil, // configured dynamically using [PackageFetcherEnvironment.OCIRepositoryStore]
"s3": new(getter.S3Getter),
}
var getterHTTPClient = cleanhttp.DefaultClient()
@@ -121,10 +129,21 @@ var getterHTTPGetter = &getter.HttpGetter{
// imports getmodules in order to indirectly access our go-getter
// configuration.)
type reusingGetter struct {
// getters are the go-getter getters that this particular instance of
// reusingGetter should use.
getters map[string]getter.Getter
previousInstalls map[string]string // initialized on first install request
previousInstallsMu sync.Mutex // must hold while interacting with previousInstalls
}
func newReusingGetter(getters map[string]getter.Getter) *reusingGetter {
return &reusingGetter{
getters: getters,
// previousInstalls initialized only on request
}
}
// getWithGoGetter fetches the package at the given address into the given
// target directory. The given address must already be in normalized form
// (using NormalizePackageAddress) or else the behavior is undefined.
@@ -188,7 +207,7 @@ func (g *reusingGetter) getWithGoGetter(ctx context.Context, instPath, packageAd
Detectors: goGetterNoDetectors, // our caller should've already done detection
Decompressors: goGetterDecompressors,
Getters: goGetterGetters,
Getters: g.getters,
Ctx: ctx,
}
err = client.Get()

View File

@@ -8,6 +8,7 @@ package getmodules
import (
"context"
"fmt"
"maps"
)
// PackageFetcher is a low-level utility for fetching remote module packages
@@ -36,9 +37,18 @@ type PackageFetcher struct {
// intended only for use in unit tests.
func NewPackageFetcher(env PackageFetcherEnvironment) *PackageFetcher {
env = preparePackageFetcherEnvironment(env)
_ = env // TODO: Actually use this, once we have an OCI Distribution getter
// We use goGetterGetters as our starting point for the available
// getters, but some need to be instantiated dynamically based on
// the given "env". We shallow-copy the source map so that multiple
// instances of PackageFetcher don't clobber each other's getters.
getters := maps.Clone(goGetterGetters)
getters["oci"] = &ociDistributionGetter{
getOCIRepositoryStore: env.OCIRepositoryStore,
}
return &PackageFetcher{
getter: &reusingGetter{},
getter: newReusingGetter(getters),
}
}