mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
wip module packages direct from a module registry
This commit is contained in:
@@ -46,7 +46,7 @@ type ModuleInstaller struct {
|
|||||||
|
|
||||||
// The keys in moduleVersionsUrl are the moduleVersion struct below and
|
// The keys in moduleVersionsUrl are the moduleVersion struct below and
|
||||||
// addresses and the values are underlying remote source addresses.
|
// addresses and the values are underlying remote source addresses.
|
||||||
registryPackageSources map[moduleVersion]addrs.ModuleSourceRemote
|
registryPackageSources map[moduleVersion]registry.PackageLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
type moduleVersion struct {
|
type moduleVersion struct {
|
||||||
@@ -79,7 +79,7 @@ func NewModuleInstaller(modsDir string, loader *configload.Loader, registryClien
|
|||||||
reg: registryClient,
|
reg: registryClient,
|
||||||
fetcher: remotePackageFetcher,
|
fetcher: remotePackageFetcher,
|
||||||
registryPackageVersions: make(map[addrs.ModuleRegistryPackage]*response.ModuleVersions),
|
registryPackageVersions: make(map[addrs.ModuleRegistryPackage]*response.ModuleVersions),
|
||||||
registryPackageSources: make(map[moduleVersion]addrs.ModuleSourceRemote),
|
registryPackageSources: make(map[moduleVersion]registry.PackageLocation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +751,7 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *config
|
|||||||
// first check the cache for the download URL
|
// first check the cache for the download URL
|
||||||
moduleAddr := moduleVersion{module: packageAddr, version: latestMatch.String()}
|
moduleAddr := moduleVersion{module: packageAddr, version: latestMatch.String()}
|
||||||
if _, exists := i.registryPackageSources[moduleAddr]; !exists {
|
if _, exists := i.registryPackageSources[moduleAddr]; !exists {
|
||||||
realAddrRaw, err := reg.ModuleLocation(ctx, regsrcAddr, latestMatch.String())
|
packageLocation, err := reg.ModuleLocation(ctx, regsrcAddr, latestMatch.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err)
|
log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err)
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
@@ -762,42 +762,42 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *config
|
|||||||
tracing.SetSpanError(span, diags)
|
tracing.SetSpanError(span, diags)
|
||||||
return nil, nil, diags
|
return nil, nil, diags
|
||||||
}
|
}
|
||||||
realAddr, err := addrs.ParseModuleSource(realAddrRaw)
|
span.SetAttributes(traceattrs.OpenTofuModuleSource(packageLocation.UILabel()))
|
||||||
if err != nil {
|
i.registryPackageSources[moduleAddr] = packageLocation
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid package location from module registry",
|
|
||||||
Detail: fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: %s.", hostname, realAddrRaw, addr, latestMatch, err),
|
|
||||||
})
|
|
||||||
tracing.SetSpanError(span, diags)
|
|
||||||
return nil, nil, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
span.SetAttributes(traceattrs.OpenTofuModuleSource(realAddr.String()))
|
|
||||||
|
|
||||||
switch realAddr := realAddr.(type) {
|
|
||||||
// Only a remote source address is allowed here: a registry isn't
|
|
||||||
// allowed to return a local path (because it doesn't know what
|
|
||||||
// its being called from) and we also don't allow recursively pointing
|
|
||||||
// at another registry source for simplicity's sake.
|
|
||||||
case addrs.ModuleSourceRemote:
|
|
||||||
i.registryPackageSources[moduleAddr] = realAddr
|
|
||||||
default:
|
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
|
||||||
Severity: hcl.DiagError,
|
|
||||||
Summary: "Invalid package location from module registry",
|
|
||||||
Detail: fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: must be a direct remote package address.", hostname, realAddrRaw, addr, latestMatch),
|
|
||||||
})
|
|
||||||
tracing.SetSpanError(span, diags)
|
|
||||||
return nil, nil, diags
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dlAddr := i.registryPackageSources[moduleAddr]
|
packageLocation := i.registryPackageSources[moduleAddr]
|
||||||
|
|
||||||
log.Printf("[TRACE] ModuleInstaller: %s %s %s is available at %q", key, packageAddr, latestMatch, dlAddr.Package)
|
log.Printf("[TRACE] ModuleInstaller: %s %s %s is available at %q", key, packageAddr, latestMatch, packageLocation.UILabel())
|
||||||
|
var err error // populated in the cases below
|
||||||
err := fetcher.FetchPackage(ctx, instPath, dlAddr.Package.String())
|
modDir := instPath // possibly overwritten below if the module is in a subdirectory of the package
|
||||||
|
switch packageLocation := packageLocation.(type) {
|
||||||
|
case registry.PackageLocationDirect:
|
||||||
|
// Direct locations are handled by the same registry client that
|
||||||
|
// returned them, since the download might require using equivalent
|
||||||
|
// credentials as were used to decide the location. modDir is the
|
||||||
|
// directory where the requested module was installed, which might
|
||||||
|
// be a subdirectory of instPath.
|
||||||
|
modDir, err = reg.InstallModulePackage(ctx, packageLocation, instPath)
|
||||||
|
case registry.PackageLocationIndirect:
|
||||||
|
// Indirect locations are handled by the package fetcher, similar to
|
||||||
|
// if the same address had been specified directly in the "source"
|
||||||
|
// argument of the module call.
|
||||||
|
err = fetcher.FetchPackage(ctx, instPath, packageLocation.SourceAddr.Package.String())
|
||||||
|
if packageLocation.SourceAddr.Subdir != "" {
|
||||||
|
subDir := filepath.FromSlash(packageLocation.SourceAddr.Subdir)
|
||||||
|
modDir = filepath.Join(modDir, subDir)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// The above cases should be exhaustive for all of the implementations
|
||||||
|
// of registry.PackageLocation, so we should not get here.
|
||||||
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
Severity: hcl.DiagError,
|
||||||
|
Summary: "Unsupported package location",
|
||||||
|
Detail: fmt.Sprintf("Registry client returned a package location of type %T, which the module installer doesn't support. This is a bug in OpenTofu.", packageLocation),
|
||||||
|
})
|
||||||
|
return nil, nil, diags
|
||||||
|
}
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
@@ -815,27 +815,19 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *config
|
|||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
Severity: hcl.DiagError,
|
Severity: hcl.DiagError,
|
||||||
Summary: "Failed to download module",
|
Summary: "Failed to download module",
|
||||||
Detail: fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, dlAddr, err),
|
Detail: fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, packageLocation, err),
|
||||||
Subject: req.CallRange.Ptr(),
|
Subject: req.CallRange.Ptr(),
|
||||||
})
|
})
|
||||||
return nil, nil, diags
|
return nil, nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, dlAddr.Package, instPath)
|
log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, packageLocation.UILabel(), modDir)
|
||||||
|
|
||||||
// Incorporate any subdir information from the original path into the
|
|
||||||
// address returned by the registry in order to find the final directory
|
|
||||||
// of the target module.
|
|
||||||
finalAddr := dlAddr.FromRegistry(addr)
|
|
||||||
subDir := filepath.FromSlash(finalAddr.Subdir)
|
|
||||||
modDir := filepath.Join(instPath, subDir)
|
|
||||||
|
|
||||||
log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir)
|
|
||||||
|
|
||||||
// Finally we are ready to try actually loading the module.
|
// Finally we are ready to try actually loading the module.
|
||||||
mod, mDiags := i.loader.Parser().LoadConfigDir(modDir, req.Call)
|
mod, mDiags := i.loader.Parser().LoadConfigDir(modDir, req.Call)
|
||||||
if mod == nil {
|
if mod == nil {
|
||||||
|
|
||||||
|
subDir := packageLocation.Subdir()
|
||||||
isMissingSubDir, missingDir := isSubDirNonExistent(modDir)
|
isMissingSubDir, missingDir := isSubDirNonExistent(modDir)
|
||||||
// nil indicates missing or unreadable directory, so we'll
|
// nil indicates missing or unreadable directory, so we'll
|
||||||
// discard the returned diags and return a more specific
|
// discard the returned diags and return a more specific
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,6 +24,7 @@ import (
|
|||||||
"github.com/opentofu/svchost"
|
"github.com/opentofu/svchost"
|
||||||
"github.com/opentofu/svchost/disco"
|
"github.com/opentofu/svchost/disco"
|
||||||
|
|
||||||
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
"github.com/opentofu/opentofu/internal/httpclient"
|
"github.com/opentofu/opentofu/internal/httpclient"
|
||||||
"github.com/opentofu/opentofu/internal/registry/regsrc"
|
"github.com/opentofu/opentofu/internal/registry/regsrc"
|
||||||
"github.com/opentofu/opentofu/internal/registry/response"
|
"github.com/opentofu/opentofu/internal/registry/response"
|
||||||
@@ -158,9 +162,13 @@ func (c *Client) addRequestCreds(ctx context.Context, host svchost.Hostname, req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModuleLocation find the download location for a specific version module.
|
// ModuleLocation find the package location for a specific module version.
|
||||||
// This returns a string, because the final location may contain special go-getter syntax.
|
//
|
||||||
func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, version string) (string, error) {
|
// This returns one of the concrete implementations of the closed interface
|
||||||
|
// [PackageLocation], depending on what type of location the registry chooses
|
||||||
|
// to report. Refer to the documentation of those types for information on
|
||||||
|
// how each variant should be used to actually install the package.
|
||||||
|
func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, version string) (PackageLocation, error) {
|
||||||
ctx, span := tracing.Tracer().Start(ctx, "Find Module Location", tracing.SpanAttributes(
|
ctx, span := tracing.Tracer().Start(ctx, "Find Module Location", tracing.SpanAttributes(
|
||||||
traceattrs.OpenTofuModuleCallName(module.RawName),
|
traceattrs.OpenTofuModuleCallName(module.RawName),
|
||||||
traceattrs.OpenTofuModuleSource(module.Module()),
|
traceattrs.OpenTofuModuleSource(module.Module()),
|
||||||
@@ -170,12 +178,12 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers
|
|||||||
|
|
||||||
host, err := module.SvcHost()
|
host, err := module.SvcHost()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
service, err := c.Discover(ctx, host, modulesServiceID)
|
service, err := c.Discover(ctx, host, modulesServiceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var p *url.URL
|
var p *url.URL
|
||||||
@@ -185,7 +193,7 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers
|
|||||||
p, err = url.Parse(path.Join(module.Module(), version, "download"))
|
p, err = url.Parse(path.Join(module.Module(), version, "download"))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
download := service.ResolveReference(p)
|
download := service.ResolveReference(p)
|
||||||
|
|
||||||
@@ -193,7 +201,7 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers
|
|||||||
|
|
||||||
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", download.String(), nil)
|
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", download.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
@@ -203,46 +211,163 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers
|
|||||||
|
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error reading response body from registry: %w", err)
|
return nil, fmt.Errorf("error reading response body from registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var location string
|
|
||||||
|
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
var v response.ModuleLocationRegistryResp
|
var v response.ModuleLocationRegistryResp
|
||||||
if err := json.Unmarshal(body, &v); err != nil {
|
if err := json.Unmarshal(body, &v); err != nil {
|
||||||
return "", fmt.Errorf("module %q version %q failed to deserialize response body %s: %w",
|
return nil, fmt.Errorf("module %q version %q failed to deserialize response body %s: %w",
|
||||||
module, version, body, err)
|
module, version, body, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
location = v.Location
|
if v.UseRegistryCredentials == nil {
|
||||||
|
// The registry has not opted in to "direct" installation, so we
|
||||||
|
// assume that it wants the old-style "indirect" behavior where
|
||||||
|
// the registry is essentially just an lookup table for
|
||||||
|
// go-getter-style source addresses, in which case the registry
|
||||||
|
// isn't involved in the final download step at all.
|
||||||
|
|
||||||
// if the location is empty, we will fallback to the header
|
if v.Location == "" {
|
||||||
if location == "" {
|
// If the location is empty, we will fallback to the header.
|
||||||
location = resp.Header.Get(xTerraformGet)
|
// Note that this only works if the body contains valid JSON syntax.
|
||||||
|
// This was probably not actually the originally intended behavior,
|
||||||
|
// since this fallback was introduced to fix a regression in
|
||||||
|
// https://github.com/opentofu/opentofu/pull/2079 but that didn't
|
||||||
|
// _quite_ restore the original behavior of ignoring the body completely
|
||||||
|
// when using this header. Nonetheless, we're keeping this constraint
|
||||||
|
// to avoid churning this protocol further since registry
|
||||||
|
// implementers tend to want to support many OpenTofu versions
|
||||||
|
// at once and so having many different variations is harder
|
||||||
|
// to test. Those who want to do the legacy thing of using
|
||||||
|
// X-Terraform-Get should use a "204 No Content" status code if
|
||||||
|
// they can't provide valid JSON syntax in the body.
|
||||||
|
return preparePackageLocationIndirect(resp.Header.Get(xTerraformGet), module, download)
|
||||||
|
}
|
||||||
|
return preparePackageLocationIndirect(v.Location, module, download)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, the registry has opted in to the new-style "direct"
|
||||||
|
// installation approach, where the registry returns a URL that's under
|
||||||
|
// its own control and we fetch from it directly instead of delegating
|
||||||
|
// to go-getter.
|
||||||
|
return preparePackageLocationDirect(v.Location, module, download, bool(*v.UseRegistryCredentials))
|
||||||
|
|
||||||
case http.StatusNoContent:
|
case http.StatusNoContent:
|
||||||
// FALLBACK: set the found location from the header
|
// FALLBACK: set the found location from the header
|
||||||
location = resp.Header.Get(xTerraformGet)
|
return preparePackageLocationIndirect(resp.Header.Get(xTerraformGet), module, download)
|
||||||
|
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
return "", fmt.Errorf("module %q version %q not found", module, version)
|
return nil, fmt.Errorf("module %q version %q not found", module, version)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// anything else is an error:
|
// anything else is an error:
|
||||||
return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body)
|
return nil, fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallModulePackage attempts to install a module package from the given
|
||||||
|
// location into the given target directory.
|
||||||
|
//
|
||||||
|
// This method is used only for "direct" package locations, where the registry
|
||||||
|
// is directly hosting packages in locations under its own control, and possibly
|
||||||
|
// authenticated using the registry's own credentials. If you have a
|
||||||
|
// [PackageLocationIndirect] instead then you must handle it separately using
|
||||||
|
// the "remote source address" installation process.
|
||||||
|
//
|
||||||
|
// If successful this returns the final path of the requested module, taking
|
||||||
|
// into account any subdirectory selection that was included in the original
|
||||||
|
// module request. If the original source address did not include a subdirectory
|
||||||
|
// portion then the result is just a normalized version of targetDir.
|
||||||
|
func (c *Client) InstallModulePackage(ctx context.Context, location PackageLocationDirect, targetDir string) (string, error) {
|
||||||
|
urlString := location.packageURL.String()
|
||||||
|
ctx, span := tracing.Tracer().Start(ctx, "Fetch Package",
|
||||||
|
tracing.SpanAttributes(traceattrs.URLFull(urlString)),
|
||||||
|
)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", urlString, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("preparing to download from %s: %w", urlString, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if location == "" {
|
host, err := location.module.SvcHost()
|
||||||
return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body)
|
if err != nil {
|
||||||
|
// We should not get here because location.module should be populated
|
||||||
|
// correctly by [Client.ModuleLocation].
|
||||||
|
return "", fmt.Errorf("package location has invalid registry hostname: %w", err)
|
||||||
|
}
|
||||||
|
if location.useRegistryCredentials {
|
||||||
|
c.addRequestCreds(ctx, host, req.Request)
|
||||||
|
}
|
||||||
|
req.Header.Set(xTerraformVersion, tfVersion)
|
||||||
|
// We'll set some content negotiation headers just in case that helps
|
||||||
|
// someone using a general-purpose static HTTP server serve content
|
||||||
|
// compressed on the fly by the server. (We will actually tolerate more
|
||||||
|
// than what we report here, in the more common case where the server
|
||||||
|
// just returns whatever it has on disk without any transformation,
|
||||||
|
// but this is just a hint for some common choices.
|
||||||
|
req.Header.Set("Accept", "application/zip, application/x-tar; *;q=0.1")
|
||||||
|
req.Header.Set("Accept-Encoding", "identity, gzip, *;q=0.1")
|
||||||
|
|
||||||
|
// We first fetch the raw content at the URL into a temporary file, and
|
||||||
|
// then we can sniff what format it seems to be in so that we'll tolerate
|
||||||
|
// servers that aren't able to correctly populate Content-Type and other
|
||||||
|
// similar header fields (which is relatively common for static file
|
||||||
|
// servers used for serving large files like these; e.g. they sometimes
|
||||||
|
// report just "application/octet-stream", or misreport which compressor
|
||||||
|
// was used for a tar stream, etc.).
|
||||||
|
f, err := os.CreateTemp("", "opentofu-modpkg-")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating temporary file for module package: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// We make a best-effort to proactively clean the temporary file, but
|
||||||
|
// if this fails we'll still let installation succeed and assume that
|
||||||
|
// an OS service will clean the temporary directory itself eventually.
|
||||||
|
_ = f.Close()
|
||||||
|
_ = os.Remove(f.Name())
|
||||||
|
}()
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err // net/http includes method and URL in its errors automatically
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
n, err := io.Copy(f, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("copying module package to temporary file: %w", err)
|
||||||
|
}
|
||||||
|
if wantN, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64); err == nil {
|
||||||
|
// If the server told us how much data it was expecting to send then
|
||||||
|
// we'll make sure we got exactly that much data.
|
||||||
|
if n != wantN {
|
||||||
|
return "", fmt.Errorf("server promised %d bytes, but returned %d bytes", wantN, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = extractModulePackage(f, targetDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("extracting package archive: %w", err)
|
||||||
|
}
|
||||||
|
modDir := targetDir
|
||||||
|
if location.module.RawSubmodule != "" {
|
||||||
|
subDir := filepath.FromSlash(location.module.RawSubmodule)
|
||||||
|
modDir = filepath.Join(modDir, subDir)
|
||||||
|
}
|
||||||
|
return modDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePackageLocationIndirect(realAddrRaw string, forModule *regsrc.Module, baseURL *url.URL) (PackageLocation, error) {
|
||||||
|
if realAddrRaw == "" {
|
||||||
|
return nil, fmt.Errorf("registry did not return a location for this package")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If location looks like it's trying to be a relative URL, treat it as
|
// If location looks like it's trying to be a relative URL, treat it as
|
||||||
@@ -256,14 +381,64 @@ func (c *Client) ModuleLocation(ctx context.Context, module *regsrc.Module, vers
|
|||||||
// to be an absolute URL, but we are more liberal here because third-party
|
// to be an absolute URL, but we are more liberal here because third-party
|
||||||
// registry implementations may not "know" their own absolute URLs if
|
// registry implementations may not "know" their own absolute URLs if
|
||||||
// e.g. they are running behind a reverse proxy frontend, or such.
|
// e.g. they are running behind a reverse proxy frontend, or such.
|
||||||
if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") {
|
if strings.HasPrefix(realAddrRaw, "/") || strings.HasPrefix(realAddrRaw, "./") || strings.HasPrefix(realAddrRaw, "../") {
|
||||||
locationURL, err := url.Parse(location)
|
locationURL, err := url.Parse(realAddrRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid relative URL for %q: %w", module, err)
|
return nil, fmt.Errorf("invalid relative URL %q: %w", realAddrRaw, err)
|
||||||
}
|
}
|
||||||
locationURL = download.ResolveReference(locationURL)
|
locationURL = baseURL.ResolveReference(locationURL)
|
||||||
location = locationURL.String()
|
realAddrRaw = locationURL.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return location, nil
|
realAddrAny, err := addrs.ParseModuleSource(realAddrRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"registry returned invalid package location %q: %w",
|
||||||
|
realAddrRaw, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
realAddr, ok := realAddrAny.(addrs.ModuleSourceRemote)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("registry returned invalid package location %q: must be a direct remote package address", realAddrRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we're installing indirectly it's possible that both the registry
|
||||||
|
// source address and the go-getter-style address returned frmo the registry
|
||||||
|
// include a "subdirectory" component, in which case we need to resolve
|
||||||
|
// the final effective subdirectory path that combines both.
|
||||||
|
realAddr = realAddr.FromRegistry(
|
||||||
|
// Unfortunately we have some tech debt here where this old registry
|
||||||
|
// client code uses some older conventions for representing module
|
||||||
|
// registry addresses, so we need to adapt this to the modern
|
||||||
|
// representation.
|
||||||
|
forModule.AsModuleSourceRegistry(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return PackageLocationIndirect{
|
||||||
|
SourceAddr: realAddr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePackageLocationDirect(locationRaw string, originalAddr *regsrc.Module, baseURL *url.URL, useRegistryCredentials bool) (PackageLocation, error) {
|
||||||
|
packageURL, err := url.Parse(locationRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("registry returned an invalid package URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !packageURL.IsAbs() {
|
||||||
|
// We resolve relative URLs against the URL we got the location from.
|
||||||
|
packageURL = baseURL.ResolveReference(packageURL)
|
||||||
|
}
|
||||||
|
if packageURL.Scheme != "http" && packageURL.Scheme != "https" {
|
||||||
|
return nil, fmt.Errorf("registry returned invalid package URL %q: must be http or https URL", locationRaw)
|
||||||
|
}
|
||||||
|
if packageURL.Fragment != "" {
|
||||||
|
return nil, fmt.Errorf("registry returned invalid package URL %q: must not include fragment part", locationRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PackageLocationDirect{
|
||||||
|
module: originalAddr,
|
||||||
|
packageURL: packageURL,
|
||||||
|
useRegistryCredentials: useRegistryCredentials,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/hashicorp/go-retryablehttp"
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
version "github.com/hashicorp/go-version"
|
version "github.com/hashicorp/go-version"
|
||||||
"github.com/opentofu/svchost/disco"
|
"github.com/opentofu/svchost/disco"
|
||||||
|
|
||||||
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
"github.com/opentofu/opentofu/internal/httpclient"
|
"github.com/opentofu/opentofu/internal/httpclient"
|
||||||
"github.com/opentofu/opentofu/internal/registry/regsrc"
|
"github.com/opentofu/opentofu/internal/registry/regsrc"
|
||||||
"github.com/opentofu/opentofu/internal/registry/response"
|
"github.com/opentofu/opentofu/internal/registry/response"
|
||||||
@@ -139,9 +141,13 @@ func TestLookupModuleLocationRelative(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := server.URL + "/relative-path"
|
want := PackageLocationIndirect{
|
||||||
if got != want {
|
SourceAddr: addrs.ModuleSourceRemote{
|
||||||
t.Errorf("wrong location %s; want %s", got, want)
|
Package: addrs.ModulePackage(server.URL + "/relative-path"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Error("wrong location\n" + diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,28 +315,80 @@ func TestLookupModuleNetworkError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModuleLocation_readRegistryResponse(t *testing.T) {
|
func TestModuleLocation_readRegistryResponse(t *testing.T) {
|
||||||
|
makeIndirectLocation := func(packageAddr string, subDir string) PackageLocationIndirect {
|
||||||
|
return PackageLocationIndirect{
|
||||||
|
SourceAddr: addrs.ModuleSourceRemote{
|
||||||
|
Package: addrs.ModulePackage(packageAddr),
|
||||||
|
Subdir: subDir,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustParseURL := func(s string) *url.URL {
|
||||||
|
ret, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
src string
|
src string
|
||||||
handlerFunc func(w http.ResponseWriter, r *http.Request)
|
handlerFunc func(w http.ResponseWriter, r *http.Request)
|
||||||
registryFlags []uint8
|
registryFlags []uint8
|
||||||
want string
|
want PackageLocation
|
||||||
wantErrorStr string
|
wantErrorStr string
|
||||||
wantToReadFromHeader bool
|
wantToReadFromHeader bool
|
||||||
wantStatusCode int
|
wantStatusCode int
|
||||||
}{
|
}{
|
||||||
"shall find the module location in the registry response body": {
|
"shall find direct module location in the registry response body, opting to use the registry's credentials": {
|
||||||
|
src: "exists-in-registry/identifier/provider",
|
||||||
|
want: PackageLocationDirect{
|
||||||
|
module: ®src.Module{
|
||||||
|
RawHost: ®src.FriendlyHost{Raw: "registry.opentofu.org"},
|
||||||
|
RawNamespace: "exists-in-registry",
|
||||||
|
RawName: "identifier",
|
||||||
|
RawProvider: "provider",
|
||||||
|
},
|
||||||
|
packageURL: mustParseURL("https://example.com/package.zip"),
|
||||||
|
useRegistryCredentials: true,
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{"location":"https://example.com/package.zip","use_registry_credentials":true}`))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"shall find direct module location in the registry response body, not opting to use the registry's credentials": {
|
||||||
|
src: "exists-in-registry/identifier/provider",
|
||||||
|
want: PackageLocationDirect{
|
||||||
|
module: ®src.Module{
|
||||||
|
RawHost: ®src.FriendlyHost{Raw: "registry.opentofu.org"},
|
||||||
|
RawNamespace: "exists-in-registry",
|
||||||
|
RawName: "identifier",
|
||||||
|
RawProvider: "provider",
|
||||||
|
},
|
||||||
|
packageURL: mustParseURL("https://example.com/package.zip"),
|
||||||
|
useRegistryCredentials: false,
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{"location":"https://example.com/package.zip","use_registry_credentials":false}`))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"shall find indirect module location in the registry response body": {
|
||||||
src: "exists-in-registry/identifier/provider",
|
src: "exists-in-registry/identifier/provider",
|
||||||
want: "file:///registry/exists",
|
want: makeIndirectLocation("file:///registry/exists", ""),
|
||||||
wantStatusCode: http.StatusOK,
|
wantStatusCode: http.StatusOK,
|
||||||
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_ = json.NewEncoder(w).Encode(response.ModuleLocationRegistryResp{Location: "file:///registry/exists"})
|
_ = json.NewEncoder(w).Encode(response.ModuleLocationRegistryResp{Location: "file:///registry/exists"})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"shall find the module location in the registry response header": {
|
"shall find indirect module location in the registry response header": {
|
||||||
src: "exists-in-registry/identifier/provider",
|
src: "exists-in-registry/identifier/provider",
|
||||||
registryFlags: []uint8{test.WithModuleLocationInHeader},
|
registryFlags: []uint8{test.WithModuleLocationInHeader},
|
||||||
want: "file:///registry/exists",
|
want: makeIndirectLocation("file:///registry/exists", ""),
|
||||||
wantToReadFromHeader: true,
|
wantToReadFromHeader: true,
|
||||||
wantStatusCode: http.StatusNoContent,
|
wantStatusCode: http.StatusNoContent,
|
||||||
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -338,9 +396,9 @@ func TestModuleLocation_readRegistryResponse(t *testing.T) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"shall read location from the registry response body even if the header with location address is also set": {
|
"shall read indirect location from the registry response body even if the header with location address is also set": {
|
||||||
src: "exists-in-registry/identifier/provider",
|
src: "exists-in-registry/identifier/provider",
|
||||||
want: "file:///registry/exists",
|
want: makeIndirectLocation("file:///registry/exists", ""),
|
||||||
wantStatusCode: http.StatusOK,
|
wantStatusCode: http.StatusOK,
|
||||||
wantToReadFromHeader: false,
|
wantToReadFromHeader: false,
|
||||||
registryFlags: []uint8{test.WithModuleLocationInBody, test.WithModuleLocationInHeader},
|
registryFlags: []uint8{test.WithModuleLocationInBody, test.WithModuleLocationInHeader},
|
||||||
@@ -391,7 +449,7 @@ func TestModuleLocation_readRegistryResponse(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"shall fail because location is not found in the response": {
|
"shall fail because location is not found in the response": {
|
||||||
src: "foo/bar/baz",
|
src: "foo/bar/baz",
|
||||||
wantErrorStr: `failed to get download URL for "foo/bar/baz": 200 OK resp:{"foo":"git::https://github.com/foo/terraform-baz-bar?ref=v0.2.0"}`,
|
wantErrorStr: `registry did not return a location for this package`,
|
||||||
wantStatusCode: http.StatusOK,
|
wantStatusCode: http.StatusOK,
|
||||||
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
handlerFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -431,8 +489,8 @@ func TestModuleLocation_readRegistryResponse(t *testing.T) {
|
|||||||
if err != nil && !strings.Contains(err.Error(), tc.wantErrorStr) {
|
if err != nil && !strings.Contains(err.Error(), tc.wantErrorStr) {
|
||||||
t.Fatalf("unexpected error content: want=%s, got=%v", tc.wantErrorStr, err)
|
t.Fatalf("unexpected error content: want=%s, got=%v", tc.wantErrorStr, err)
|
||||||
}
|
}
|
||||||
if got != tc.want {
|
if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(PackageLocationDirect{})); diff != "" {
|
||||||
t.Fatalf("unexpected location: want=%s, got=%v", tc.want, got)
|
t.Fatal("unexpected location\n" + diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify status code if we have a successful response
|
// Verify status code if we have a successful response
|
||||||
|
|||||||
122
internal/registry/package_extract.go
Normal file
122
internal/registry/package_extract.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-getter"
|
||||||
|
"github.com/ulikunitz/xz"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extractModulePackage(tempF *os.File, targetDir string) error {
|
||||||
|
// We reuse go-getter's decompressors for the actual extraction, because
|
||||||
|
// they are already hardened against a number of previously-discovered
|
||||||
|
// attacks involving crafted archives, and if any similar problems are
|
||||||
|
// discovered later then upgrading go-getter will patch both this and
|
||||||
|
// the go-getter-based module installation path.
|
||||||
|
decompressor, err := sniffPackageDecompressor(tempF)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getter.Decompressor wants to work with filenames rather than open
|
||||||
|
// files, so we need to pass it the path to our temporary file now.
|
||||||
|
// Note that this could potentially race if another process modifies
|
||||||
|
// or removes the file before the decompressor opens it, but the
|
||||||
|
// decompressors should all be robust to malicious input anyway.
|
||||||
|
return decompressor.Decompress(targetDir, tempF.Name(), true, 0 /*default umask*/)
|
||||||
|
}
|
||||||
|
|
||||||
|
var packageDecompressSniffers = []func(*os.File, int64) getter.Decompressor{
|
||||||
|
func(f *os.File, size int64) getter.Decompressor {
|
||||||
|
// zip.NewReader succeeds only if the file has a zip header
|
||||||
|
_, err := zip.NewReader(f, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &getter.ZipDecompressor{}
|
||||||
|
},
|
||||||
|
func(f *os.File, _ int64) getter.Decompressor {
|
||||||
|
// gzip.NewReader succeeds only if the file has a gzip header
|
||||||
|
_, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Tar archives don't have a header, so we just assume that any
|
||||||
|
// gzip stream is intended to contain a tar stream.
|
||||||
|
return &getter.TarGzipDecompressor{}
|
||||||
|
},
|
||||||
|
func(f *os.File, _ int64) getter.Decompressor {
|
||||||
|
buf := make([]byte, xz.HeaderLen)
|
||||||
|
n, err := f.Read(buf)
|
||||||
|
if err != nil || n != len(buf) {
|
||||||
|
return nil // not able to read an xz header
|
||||||
|
}
|
||||||
|
if !xz.ValidHeader(buf) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Tar archives don't have a header, so we just assume that any
|
||||||
|
// xz stream is intended to contain a tar stream.
|
||||||
|
return &getter.TarXzDecompressor{}
|
||||||
|
},
|
||||||
|
func(f *os.File, _ int64) getter.Decompressor {
|
||||||
|
// encoding/bzip2 doesn't offer a direct way to ask if a stream
|
||||||
|
// has a valid bzip2 header, so we'll check it manually by looking
|
||||||
|
// for the "BZ" magic number at the very start. This sniffer is
|
||||||
|
// intentionally last because it's doing the least checking and
|
||||||
|
// so is most likely to generate false positives.
|
||||||
|
// (The go-getter decompressor we return will check this more
|
||||||
|
// thoroughly; our job here is just to decide if it seems likely
|
||||||
|
// that this was intended to be a bzip2 stream.)
|
||||||
|
|
||||||
|
// We're reading four bytes here because a file smaller than that
|
||||||
|
// cannot possibly be a valid bzip2 stream. The last two bytes here
|
||||||
|
// are real header fields though, not part of the magic number.
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
n, err := f.Read(buf)
|
||||||
|
if err != nil || n != len(buf) {
|
||||||
|
return nil // not able to read a magic number
|
||||||
|
}
|
||||||
|
if buf[0] != 'B' || buf[1] != 'Z' {
|
||||||
|
return nil // not the magic number we were looking for
|
||||||
|
}
|
||||||
|
// Tar archives don't have a header, so we just assume that any
|
||||||
|
// bzip2 stream is intended to contain a tar stream.
|
||||||
|
return &getter.TarBzip2Decompressor{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func sniffPackageDecompressor(tempF *os.File) (getter.Decompressor, error) {
|
||||||
|
// Our approach here is just to try opening the file in a few different
|
||||||
|
// ways where success implies a file was probably intended to be of
|
||||||
|
// a particular format, but once we've decided we'll just let the real
|
||||||
|
// decompressor do the actual validation of the package.
|
||||||
|
|
||||||
|
info, err := tempF.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err // Error message already mentions it was trying to stat
|
||||||
|
}
|
||||||
|
fileSize := info.Size()
|
||||||
|
for _, sniffer := range packageDecompressSniffers {
|
||||||
|
_, err := tempF.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
// Should not get here because the caller should always give us
|
||||||
|
// a regular file. The error message from stdlib already mentions that
|
||||||
|
// it was trying to seek.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ret := sniffer(tempF, fileSize); ret != nil {
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we fall out here then we weren't able to detect a supported format.
|
||||||
|
return nil, fmt.Errorf("module package is not zip archive, or tar archive with gz, xz, or bzip2 compression")
|
||||||
|
}
|
||||||
125
internal/registry/package_location.go
Normal file
125
internal/registry/package_location.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Copyright (c) The OpenTofu Authors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright (c) 2023 HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
|
"github.com/opentofu/opentofu/internal/registry/regsrc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PackageLocation abstractly represents a location that a module package should
|
||||||
|
// be installed from.
|
||||||
|
//
|
||||||
|
// There are exactly two concrete implementations of this interface:
|
||||||
|
// [PackageLocationDirect] for packages that are hosted as part of the
|
||||||
|
// registry they were reported from, and [PackageLocationIndirect] (for
|
||||||
|
// registry packages that are really just aliases for source addresses that
|
||||||
|
// someone could've specified directly in their configuration).
|
||||||
|
//
|
||||||
|
// This is a closed interface. If any new implementations of it are added in
|
||||||
|
// future then an exhaustive type-switch over these in the module installer
|
||||||
|
// will need to be updated to support the new variants.
|
||||||
|
type PackageLocation interface {
|
||||||
|
// UILabel returns a label that can be used to concisely refer to this
|
||||||
|
// location in the OpenTofu UI, such as when reporting progress or
|
||||||
|
// describing problems in error messages.
|
||||||
|
//
|
||||||
|
// The result is not necessarily a unique identifier for the location. It's
|
||||||
|
// just expected to be something a human reader could use to confirm whether
|
||||||
|
// OpenTofu is installing from somewhere reasonable and expected.
|
||||||
|
UILabel() string
|
||||||
|
|
||||||
|
// Subdir returns a path to a directory within the package that contains
|
||||||
|
// the module that is being selected. Returns an empty string if the root
|
||||||
|
// of the package contains the selected module.
|
||||||
|
Subdir() string
|
||||||
|
|
||||||
|
// This unexported method means that only types within this package can
|
||||||
|
// implement this interface.
|
||||||
|
packageLocationSigil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageLocationDirect represents a module package location that's considered
|
||||||
|
// to be a part of the registry that reported it, and so the same registry
|
||||||
|
// client that reported this location should also be used to install it.
|
||||||
|
//
|
||||||
|
// This type is intentionally opaque, since values of this type should be
|
||||||
|
// passed directly to [Client.InstallPackage] on the same Client instance
|
||||||
|
// that returned this value. It encapsulates everything that client would need
|
||||||
|
// to perform the installation.
|
||||||
|
type PackageLocationDirect struct {
|
||||||
|
// module is the registry module address that the package at this location
|
||||||
|
// is intended to satisfy. We track this so that the client can decide
|
||||||
|
// which credentials (if any) to use when requesting the package.
|
||||||
|
module *regsrc.Module
|
||||||
|
|
||||||
|
// packageURL is the absolute HTTP or HTTPS URL where the module package
|
||||||
|
// is located. This URL should respond to a GET request by returning a
|
||||||
|
// successful response whose body is either a "zip" archive, or is a
|
||||||
|
// "tar" archive using either gz, xz, or bzip2 compression.
|
||||||
|
packageURL *url.URL
|
||||||
|
|
||||||
|
// useRegistryCredentials records whether the registry directed OpenTofu
|
||||||
|
// to reuse the same credentials that were used to request this location
|
||||||
|
// (or, at least, functionally-equivalent credentials) when making a GET
|
||||||
|
// request to the URL given in archiveURL.
|
||||||
|
//
|
||||||
|
// This is used for private registries that wish to protect both metadata
|
||||||
|
// and packages using the same credentials. If this is false then the
|
||||||
|
// request to archiveURL uses no credentials at all and so that URL must
|
||||||
|
// either be willing to serve an anonymous request or some sort of
|
||||||
|
// credential information must be packed into the URL itself, such as if
|
||||||
|
// using a mechanism like AWS S3's "presigned URLs":
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
|
||||||
|
useRegistryCredentials bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PackageLocation = PackageLocationDirect{}
|
||||||
|
|
||||||
|
// UILabel implements PackageLocation.
|
||||||
|
func (p PackageLocationDirect) UILabel() string {
|
||||||
|
return p.packageURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subdir implements PackageLocation.
|
||||||
|
func (p PackageLocationDirect) Subdir() string {
|
||||||
|
return p.module.RawSubmodule
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageLocationSigil implements PackageLocation.
|
||||||
|
func (p PackageLocationDirect) packageLocationSigil() {}
|
||||||
|
|
||||||
|
// PackageLocationIndirect represents a module package that is accessible
|
||||||
|
// through a "remote" module source address just like what could be written
|
||||||
|
// directly in a "source" argument in a module call, and so must be installed
|
||||||
|
// through the normal remote package fetcher instead of through the registry
|
||||||
|
// client.
|
||||||
|
//
|
||||||
|
// For locations of this type, the registry client that produced it is no longer
|
||||||
|
// involved after the location has been decided.
|
||||||
|
type PackageLocationIndirect struct {
|
||||||
|
// SourceAddr is the remote source address to install from, which should
|
||||||
|
// be treated in an equivalent way to how this address would've been treated
|
||||||
|
// if specified directly in a module call's "source" argument.
|
||||||
|
SourceAddr addrs.ModuleSourceRemote
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PackageLocation = PackageLocationIndirect{}
|
||||||
|
|
||||||
|
// UILabel implements PackageLocation.
|
||||||
|
func (p PackageLocationIndirect) UILabel() string {
|
||||||
|
return p.SourceAddr.ForDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subdir implements PackageLocation.
|
||||||
|
func (p PackageLocationIndirect) Subdir() string {
|
||||||
|
return p.SourceAddr.Subdir
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageLocationSigil implements PackageLocation.
|
||||||
|
func (p PackageLocationIndirect) packageLocationSigil() {}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
regaddr "github.com/opentofu/registry-address/v2"
|
||||||
"github.com/opentofu/svchost"
|
"github.com/opentofu/svchost"
|
||||||
|
|
||||||
"github.com/opentofu/opentofu/internal/addrs"
|
"github.com/opentofu/opentofu/internal/addrs"
|
||||||
@@ -116,6 +117,25 @@ func ModuleFromModuleSourceAddr(addr addrs.ModuleSourceRegistry) *Module {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsModuleSourceRegistry translates this legacy representation of module
|
||||||
|
// registry addresses back into the modern model [addrs.ModuleSourceRegistry].
|
||||||
|
//
|
||||||
|
// Normally [addrs.ModuleSourceRegistry] values are normalized during parsing,
|
||||||
|
// but this function doesn't actually do any parsing so its result is not
|
||||||
|
// guaranteed to be normalized unless the receiver was originally created
|
||||||
|
// with [ModuleFromModuleSourceAddr] and not modified in the meantime.
|
||||||
|
func (m *Module) AsModuleSourceRegistry() addrs.ModuleSourceRegistry {
|
||||||
|
return addrs.ModuleSourceRegistry{
|
||||||
|
Package: regaddr.ModulePackage{
|
||||||
|
Host: svchost.Hostname(m.Host().Normalized()),
|
||||||
|
Namespace: m.RawNamespace,
|
||||||
|
Name: m.RawName,
|
||||||
|
TargetSystem: m.RawProvider, // this field was never actually enforced to be a provider address, so now has a more general name
|
||||||
|
},
|
||||||
|
Subdir: m.RawSubmodule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ModuleFromRegistryPackageAddr is similar to ModuleFromModuleSourceAddr, but
|
// ModuleFromRegistryPackageAddr is similar to ModuleFromModuleSourceAddr, but
|
||||||
// it works with just the isolated registry package address, and not the
|
// it works with just the isolated registry package address, and not the
|
||||||
// full source address.
|
// full source address.
|
||||||
|
|||||||
@@ -3,9 +3,51 @@
|
|||||||
|
|
||||||
package response
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// ModuleLocationRegistryResp defines the OpenTofu registry response
|
// ModuleLocationRegistryResp defines the OpenTofu registry response
|
||||||
// returned when calling the endpoint /v1/modules/:namespace/:name/:system/:version/download
|
// returned when calling the endpoint /v1/modules/:namespace/:name/:system/:version/download
|
||||||
type ModuleLocationRegistryResp struct {
|
type ModuleLocationRegistryResp struct {
|
||||||
// The URL to download the module from.
|
// The URL to download the module from.
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
|
|
||||||
|
// If not nil, represents that the registry wishes to provide the module
|
||||||
|
// package directly itself instead of delegating to a separate
|
||||||
|
// go-getter-style source address.
|
||||||
|
//
|
||||||
|
// In that case, the registry can set either "true" to request that the
|
||||||
|
// final download request should use the same credentials used to fetch the
|
||||||
|
// download location, or "false" to request that the request should be
|
||||||
|
// made anonymously (e.g. if the URL already contains something that acts
|
||||||
|
// as authentication credentials).
|
||||||
|
UseRegistryCredentials *StrictBool `json:"use_registry_credentials"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictBool is a named type representing a bool value that must be written
|
||||||
|
// in JSON as exactly "true" or "false". In particular, "null" is not permitted
|
||||||
|
// as an alias for "false", unlike the Go JSON package's default behavior.
|
||||||
|
//
|
||||||
|
// This is here really just to implement [json.Unmarshaler] for conveniently
|
||||||
|
// handling JSON properties that have this requirement.
|
||||||
|
type StrictBool bool
|
||||||
|
|
||||||
|
func (b *StrictBool) UnmarshalJSON(src []byte) error {
|
||||||
|
// This method gets called only when the associated JSON property is
|
||||||
|
// actually present, and in that case gets called with a preallocated
|
||||||
|
// bool value that we need to overwrite based on the source.
|
||||||
|
src = bytes.TrimSpace(src)
|
||||||
|
|
||||||
|
// There are only two possible valid JSON boolean tokens, so we'll just
|
||||||
|
// handle them directly here for simplicity's sake.
|
||||||
|
if bytes.Equal(src, []byte{'t', 'r', 'u', 'e'}) {
|
||||||
|
*b = true
|
||||||
|
} else if bytes.Equal(src, []byte{'f', 'a', 'l', 's', 'e'}) {
|
||||||
|
*b = false
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("must be either true or false")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user