github.com/opentofu/opentofu@v1.7.1/internal/registry/regsrc/module.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package regsrc 7 8 import ( 9 "errors" 10 "fmt" 11 "regexp" 12 "strings" 13 14 svchost "github.com/hashicorp/terraform-svchost" 15 "github.com/opentofu/opentofu/internal/addrs" 16 ) 17 18 var ( 19 ErrInvalidModuleSource = errors.New("not a valid registry module source") 20 21 // nameSubRe is the sub-expression that matches a valid module namespace or 22 // name. It's strictly a super-set of what GitHub allows for user/org and 23 // repo names respectively, but more restrictive than our original repo-name 24 // regex which allowed periods but could cause ambiguity with hostname 25 // prefixes. It does not anchor the start or end so it can be composed into 26 // more complex RegExps below. Alphanumeric with - and _ allowed in non 27 // leading or trailing positions. Max length 64 chars. (GitHub username is 28 // 38 max.) 29 nameSubRe = "[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?" 30 31 // providerSubRe is the sub-expression that matches a valid provider. It 32 // does not anchor the start or end so it can be composed into more complex 33 // RegExps below. Only lowercase chars and digits are supported in practice. 34 // Max length 64 chars. 35 providerSubRe = "[0-9a-z]{1,64}" 36 37 // moduleSourceRe is a regular expression that matches the basic 38 // namespace/name/provider[//...] format for registry sources. It assumes 39 // any FriendlyHost prefix has already been removed if present. 40 moduleSourceRe = regexp.MustCompile( 41 fmt.Sprintf("^(%s)\\/(%s)\\/(%s)(?:\\/\\/(.*))?$", 42 nameSubRe, nameSubRe, providerSubRe)) 43 44 // NameRe is a regular expression defining the format allowed for namespace 45 // or name fields in module registry implementations. 46 NameRe = regexp.MustCompile("^" + nameSubRe + "$") 47 48 // ProviderRe is a regular expression defining the format allowed for 49 // provider fields in module registry implementations. 50 ProviderRe = regexp.MustCompile("^" + providerSubRe + "$") 51 52 // these hostnames are not allowed as registry sources, because they are 53 // already special case module sources in tofu. 54 disallowed = map[string]bool{ 55 "github.com": true, 56 "bitbucket.org": true, 57 } 58 ) 59 60 // Module describes a OpenTofu Registry Module source. 61 type Module struct { 62 // RawHost is the friendly host prefix if one was present. It might be nil 63 // if the original source had no host prefix which implies 64 // PublicRegistryHost but is distinct from having an actual pointer to 65 // PublicRegistryHost since it encodes the fact the original string didn't 66 // include a host prefix at all which is significant for recovering actual 67 // input not just normalized form. Most callers should access it with Host() 68 // which will return public registry host instance if it's nil. 69 RawHost *FriendlyHost 70 RawNamespace string 71 RawName string 72 RawProvider string 73 RawSubmodule string 74 } 75 76 // NewModule construct a new module source from separate parts. Pass empty 77 // string if host or submodule are not needed. 78 func NewModule(host, namespace, name, provider, submodule string) (*Module, error) { 79 m := &Module{ 80 RawNamespace: namespace, 81 RawName: name, 82 RawProvider: provider, 83 RawSubmodule: submodule, 84 } 85 if host != "" { 86 h := NewFriendlyHost(host) 87 if h != nil { 88 fmt.Println("HOST:", h) 89 if !h.Valid() || disallowed[h.Display()] { 90 return nil, ErrInvalidModuleSource 91 } 92 } 93 m.RawHost = h 94 } 95 return m, nil 96 } 97 98 // ModuleFromModuleSourceAddr is an adapter to automatically transform the 99 // modern representation of registry module addresses, 100 // addrs.ModuleSourceRegistry, into the legacy representation regsrc.Module. 101 // 102 // Note that the new-style model always does normalization during parsing and 103 // does not preserve the raw user input at all, and so although the fields 104 // of regsrc.Module are all called "Raw...", initializing a Module indirectly 105 // through an addrs.ModuleSourceRegistry will cause those values to be the 106 // normalized ones, not the raw user input. 107 // 108 // Use this only for temporary shims to call into existing code that still 109 // uses regsrc.Module. Eventually all other subsystems should be updated to 110 // use addrs.ModuleSourceRegistry instead, and then package regsrc can be 111 // removed altogether. 112 func ModuleFromModuleSourceAddr(addr addrs.ModuleSourceRegistry) *Module { 113 ret := ModuleFromRegistryPackageAddr(addr.Package) 114 ret.RawSubmodule = addr.Subdir 115 return ret 116 } 117 118 // ModuleFromRegistryPackageAddr is similar to ModuleFromModuleSourceAddr, but 119 // it works with just the isolated registry package address, and not the 120 // full source address. 121 // 122 // The practical implication of that is that RawSubmodule will always be 123 // the empty string in results from this function, because "Submodule" maps 124 // to "Subdir" and that's a module source address concept, not a module 125 // package concept. In practice this typically doesn't matter because the 126 // registry client ignores the RawSubmodule field anyway; that's a concern 127 // for the higher-level module installer to deal with. 128 func ModuleFromRegistryPackageAddr(addr addrs.ModuleRegistryPackage) *Module { 129 return &Module{ 130 RawHost: NewFriendlyHost(addr.Host.String()), 131 RawNamespace: addr.Namespace, 132 RawName: addr.Name, 133 RawProvider: addr.TargetSystem, // this field was never actually enforced to be a provider address, so now has a more general name 134 } 135 } 136 137 // ParseModuleSource attempts to parse source as a OpenTofu registry module 138 // source. If the string is not found to be in a valid format, 139 // ErrInvalidModuleSource is returned. Note that this can only be used on 140 // "input" strings, e.g. either ones supplied by the user or potentially 141 // normalised but in Display form (unicode). It will fail to parse a source with 142 // a punycoded domain since this is not permitted input from a user. If you have 143 // an already normalized string internally, you can compare it without parsing 144 // by comparing with the normalized version of the subject with the normal 145 // string equality operator. 146 func ParseModuleSource(source string) (*Module, error) { 147 // See if there is a friendly host prefix. 148 host, rest := ParseFriendlyHost(source) 149 if host != nil { 150 if !host.Valid() || disallowed[host.Display()] { 151 return nil, ErrInvalidModuleSource 152 } 153 } 154 155 matches := moduleSourceRe.FindStringSubmatch(rest) 156 if len(matches) < 4 { 157 return nil, ErrInvalidModuleSource 158 } 159 160 m := &Module{ 161 RawHost: host, 162 RawNamespace: matches[1], 163 RawName: matches[2], 164 RawProvider: matches[3], 165 } 166 167 if len(matches) == 5 { 168 m.RawSubmodule = matches[4] 169 } 170 171 return m, nil 172 } 173 174 // Display returns the source formatted for display to the user in CLI or web 175 // output. 176 func (m *Module) Display() string { 177 return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Display()), false) 178 } 179 180 // Normalized returns the source formatted for internal reference or comparison. 181 func (m *Module) Normalized() string { 182 return m.formatWithPrefix(m.normalizedHostPrefix(m.Host().Normalized()), false) 183 } 184 185 // String returns the source formatted as the user originally typed it assuming 186 // it was parsed from user input. 187 func (m *Module) String() string { 188 // Don't normalize public registry hostname - leave it exactly like the user 189 // input it. 190 hostPrefix := "" 191 if m.RawHost != nil { 192 hostPrefix = m.RawHost.String() + "/" 193 } 194 return m.formatWithPrefix(hostPrefix, true) 195 } 196 197 // Equal compares the module source against another instance taking 198 // normalization into account. 199 func (m *Module) Equal(other *Module) bool { 200 return m.Normalized() == other.Normalized() 201 } 202 203 // Host returns the FriendlyHost object describing which registry this module is 204 // in. If the original source string had not host component this will return the 205 // PublicRegistryHost. 206 func (m *Module) Host() *FriendlyHost { 207 if m.RawHost == nil { 208 return PublicRegistryHost 209 } 210 return m.RawHost 211 } 212 213 func (m *Module) normalizedHostPrefix(host string) string { 214 if m.Host().Equal(PublicRegistryHost) { 215 return "" 216 } 217 return host + "/" 218 } 219 220 func (m *Module) formatWithPrefix(hostPrefix string, preserveCase bool) string { 221 suffix := "" 222 if m.RawSubmodule != "" { 223 suffix = "//" + m.RawSubmodule 224 } 225 str := fmt.Sprintf("%s%s/%s/%s%s", hostPrefix, m.RawNamespace, m.RawName, 226 m.RawProvider, suffix) 227 228 // lower case by default 229 if !preserveCase { 230 return strings.ToLower(str) 231 } 232 return str 233 } 234 235 // Module returns just the registry ID of the module, without a hostname or 236 // suffix. 237 func (m *Module) Module() string { 238 return fmt.Sprintf("%s/%s/%s", m.RawNamespace, m.RawName, m.RawProvider) 239 } 240 241 // SvcHost returns the svchost.Hostname for this module. Since FriendlyHost may 242 // contain an invalid hostname, this also returns an error indicating if it 243 // could be converted to a svchost.Hostname. If no host is specified, the 244 // default PublicRegistryHost is returned. 245 func (m *Module) SvcHost() (svchost.Hostname, error) { 246 if m.RawHost == nil { 247 return svchost.ForComparison(PublicRegistryHost.Raw) 248 } 249 return svchost.ForComparison(m.RawHost.Raw) 250 }