github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/registry/regsrc/module.go (about) 1 package regsrc 2 3 import ( 4 "errors" 5 "fmt" 6 "regexp" 7 "strings" 8 9 svchost "github.com/hashicorp/terraform-svchost" 10 ) 11 12 var ( 13 ErrInvalidModuleSource = errors.New("not a valid registry module source") 14 15 // nameSubRe is the sub-expression that matches a valid module namespace or 16 // name. It's strictly a super-set of what GitHub allows for user/org and 17 // repo names respectively, but more restrictive than our original repo-name 18 // regex which allowed periods but could cause ambiguity with hostname 19 // prefixes. It does not anchor the start or end so it can be composed into 20 // more complex RegExps below. Alphanumeric with - and _ allowed in non 21 // leading or trailing positions. Max length 64 chars. (GitHub username is 22 // 38 max.) 23 nameSubRe = "[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?" 24 25 // providerSubRe is the sub-expression that matches a valid provider. It 26 // does not anchor the start or end so it can be composed into more complex 27 // RegExps below. Only lowercase chars and digits are supported in practice. 28 // Max length 64 chars. 29 providerSubRe = "[0-9a-z]{1,64}" 30 31 // moduleSourceRe is a regular expression that matches the basic 32 // namespace/name/provider[//...] format for registry sources. It assumes 33 // any FriendlyHost prefix has already been removed if present. 34 moduleSourceRe = regexp.MustCompile( 35 fmt.Sprintf("^(%s)\\/(%s)\\/(%s)(?:\\/\\/(.*))?$", 36 nameSubRe, nameSubRe, providerSubRe)) 37 38 // these hostnames are not allowed as registry sources, because they are 39 // already special case module sources in terraform. 40 disallowed = map[string]bool{ 41 "github.com": true, 42 "bitbucket.org": true, 43 } 44 ) 45 46 // Module describes a Terraform Registry Module source. 47 type Module struct { 48 // RawHost is the friendly host prefix if one was present. It might be nil 49 // if the original source had no host prefix which implies 50 // PublicRegistryHost but is distinct from having an actual pointer to 51 // PublicRegistryHost since it encodes the fact the original string didn't 52 // include a host prefix at all which is significant for recovering actual 53 // input not just normalized form. Most callers should access it with Host() 54 // which will return public registry host instance if it's nil. 55 RawHost *FriendlyHost 56 RawNamespace string 57 RawName string 58 RawProvider string 59 RawSubmodule string 60 } 61 62 // ParseModuleSource attempts to parse source as a Terraform registry module 63 // source. If the string is not found to be in a valid format, 64 // ErrInvalidModuleSource is returned. Note that this can only be used on 65 // "input" strings, e.g. either ones supplied by the user or potentially 66 // normalised but in Display form (unicode). It will fail to parse a source with 67 // a punycoded domain since this is not permitted input from a user. If you have 68 // an already normalized string internally, you can compare it without parsing 69 // by comparing with the normalized version of the subject with the normal 70 // string equality operator. 71 func ParseModuleSource(source string) (*Module, error) { 72 // See if there is a friendly host prefix. 73 host, rest := ParseFriendlyHost(source) 74 if host != nil { 75 if !host.Valid() || disallowed[host.Display()] { 76 return nil, ErrInvalidModuleSource 77 } 78 } 79 80 matches := moduleSourceRe.FindStringSubmatch(rest) 81 if len(matches) < 4 { 82 return nil, ErrInvalidModuleSource 83 } 84 85 m := &Module{ 86 RawHost: host, 87 RawNamespace: matches[1], 88 RawName: matches[2], 89 RawProvider: matches[3], 90 } 91 92 if len(matches) == 5 { 93 m.RawSubmodule = matches[4] 94 } 95 96 return m, nil 97 } 98 99 // Display returns the source formatted for display to the user in CLI or web 100 // output. 101 func (m *Module) Display() string { 102 return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Display()), false) 103 } 104 105 // Normalized returns the source formatted for internal reference or comparison. 106 func (m *Module) Normalized() string { 107 return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Normalized()), false) 108 } 109 110 // String returns the source formatted as the user originally typed it assuming 111 // it was parsed from user input. 112 func (m *Module) String() string { 113 // Don't normalize public registry hostname - leave it exactly like the user 114 // input it. 115 hostPrefix := "" 116 if m.RawHost != nil { 117 hostPrefix = m.RawHost.String() + "/" 118 } 119 return m.formatWithPrefix(hostPrefix, true) 120 } 121 122 // Equal compares the module source against another instance taking 123 // normalization into account. 124 func (m *Module) Equal(other *Module) bool { 125 return m.Normalized() == other.Normalized() 126 } 127 128 // Host returns the FriendlyHost object describing which registry this module is 129 // in. If the original source string had not host component this will return the 130 // PublicRegistryHost. 131 func (m *Module) Host() *FriendlyHost { 132 if m.RawHost == nil { 133 return PublicRegistryHost 134 } 135 return m.RawHost 136 } 137 138 func (m *Module) normalizedHostPrefix(host string) string { 139 if m.Host().Equal(PublicRegistryHost) { 140 return "" 141 } 142 return host + "/" 143 } 144 145 func (m *Module) formatWithPrefix(hostPrefix string, preserveCase bool) string { 146 suffix := "" 147 if m.RawSubmodule != "" { 148 suffix = "//" + m.RawSubmodule 149 } 150 str := fmt.Sprintf("%s%s/%s/%s%s", hostPrefix, m.RawNamespace, m.RawName, 151 m.RawProvider, suffix) 152 153 // lower case by default 154 if !preserveCase { 155 return strings.ToLower(str) 156 } 157 return str 158 } 159 160 // Module returns just the registry ID of the module, without a hostname or 161 // suffix. 162 func (m *Module) Module() string { 163 return fmt.Sprintf("%s/%s/%s", m.RawNamespace, m.RawName, m.RawProvider) 164 } 165 166 // SvcHost returns the svchost.Hostname for this module. Since FriendlyHost may 167 // contain an invalid hostname, this also returns an error indicating if it 168 // could be converted to a svchost.Hostname. If no host is specified, the 169 // default PublicRegistryHost is returned. 170 func (m *Module) SvcHost() (svchost.Hostname, error) { 171 if m.RawHost == nil { 172 return svchost.ForComparison(PublicRegistryHost.Raw) 173 } 174 return svchost.ForComparison(m.RawHost.Raw) 175 }