2017-10-25 00:00:11 +02:00
|
|
|
package regsrc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
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))
|
2017-10-25 01:27:36 +02:00
|
|
|
|
|
|
|
// disallowed is a set of hostnames that have special usage in modules and
|
|
|
|
// can't be registry hosts
|
|
|
|
disallowed = map[string]bool{
|
|
|
|
"github.com": true,
|
|
|
|
"bitbucket.org": true,
|
|
|
|
}
|
2017-10-25 00:00:11 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
m := &Module{
|
|
|
|
RawNamespace: namespace,
|
|
|
|
RawName: name,
|
|
|
|
RawProvider: provider,
|
|
|
|
RawSubmodule: submodule,
|
|
|
|
}
|
|
|
|
if host != "" {
|
|
|
|
m.RawHost = NewFriendlyHost(host)
|
|
|
|
}
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
2017-10-25 01:27:36 +02:00
|
|
|
if host != nil {
|
|
|
|
if !host.Valid() || disallowed[host.Display()] {
|
|
|
|
return nil, ErrInvalidModuleSource
|
|
|
|
}
|
2017-10-25 00:00:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2017-10-25 23:32:43 +02:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2017-10-25 00:00:11 +02:00
|
|
|
// 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
|
|
|
|
}
|