Files
opentf/internal/registry/regsrc/module.go
Martin Atkins d2bef1fd47 Adopt OpenTofu's own "svchost" module
Previously we were using a third-party library, but that doesn't have any
support for passing context.Context through its API and so isn't suitable
for our goals of adding OpenTelemetry tracing for all outgoing network
requests.

We now have our own fork that is updated to use context.Context. It also
has a slightly reduced scope no longer including various details that
are tightly-coupled to our cliconfig mechanism and so better placed in the
main OpenTofu codebase so we can evolve it in future without making
lockstep library releases.

The "registry-address" library also uses svchost and uses some of its types
in its public API, so this also incorporates v2 of that library that is
updated to use our own svchost module.

Unfortunately this commit is a mix of mechanical updates to the new
libraries and some new code dealing with the functionality that is removed
in our fork of svchost. The new code is primarily in the "svcauthconfig"
package, which is similar in purpose "ociauthconfig" but for OpenTofu's
own auth mechanism instead of the OCI Distribution protocol's auth
mechanism.

This includes some additional plumbing of context.Context where it was
possible to do so without broad changes to files that would not otherwise
have been included in this commit, but there are a few leftover spots that
are context.TODO() which we'll address separately in later commits.

This removes the temporary workaround from d079da6e9e, since we are now
able to plumb the OpenTelemetry span tree all the way to the service
discovery requests.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-06-12 09:37:59 -07:00

