github.com/magodo/terraform@v0.11.12-beta1/registry/regsrc/module.go (about) 1 package regsrc 2 3 import ( 4 "errors" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "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 // NameRe is a regular expression defining the format allowed for namespace 39 // or name fields in module registry implementations. 40 NameRe = regexp.MustCompile("^" + nameSubRe + "$") 41 42 // ProviderRe is a regular expression defining the format allowed for 43 // provider fields in module registry implementations. 44 ProviderRe = regexp.MustCompile("^" + providerSubRe + "$") 45 46 // these hostnames are not allowed as registry sources, because they are 47 // already special case module sources in terraform. 48 disallowed = map[string]bool{ 49 "github.com": true, 50 "bitbucket.org": true, 51 } 52 ) 53 54 // Module describes a Terraform Registry Module source. 55 type Module struct { 56 // RawHost is the friendly host prefix if one was present. It might be nil 57 // if the original source had no host prefix which implies 58 // PublicRegistryHost but is distinct from having an actual pointer to 59 // PublicRegistryHost since it encodes the fact the original string didn't 60 // include a host prefix at all which is significant for recovering actual 61 // input not just normalized form. Most callers should access it with Host() 62 // which will return public registry host instance if it's nil. 63 RawHost *FriendlyHost 64 RawNamespace string 65 RawName string 66 RawProvider string 67 RawSubmodule string 68 } 69 70 // NewModule construct a new module source from separate parts. Pass empty 71 // string if host or submodule are not needed. 72 func NewModule(host, namespace, name, provider, submodule string) (*Module, error) { 73 m := &Module{ 74 RawNamespace: namespace, 75 RawName: name, 76 RawProvider: provider, 77 RawSubmodule: submodule, 78 } 79 if host != "" { 80 h := NewFriendlyHost(host) 81 if h != nil { 82 fmt.Println("HOST:", h) 83 if !h.Valid() || disallowed[h.Display()] { 84 return nil, ErrInvalidModuleSource 85 } 86 } 87 m.RawHost = h 88 } 89 return m, nil 90 } 91 92 // ParseModuleSource attempts to parse source as a Terraform registry module 93 // source. If the string is not found to be in a valid format, 94 // ErrInvalidModuleSource is returned. Note that this can only be used on 95 // "input" strings, e.g. either ones supplied by the user or potentially 96 // normalised but in Display form (unicode). It will fail to parse a source with 97 // a punycoded domain since this is not permitted input from a user. If you have 98 // an already normalized string internally, you can compare it without parsing 99 // by comparing with the normalized version of the subject with the normal 100 // string equality operator. 101 func ParseModuleSource(source string) (*Module, error) { 102 // See if there is a friendly host prefix. 103 host, rest := ParseFriendlyHost(source) 104 if host != nil { 105 if !host.Valid() || disallowed[host.Display()] { 106 return nil, ErrInvalidModuleSource 107 } 108 } 109 110 matches := moduleSourceRe.FindStringSubmatch(rest) 111 if len(matches) < 4 { 112 return nil, ErrInvalidModuleSource 113 } 114 115 m := &Module{ 116 RawHost: host, 117 RawNamespace: matches[1], 118 RawName: matches[2], 119 RawProvider: matches[3], 120 } 121 122 if len(matches) == 5 { 123 m.RawSubmodule = matches[4] 124 } 125 126 return m, nil 127 } 128 129 // Display returns the source formatted for display to the user in CLI or web 130 // output. 131 func (m *Module) Display() string { 132 return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Display()), false) 133 } 134 135 // Normalized returns the source formatted for internal reference or comparison. 136 func (m *Module) Normalized() string { 137 return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Normalized()), false) 138 } 139 140 // String returns the source formatted as the user originally typed it assuming 141 // it was parsed from user input. 142 func (m *Module) String() string { 143 // Don't normalize public registry hostname - leave it exactly like the user 144 // input it. 145 hostPrefix := "" 146 if m.RawHost != nil { 147 hostPrefix = m.RawHost.String() + "/" 148 } 149 return m.formatWithPrefix(hostPrefix, true) 150 } 151 152 // Equal compares the module source against another instance taking 153 // normalization into account. 154 func (m *Module) Equal(other *Module) bool { 155 return m.Normalized() == other.Normalized() 156 } 157 158 // Host returns the FriendlyHost object describing which registry this module is 159 // in. If the original source string had not host component this will return the 160 // PublicRegistryHost. 161 func (m *Module) Host() *FriendlyHost { 162 if m.RawHost == nil { 163 return PublicRegistryHost 164 } 165 return m.RawHost 166 } 167 168 func (m *Module) normalizedHostPrefix(host string) string { 169 if m.Host().Equal(PublicRegistryHost) { 170 return "" 171 } 172 return host + "/" 173 } 174 175 func (m *Module) formatWithPrefix(hostPrefix string, preserveCase bool) string { 176 suffix := "" 177 if m.RawSubmodule != "" { 178 suffix = "//" + m.RawSubmodule 179 } 180 str := fmt.Sprintf("%s%s/%s/%s%s", hostPrefix, m.RawNamespace, m.RawName, 181 m.RawProvider, suffix) 182 183 // lower case by default 184 if !preserveCase { 185 return strings.ToLower(str) 186 } 187 return str 188 } 189 190 // Module returns just the registry ID of the module, without a hostname or 191 // suffix. 192 func (m *Module) Module() string { 193 return fmt.Sprintf("%s/%s/%s", m.RawNamespace, m.RawName, m.RawProvider) 194 } 195 196 // SvcHost returns the svchost.Hostname for this module. Since FriendlyHost may 197 // contain an invalid hostname, this also returns an error indicating if it 198 // could be converted to a svchost.Hostname. If no host is specified, the 199 // default PublicRegistryHost is returned. 200 func (m *Module) SvcHost() (svchost.Hostname, error) { 201 if m.RawHost == nil { 202 return svchost.ForComparison(PublicRegistryHost.Raw) 203 } 204 return svchost.ForComparison(m.RawHost.Raw) 205 }