github.com/opentofu/opentofu@v1.7.1/internal/registry/regsrc/module.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package regsrc
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"regexp"
    12  	"strings"
    13  
    14  	svchost "github.com/hashicorp/terraform-svchost"
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  )
    17  
    18  var (
    19  	ErrInvalidModuleSource = errors.New("not a valid registry module source")
    20  
    21  	// nameSubRe is the sub-expression that matches a valid module namespace or
    22  	// name. It's strictly a super-set of what GitHub allows for user/org and
    23  	// repo names respectively, but more restrictive than our original repo-name
    24  	// regex which allowed periods but could cause ambiguity with hostname
    25  	// prefixes. It does not anchor the start or end so it can be composed into
    26  	// more complex RegExps below. Alphanumeric with - and _ allowed in non
    27  	// leading or trailing positions. Max length 64 chars. (GitHub username is
    28  	// 38 max.)
    29  	nameSubRe = "[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?"
    30  
    31  	// providerSubRe is the sub-expression that matches a valid provider. It
    32  	// does not anchor the start or end so it can be composed into more complex
    33  	// RegExps below. Only lowercase chars and digits are supported in practice.
    34  	// Max length 64 chars.
    35  	providerSubRe = "[0-9a-z]{1,64}"
    36  
    37  	// moduleSourceRe is a regular expression that matches the basic
    38  	// namespace/name/provider[//...] format for registry sources. It assumes
    39  	// any FriendlyHost prefix has already been removed if present.
    40  	moduleSourceRe = regexp.MustCompile(
    41  		fmt.Sprintf("^(%s)\\/(%s)\\/(%s)(?:\\/\\/(.*))?$",
    42  			nameSubRe, nameSubRe, providerSubRe))
    43  
    44  	// NameRe is a regular expression defining the format allowed for namespace
    45  	// or name fields in module registry implementations.
    46  	NameRe = regexp.MustCompile("^" + nameSubRe + "$")
    47  
    48  	// ProviderRe is a regular expression defining the format allowed for
    49  	// provider fields in module registry implementations.
    50  	ProviderRe = regexp.MustCompile("^" + providerSubRe + "$")
    51  
    52  	// these hostnames are not allowed as registry sources, because they are
    53  	// already special case module sources in tofu.
    54  	disallowed = map[string]bool{
    55  		"github.com":    true,
    56  		"bitbucket.org": true,
    57  	}
    58  )
    59  
    60  // Module describes a OpenTofu Registry Module source.
    61  type Module struct {
    62  	// RawHost is the friendly host prefix if one was present. It might be nil
    63  	// if the original source had no host prefix which implies
    64  	// PublicRegistryHost but is distinct from having an actual pointer to
    65  	// PublicRegistryHost since it encodes the fact the original string didn't
    66  	// include a host prefix at all which is significant for recovering actual
    67  	// input not just normalized form. Most callers should access it with Host()
    68  	// which will return public registry host instance if it's nil.
    69  	RawHost      *FriendlyHost
    70  	RawNamespace string
    71  	RawName      string
    72  	RawProvider  string
    73  	RawSubmodule string
    74  }
    75  
    76  // NewModule construct a new module source from separate parts. Pass empty
    77  // string if host or submodule are not needed.
    78  func NewModule(host, namespace, name, provider, submodule string) (*Module, error) {
    79  	m := &Module{
    80  		RawNamespace: namespace,
    81  		RawName:      name,
    82  		RawProvider:  provider,
    83  		RawSubmodule: submodule,
    84  	}
    85  	if host != "" {
    86  		h := NewFriendlyHost(host)
    87  		if h != nil {
    88  			fmt.Println("HOST:", h)
    89  			if !h.Valid() || disallowed[h.Display()] {
    90  				return nil, ErrInvalidModuleSource
    91  			}
    92  		}
    93  		m.RawHost = h
    94  	}
    95  	return m, nil
    96  }
    97  
    98  // ModuleFromModuleSourceAddr is an adapter to automatically transform the
    99  // modern representation of registry module addresses,
   100  // addrs.ModuleSourceRegistry, into the legacy representation regsrc.Module.
   101  //
   102  // Note that the new-style model always does normalization during parsing and
   103  // does not preserve the raw user input at all, and so although the fields
   104  // of regsrc.Module are all called "Raw...", initializing a Module indirectly
   105  // through an addrs.ModuleSourceRegistry will cause those values to be the
   106  // normalized ones, not the raw user input.
   107  //
   108  // Use this only for temporary shims to call into existing code that still
   109  // uses regsrc.Module. Eventually all other subsystems should be updated to
   110  // use addrs.ModuleSourceRegistry instead, and then package regsrc can be
   111  // removed altogether.
   112  func ModuleFromModuleSourceAddr(addr addrs.ModuleSourceRegistry) *Module {
   113  	ret := ModuleFromRegistryPackageAddr(addr.Package)
   114  	ret.RawSubmodule = addr.Subdir
   115  	return ret
   116  }
   117  
   118  // ModuleFromRegistryPackageAddr is similar to ModuleFromModuleSourceAddr, but
   119  // it works with just the isolated registry package address, and not the
   120  // full source address.
   121  //
   122  // The practical implication of that is that RawSubmodule will always be
   123  // the empty string in results from this function, because "Submodule" maps
   124  // to "Subdir" and that's a module source address concept, not a module
   125  // package concept. In practice this typically doesn't matter because the
   126  // registry client ignores the RawSubmodule field anyway; that's a concern
   127  // for the higher-level module installer to deal with.
   128  func ModuleFromRegistryPackageAddr(addr addrs.ModuleRegistryPackage) *Module {
   129  	return &Module{
   130  		RawHost:      NewFriendlyHost(addr.Host.String()),
   131  		RawNamespace: addr.Namespace,
   132  		RawName:      addr.Name,
   133  		RawProvider:  addr.TargetSystem, // this field was never actually enforced to be a provider address, so now has a more general name
   134  	}
   135  }
   136  
   137  // ParseModuleSource attempts to parse source as a OpenTofu registry module
   138  // source. If the string is not found to be in a valid format,
   139  // ErrInvalidModuleSource is returned. Note that this can only be used on
   140  // "input" strings, e.g. either ones supplied by the user or potentially
   141  // normalised but in Display form (unicode). It will fail to parse a source with
   142  // a punycoded domain since this is not permitted input from a user. If you have
   143  // an already normalized string internally, you can compare it without parsing
   144  // by comparing with the normalized version of the subject with the normal
   145  // string equality operator.
   146  func ParseModuleSource(source string) (*Module, error) {
   147  	// See if there is a friendly host prefix.
   148  	host, rest := ParseFriendlyHost(source)
   149  	if host != nil {
   150  		if !host.Valid() || disallowed[host.Display()] {
   151  			return nil, ErrInvalidModuleSource
   152  		}
   153  	}
   154  
   155  	matches := moduleSourceRe.FindStringSubmatch(rest)
   156  	if len(matches) < 4 {
   157  		return nil, ErrInvalidModuleSource
   158  	}
   159  
   160  	m := &Module{
   161  		RawHost:      host,
   162  		RawNamespace: matches[1],
   163  		RawName:      matches[2],
   164  		RawProvider:  matches[3],
   165  	}
   166  
   167  	if len(matches) == 5 {
   168  		m.RawSubmodule = matches[4]
   169  	}
   170  
   171  	return m, nil
   172  }
   173  
   174  // Display returns the source formatted for display to the user in CLI or web
   175  // output.
   176  func (m *Module) Display() string {
   177  	return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Display()), false)
   178  }
   179  
   180  // Normalized returns the source formatted for internal reference or comparison.
   181  func (m *Module) Normalized() string {
   182  	return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Normalized()), false)
   183  }
   184  
   185  // String returns the source formatted as the user originally typed it assuming
   186  // it was parsed from user input.
   187  func (m *Module) String() string {
   188  	// Don't normalize public registry hostname - leave it exactly like the user
   189  	// input it.
   190  	hostPrefix := ""
   191  	if m.RawHost != nil {
   192  		hostPrefix = m.RawHost.String() + "/"
   193  	}
   194  	return m.formatWithPrefix(hostPrefix, true)
   195  }
   196  
   197  // Equal compares the module source against another instance taking
   198  // normalization into account.
   199  func (m *Module) Equal(other *Module) bool {
   200  	return m.Normalized() == other.Normalized()
   201  }
   202  
   203  // Host returns the FriendlyHost object describing which registry this module is
   204  // in. If the original source string had not host component this will return the
   205  // PublicRegistryHost.
   206  func (m *Module) Host() *FriendlyHost {
   207  	if m.RawHost == nil {
   208  		return PublicRegistryHost
   209  	}
   210  	return m.RawHost
   211  }
   212  
   213  func (m *Module) normalizedHostPrefix(host string) string {
   214  	if m.Host().Equal(PublicRegistryHost) {
   215  		return ""
   216  	}
   217  	return host + "/"
   218  }
   219  
   220  func (m *Module) formatWithPrefix(hostPrefix string, preserveCase bool) string {
   221  	suffix := ""
   222  	if m.RawSubmodule != "" {
   223  		suffix = "//" + m.RawSubmodule
   224  	}
   225  	str := fmt.Sprintf("%s%s/%s/%s%s", hostPrefix, m.RawNamespace, m.RawName,
   226  		m.RawProvider, suffix)
   227  
   228  	// lower case by default
   229  	if !preserveCase {
   230  		return strings.ToLower(str)
   231  	}
   232  	return str
   233  }
   234  
   235  // Module returns just the registry ID of the module, without a hostname or
   236  // suffix.
   237  func (m *Module) Module() string {
   238  	return fmt.Sprintf("%s/%s/%s", m.RawNamespace, m.RawName, m.RawProvider)
   239  }
   240  
   241  // SvcHost returns the svchost.Hostname for this module. Since FriendlyHost may
   242  // contain an invalid hostname, this also returns an error indicating if it
   243  // could be converted to a svchost.Hostname. If no host is specified, the
   244  // default PublicRegistryHost is returned.
   245  func (m *Module) SvcHost() (svchost.Hostname, error) {
   246  	if m.RawHost == nil {
   247  		return svchost.ForComparison(PublicRegistryHost.Raw)
   248  	}
   249  	return svchost.ForComparison(m.RawHost.Raw)
   250  }