github.com/hashicorp/terraform-plugin-sdk@v1.17.2/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 "github.com/hashicorp/terraform-plugin-sdk/httpclient" 14 "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" 15 "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" 16 tfversion "github.com/hashicorp/terraform-plugin-sdk/internal/version" 17 "github.com/hashicorp/terraform-svchost" 18 "github.com/hashicorp/terraform-svchost/auth" 19 "github.com/hashicorp/terraform-svchost/disco" 20 ) 21 22 // Disco return a *disco.Disco mapping registry.terraform.io, 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.TerraformUserAgent(tfversion.String())) 33 34 d.ForceHostServices(svchost.Hostname("registry.terraform.io"), 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 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() 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 w.Header().Set("X-Terraform-Get", location) 171 w.WriteHeader(http.StatusNoContent) 172 // no body 173 return 174 } 175 176 moduleVersions := func(w http.ResponseWriter, r *http.Request) { 177 p := strings.TrimLeft(r.URL.Path, "/") 178 re := regexp.MustCompile(`^([-a-z]+/\w+/\w+)/versions$`) 179 matches := re.FindStringSubmatch(p) 180 if len(matches) != 2 { 181 w.WriteHeader(http.StatusBadRequest) 182 return 183 } 184 185 // check for auth 186 if strings.Contains(matches[1], "private/") { 187 if !strings.Contains(r.Header.Get("Authorization"), testCred) { 188 http.Error(w, "", http.StatusForbidden) 189 } 190 } 191 192 name := matches[1] 193 versions, ok := testMods[name] 194 if !ok { 195 http.NotFound(w, r) 196 return 197 } 198 199 // only adding the single requested module for now 200 // this is the minimal that any regisry is epected to support 201 mpvs := &response.ModuleProviderVersions{ 202 Source: name, 203 } 204 205 for _, v := range versions { 206 mv := &response.ModuleVersion{ 207 Version: v.version, 208 } 209 mpvs.Versions = append(mpvs.Versions, mv) 210 } 211 212 resp := response.ModuleVersions{ 213 Modules: []*response.ModuleProviderVersions{mpvs}, 214 } 215 216 js, err := json.Marshal(resp) 217 if err != nil { 218 http.Error(w, err.Error(), http.StatusInternalServerError) 219 return 220 } 221 w.Header().Set("Content-Type", "application/json") 222 w.Write(js) 223 } 224 225 mux.Handle("/v1/modules/", 226 http.StripPrefix("/v1/modules/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 227 if strings.HasSuffix(r.URL.Path, "/download") { 228 moduleDownload(w, r) 229 return 230 } 231 232 if strings.HasSuffix(r.URL.Path, "/versions") { 233 moduleVersions(w, r) 234 return 235 } 236 237 http.NotFound(w, r) 238 })), 239 ) 240 241 providerDownload := func(w http.ResponseWriter, r *http.Request) { 242 p := strings.TrimLeft(r.URL.Path, "/") 243 v := strings.Split(string(p), "/") 244 245 if len(v) != 6 { 246 w.WriteHeader(http.StatusBadRequest) 247 return 248 } 249 250 name := fmt.Sprintf("%s/%s", v[0], v[1]) 251 252 providers, ok := testProviders[name] 253 if !ok { 254 http.NotFound(w, r) 255 return 256 } 257 258 // for this test / moment we will only return the one provider 259 loc := response.TerraformProviderPlatformLocation{ 260 DownloadURL: providers[0].url, 261 } 262 263 js, err := json.Marshal(loc) 264 if err != nil { 265 http.Error(w, err.Error(), http.StatusInternalServerError) 266 return 267 } 268 269 w.Header().Set("Content-Type", "application/json") 270 w.Write(js) 271 272 } 273 274 providerVersions := func(w http.ResponseWriter, r *http.Request) { 275 p := strings.TrimLeft(r.URL.Path, "/") 276 re := regexp.MustCompile(`^([-a-z]+/\w+)/versions$`) 277 matches := re.FindStringSubmatch(p) 278 279 if len(matches) != 2 { 280 w.WriteHeader(http.StatusBadRequest) 281 return 282 } 283 284 // check for auth 285 if strings.Contains(matches[1], "private/") { 286 if !strings.Contains(r.Header.Get("Authorization"), testCred) { 287 http.Error(w, "", http.StatusForbidden) 288 } 289 } 290 291 name := providerAlias(fmt.Sprintf("%s", matches[1])) 292 versions, ok := testProviders[name] 293 if !ok { 294 http.NotFound(w, r) 295 return 296 } 297 298 // only adding the single requested provider for now 299 // this is the minimal that any registry is expected to support 300 pvs := &response.TerraformProviderVersions{ 301 ID: name, 302 } 303 304 for _, v := range versions { 305 pv := &response.TerraformProviderVersion{ 306 Version: v.version, 307 } 308 pvs.Versions = append(pvs.Versions, pv) 309 } 310 311 js, err := json.Marshal(pvs) 312 if err != nil { 313 http.Error(w, err.Error(), http.StatusInternalServerError) 314 return 315 } 316 317 w.Header().Set("Content-Type", "application/json") 318 w.Write(js) 319 } 320 321 mux.Handle("/v1/providers/", 322 http.StripPrefix("/v1/providers/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 323 if strings.Contains(r.URL.Path, "/download") { 324 providerDownload(w, r) 325 return 326 } 327 328 if strings.HasSuffix(r.URL.Path, "/versions") { 329 providerVersions(w, r) 330 return 331 } 332 333 http.NotFound(w, r) 334 })), 335 ) 336 337 mux.HandleFunc("/.well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) { 338 w.Header().Set("Content-Type", "application/json") 339 io.WriteString(w, `{"modules.v1":"http://localhost/v1/modules/", "providers.v1":"http://localhost/v1/providers/"}`) 340 }) 341 return mux 342 } 343 344 // Registry returns an httptest server that mocks out some registry functionality. 345 func Registry() *httptest.Server { 346 return httptest.NewServer(mockRegHandler()) 347 }