| package regsrc |
| |
| import ( |
| "errors" |
| "fmt" |
| "regexp" |
| "strings" |
| |
| svchost "github.com/hashicorp/terraform-svchost" |
| "github.com/hashicorp/terraform/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 terraform. |
| disallowed = map[string]bool{ |
| "github.com": true, |
| "bitbucket.org": true, |
| } |
| ) |
| |
| // Module describes a Terraform 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.PackageAddr) |
| 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 Terraform 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) |
| } |