252 lines
8.8 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package regsrc
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs"
)
var (
ErrInvalidModuleSource = errors.New("not a valid registry module source")
// nameSubRe is the sub-expression that matches a valid module namespace or
// name. It's strictly a super-set of what GitHub allows for user/org and
// repo names respectively, but more restrictive than our original repo-name
// regex which allowed periods but could cause ambiguity with hostname
// prefixes. It does not anchor the start or end so it can be composed into
// more complex RegExps below. Alphanumeric with - and _ allowed in non
// leading or trailing positions. Max length 64 chars. (GitHub username is
// 38 max.)
nameSubRe = "[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?"
// providerSubRe is the sub-expression that matches a valid provider. It
// does not anchor the start or end so it can be composed into more complex
// RegExps below. Only lowercase chars and digits are supported in practice.
// Max length 64 chars.
providerSubRe = "[0-9a-z]{1,64}"
// moduleSourceRe is a regular expression that matches the basic
// namespace/name/provider[//...] format for registry sources. It assumes
// any FriendlyHost prefix has already been removed if present.
moduleSourceRe = regexp.MustCompile(
fmt.Sprintf("^(%s)\\/(%s)\\/(%s)(?:\\/\\/(.*))?$",
nameSubRe, nameSubRe, providerSubRe))
// NameRe is a regular expression defining the format allowed for namespace
// or name fields in module registry implementations.
NameRe = regexp.MustCompile("^" + nameSubRe + "$")
// ProviderRe is a regular expression defining the format allowed for
// provider fields in module registry implementations.
ProviderRe = regexp.MustCompile("^" + providerSubRe + "$")
// these hostnames are not allowed as registry sources, because they are
// already special case module sources in tofu.
disallowed = map[string]bool{
"github.com": true,
"bitbucket.org": true,
}
)
// Module describes a OpenTofu Registry Module source.
type Module struct {
// RawHost is the friendly host prefix if one was present. It might be nil
// if the original source had no host prefix which implies
// PublicRegistryHost but is distinct from having an actual pointer to
// PublicRegistryHost since it encodes the fact the original string didn't
// include a host prefix at all which is significant for recovering actual
// input not just normalized form. Most callers should access it with Host()
// which will return public registry host instance if it's nil.
RawHost *FriendlyHost
RawNamespace string
RawName string
RawProvider string
RawSubmodule string
}
// NewModule construct a new module source from separate parts. Pass empty
// string if host or submodule are not needed.
func NewModule(host, namespace, name, provider, submodule string) (*Module, error) {
m := &Module{
RawNamespace: namespace,
RawName: name,
RawProvider: provider,
RawSubmodule: submodule,
}
if host != "" {
h := NewFriendlyHost(host)
if h != nil {
fmt.Println("HOST:", h)
if !h.Valid() || disallowed[h.Display()] {
return nil, ErrInvalidModuleSource
}
}
m.RawHost = h
}
return m, nil
}
// ModuleFromModuleSourceAddr is an adapter to automatically transform the
// modern representation of registry module addresses,
// addrs.ModuleSourceRegistry, into the legacy representation regsrc.Module.
//
// Note that the new-style model always does normalization during parsing and
// does not preserve the raw user input at all, and so although the fields
// of regsrc.Module are all called "Raw...", initializing a Module indirectly
// through an addrs.ModuleSourceRegistry will cause those values to be the
// normalized ones, not the raw user input.
//
// Use this only for temporary shims to call into existing code that still
// uses regsrc.Module. Eventually all other subsystems should be updated to
// use addrs.ModuleSourceRegistry instead, and then package regsrc can be
// removed altogether.
func ModuleFromModuleSourceAddr(addr addrs.ModuleSourceRegistry) *Module {
ret := ModuleFromRegistryPackageAddr(addr.Package)
ret.RawSubmodule = addr.Subdir
return ret
}
// ModuleFromRegistryPackageAddr is similar to ModuleFromModuleSourceAddr, but
// it works with just the isolated registry package address, and not the
// full source address.
//
// The practical implication of that is that RawSubmodule will always be
// the empty string in results from this function, because "Submodule" maps
// to "Subdir" and that's a module source address concept, not a module
// package concept. In practice this typically doesn't matter because the
// registry client ignores the RawSubmodule field anyway; that's a concern
// for the higher-level module installer to deal with.
func ModuleFromRegistryPackageAddr(addr addrs.ModuleRegistryPackage) *Module {
return &Module{
RawHost: NewFriendlyHost(addr.Host.String()),
RawNamespace: addr.Namespace,
RawName: addr.Name,
RawProvider: addr.TargetSystem, // this field was never actually enforced to be a provider address, so now has a more general name
}
}
// ParseModuleSource attempts to parse source as a OpenTofu registry module
// source. If the string is not found to be in a valid format,
// ErrInvalidModuleSource is returned. Note that this can only be used on
// "input" strings, e.g. either ones supplied by the user or potentially
// normalised but in Display form (unicode). It will fail to parse a source with
// a punycoded domain since this is not permitted input from a user. If you have
// an already normalized string internally, you can compare it without parsing
// by comparing with the normalized version of the subject with the normal
// string equality operator.
func ParseModuleSource(source string) (*Module, error) {
// See if there is a friendly host prefix.
host, rest := ParseFriendlyHost(source)
if host != nil {
if !host.Valid() || disallowed[host.Display()] {
return nil, ErrInvalidModuleSource
}
}
matches := moduleSourceRe.FindStringSubmatch(rest)
if len(matches) < 4 {
return nil, ErrInvalidModuleSource
}
m := &Module{
RawHost: host,
RawNamespace: matches[1],
RawName: matches[2],
RawProvider: matches[3],
}
if len(matches) == 5 {
m.RawSubmodule = matches[4]
}
return m, nil
}
// Display returns the source formatted for display to the user in CLI or web
// output.
func (m *Module) Display() string {
return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Display()), false)
}
// Normalized returns the source formatted for internal reference or comparison.
func (m *Module) Normalized() string {
return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Normalized()), false)
}
// String returns the source formatted as the user originally typed it assuming
// it was parsed from user input.
func (m *Module) String() string {
// Don't normalize public registry hostname - leave it exactly like the user
// input it.
hostPrefix := ""
if m.RawHost != nil {
hostPrefix = m.RawHost.String() + "/"
}
return m.formatWithPrefix(hostPrefix, true)
}
// Equal compares the module source against another instance taking
// normalization into account.
func (m *Module) Equal(other *Module) bool {
return m.Normalized() == other.Normalized()
}
// Host returns the FriendlyHost object describing which registry this module is
// in. If the original source string had not host component this will return the
// PublicRegistryHost.
func (m *Module) Host() *FriendlyHost {
if m.RawHost == nil {
return PublicRegistryHost
}
return m.RawHost
}
func (m *Module) normalizedHostPrefix(host string) string {
if m.Host().Equal(PublicRegistryHost) {
return ""
}
return host + "/"
}
func (m *Module) formatWithPrefix(hostPrefix string, preserveCase bool) string {
suffix := ""
if m.RawSubmodule != "" {
suffix = "//" + m.RawSubmodule
}
str := fmt.Sprintf("%s%s/%s/%s%s", hostPrefix, m.RawNamespace, m.RawName,
m.RawProvider, suffix)
// lower case by default
if !preserveCase {
return strings.ToLower(str)
}
return str
}
// Module returns just the registry ID of the module, without a hostname or
// suffix.
func (m *Module) Module() string {
return fmt.Sprintf("%s/%s/%s", m.RawNamespace, m.RawName, m.RawProvider)
}
// SvcHost returns the svchost.Hostname for this module. Since FriendlyHost may
// contain an invalid hostname, this also returns an error indicating if it
// could be converted to a svchost.Hostname. If no host is specified, the
// default PublicRegistryHost is returned.
func (m *Module) SvcHost() (svchost.Hostname, error) {
if m.RawHost == nil {
return svchost.ForComparison(PublicRegistryHost.Raw)
}
return svchost.ForComparison(m.RawHost.Raw)
}