github.com/opentofu/opentofu@v1.7.1/internal/registry/test/mock_registry.go (about) 1 package test 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "regexp" 11 "strings" 12 13 svchost "github.com/hashicorp/terraform-svchost" 14 "github.com/hashicorp/terraform-svchost/auth" 15 "github.com/hashicorp/terraform-svchost/disco" 16 "github.com/opentofu/opentofu/internal/httpclient" 17 "github.com/opentofu/opentofu/internal/registry/regsrc" 18 "github.com/opentofu/opentofu/internal/registry/response" 19 tfversion "github.com/opentofu/opentofu/version" 20 ) 21 22 // Disco return a *disco.Disco mapping registry.opentofu.org, localhost, 23 // localhost.localdomain, and example.com to the test server. 24 func Disco(s *httptest.Server) *disco.Disco { 25 services := map[string]interface{}{ 26 // Note that both with and without trailing slashes are supported behaviours 27 // TODO: add specific tests to enumerate both possibilities. 28 "modules.v1": fmt.Sprintf("%s/v1/modules", s.URL), 29 "providers.v1": fmt.Sprintf("%s/v1/providers", s.URL), 30 } 31 d := disco.NewWithCredentialsSource(credsSrc) 32 d.SetUserAgent(httpclient.OpenTofuUserAgent(tfversion.String())) 33 34 d.ForceHostServices(svchost.Hostname("registry.opentofu.org"), services) 35 d.ForceHostServices(svchost.Hostname("localhost"), services) 36 d.ForceHostServices(svchost.Hostname("localhost.localdomain"), services) 37 d.ForceHostServices(svchost.Hostname("example.com"), services) 38 return d 39 } 40 41 // Map of module names and location of test modules. 42 // Only one version for now, as we only lookup latest from the registry. 43 type testMod struct { 44 location string 45 version string 46 } 47 48 // Map of provider names and location of test providers. 49 // Only one version for now, as we only lookup latest from the registry. 50 type testProvider struct { 51 version string 52 url string 53 } 54 55 const ( 56 testCred = "test-auth-token" 57 ) 58 59 var ( 60 regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized()) 61 credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ 62 regHost: {"token": testCred}, 63 }) 64 ) 65 66 // All the locationes from the mockRegistry start with a file:// scheme. If 67 // the location string here doesn't have a scheme, the mockRegistry will 68 // find the absolute path and return a complete URL. 69 var testMods = map[string][]testMod{ 70 "registry/foo/bar": {{ 71 location: "file:///download/registry/foo/bar/0.2.3//*?archive=tar.gz", 72 version: "0.2.3", 73 }}, 74 "registry/foo/baz": {{ 75 location: "file:///download/registry/foo/baz/1.10.0//*?archive=tar.gz", 76 version: "1.10.0", 77 }}, 78 "registry/local/sub": {{ 79 location: "testdata/registry-tar-subdir/foo.tgz//*?archive=tar.gz", 80 version: "0.1.2", 81 }}, 82 "exists-in-registry/identifier/provider": {{ 83 location: "file:///registry/exists", 84 version: "0.2.0", 85 }}, 86 "relative/foo/bar": {{ // There is an exception for the "relative/" prefix in the test registry server 87 location: "/relative-path", 88 version: "0.2.0", 89 }}, 90 "test-versions/name/provider": { 91 {version: "2.2.0"}, 92 {version: "2.1.1"}, 93 {version: "1.2.2"}, 94 {version: "1.2.1"}, 95 }, 96 "private/name/provider": { 97 {version: "1.0.0"}, 98 }, 99 } 100 101 var testProviders = map[string][]testProvider{ 102 "-/foo": { 103 { 104 version: "0.2.3", 105 url: "https://releases.hashicorp.com/terraform-provider-foo/0.2.3/terraform-provider-foo.zip", 106 }, 107 {version: "0.3.0"}, 108 }, 109 "-/bar": { 110 { 111 version: "0.1.1", 112 url: "https://releases.hashicorp.com/terraform-provider-bar/0.1.1/terraform-provider-bar.zip", 113 }, 114 {version: "0.1.2"}, 115 }, 116 } 117 118 func providerAlias(provider string) string { 119 re := regexp.MustCompile("^-/") 120 if re.MatchString(provider) { 121 return re.ReplaceAllString(provider, "terraform-providers/") 122 } 123 return provider 124 } 125 126 func init() { 127 // Add provider aliases 128 for provider, info := range testProviders { 129 alias := providerAlias(provider) 130 testProviders[alias] = info 131 } 132 } 133 134 func mockRegHandler(config map[uint8]struct{}) http.Handler { 135 mux := http.NewServeMux() 136 137 moduleDownload := func(w http.ResponseWriter, r *http.Request) { 138 p := strings.TrimLeft(r.URL.Path, "/") 139 // handle download request 140 re := regexp.MustCompile(`^([-a-z]+/\w+/\w+).*/download$`) 141 // download lookup 142 matches := re.FindStringSubmatch(p) 143 if len(matches) != 2 { 144 w.WriteHeader(http.StatusBadRequest) 145 return 146 } 147 148 // check for auth 149 if strings.Contains(matches[0], "private/") { 150 if !strings.Contains(r.Header.Get("Authorization"), testCred) { 151 http.Error(w, "", http.StatusForbidden) 152 return 153 } 154 } 155 156 versions, ok := testMods[matches[1]] 157 if !ok { 158 http.NotFound(w, r) 159 return 160 } 161 mod := versions[0] 162 163 location := mod.location 164 if !strings.HasPrefix(matches[0], "relative/") && !strings.HasPrefix(location, "file:///") { 165 // we can't use filepath.Abs because it will clean `//` 166 wd, _ := os.Getwd() 167 location = fmt.Sprintf("file://%s/%s", wd, location) 168 } 169 170 // the location will be returned in the response header 171 _, inHeader := config[WithModuleLocationInHeader] 172 // the location will be returned in the response body 173 _, inBody := config[WithModuleLocationInBody] 174 175 if inHeader { 176 w.Header().Set("X-Terraform-Get", location) 177 } 178 179 if inBody { 180 w.WriteHeader(http.StatusOK) 181 o, err := json.Marshal(response.ModuleLocationRegistryResp{Location: location}) 182 if err != nil { 183 panic("mock error: " + err.Error()) 184 } 185 _, _ = w.Write(o) 186 return 187 } 188 189 w.WriteHeader(http.StatusNoContent) 190 } 191 192 moduleVersions := func(w http.ResponseWriter, r *http.Request) { 193 p := strings.TrimLeft(r.URL.Path, "/") 194 re := regexp.MustCompile(`^([-a-z]+/\w+/\w+)/versions$`) 195 matches := re.FindStringSubmatch(p) 196 if len(matches) != 2 { 197 w.WriteHeader(http.StatusBadRequest) 198 return 199 } 200 201 // check for auth 202 if strings.Contains(matches[1], "private/") { 203 if !strings.Contains(r.Header.Get("Authorization"), testCred) { 204 http.Error(w, "", http.StatusForbidden) 205 } 206 } 207 208 name := matches[1] 209 versions, ok := testMods[name] 210 if !ok { 211 http.NotFound(w, r) 212 return 213 } 214 215 // only adding the single requested module for now 216 // this is the minimal that any regisry is epected to support 217 mpvs := &response.ModuleProviderVersions{ 218 Source: name, 219 } 220 221 for _, v := range versions { 222 mv := &response.ModuleVersion{ 223 Version: v.version, 224 } 225 mpvs.Versions = append(mpvs.Versions, mv) 226 } 227 228 resp := response.ModuleVersions{ 229 Modules: []*response.ModuleProviderVersions{mpvs}, 230 } 231 232 js, err := json.Marshal(resp) 233 if err != nil { 234 http.Error(w, err.Error(), http.StatusInternalServerError) 235 return 236 } 237 w.Header().Set("Content-Type", "application/json") 238 w.Write(js) 239 } 240 241 mux.Handle("/v1/modules/", 242 http.StripPrefix("/v1/modules/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 243 if strings.HasSuffix(r.URL.Path, "/download") { 244 moduleDownload(w, r) 245 return 246 } 247 248 if strings.HasSuffix(r.URL.Path, "/versions") { 249 moduleVersions(w, r) 250 return 251 } 252 253 http.NotFound(w, r) 254 })), 255 ) 256 257 mux.HandleFunc("/.well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) { 258 w.Header().Set("Content-Type", "application/json") 259 io.WriteString(w, `{"modules.v1":"http://localhost/v1/modules/", "providers.v1":"http://localhost/v1/providers/"}`) 260 }) 261 return mux 262 } 263 264 const ( 265 // WithModuleLocationInBody sets to return the module's location in the response body 266 WithModuleLocationInBody uint8 = iota 267 // WithModuleLocationInHeader sets to return the module's location in the response header 268 WithModuleLocationInHeader 269 ) 270 271 // Registry returns an httptest server that mocks out some registry functionality. 272 func Registry(flags ...uint8) *httptest.Server { 273 if len(flags) == 0 { 274 return httptest.NewServer(mockRegHandler( 275 map[uint8]struct{}{ 276 // default setting 277 WithModuleLocationInBody: {}, 278 }, 279 )) 280 } 281 282 cfg := map[uint8]struct{}{} 283 for _, flag := range flags { 284 cfg[flag] = struct{}{} 285 } 286 return httptest.NewServer(mockRegHandler(cfg)) 287 } 288 289 // RegistryRetryableErrorsServer returns an httptest server that mocks out the 290 // registry API to return 502 errors. 291 func RegistryRetryableErrorsServer() *httptest.Server { 292 mux := http.NewServeMux() 293 mux.HandleFunc("/v1/modules/", func(w http.ResponseWriter, r *http.Request) { 294 http.Error(w, "mocked server error", http.StatusBadGateway) 295 }) 296 mux.HandleFunc("/v1/providers/", func(w http.ResponseWriter, r *http.Request) { 297 http.Error(w, "mocked server error", http.StatusBadGateway) 298 }) 299 return httptest.NewServer(mux) 300 }