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  }