github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/registry/regsrc/module.go (about)

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