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 }