github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/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/cycloidio/terraform/httpclient"
    17  	"github.com/cycloidio/terraform/registry/regsrc"
    18  	"github.com/cycloidio/terraform/registry/response"
    19  	tfversion "github.com/cycloidio/terraform/version"
    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  	}
   174  
   175  	moduleVersions := func(w http.ResponseWriter, r *http.Request) {
   176  		p := strings.TrimLeft(r.URL.Path, "/")
   177  		re := regexp.MustCompile(`^([-a-z]+/\w+/\w+)/versions$`)
   178  		matches := re.FindStringSubmatch(p)
   179  		if len(matches) != 2 {
   180  			w.WriteHeader(http.StatusBadRequest)
   181  			return
   182  		}
   183  
   184  		// check for auth
   185  		if strings.Contains(matches[1], "private/") {
   186  			if !strings.Contains(r.Header.Get("Authorization"), testCred) {
   187  				http.Error(w, "", http.StatusForbidden)
   188  			}
   189  		}
   190  
   191  		name := matches[1]
   192  		versions, ok := testMods[name]
   193  		if !ok {
   194  			http.NotFound(w, r)
   195  			return
   196  		}
   197  
   198  		// only adding the single requested module for now
   199  		// this is the minimal that any regisry is epected to support
   200  		mpvs := &response.ModuleProviderVersions{
   201  			Source: name,
   202  		}
   203  
   204  		for _, v := range versions {
   205  			mv := &response.ModuleVersion{
   206  				Version: v.version,
   207  			}
   208  			mpvs.Versions = append(mpvs.Versions, mv)
   209  		}
   210  
   211  		resp := response.ModuleVersions{
   212  			Modules: []*response.ModuleProviderVersions{mpvs},
   213  		}
   214  
   215  		js, err := json.Marshal(resp)
   216  		if err != nil {
   217  			http.Error(w, err.Error(), http.StatusInternalServerError)
   218  			return
   219  		}
   220  		w.Header().Set("Content-Type", "application/json")
   221  		w.Write(js)
   222  	}
   223  
   224  	mux.Handle("/v1/modules/",
   225  		http.StripPrefix("/v1/modules/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   226  			if strings.HasSuffix(r.URL.Path, "/download") {
   227  				moduleDownload(w, r)
   228  				return
   229  			}
   230  
   231  			if strings.HasSuffix(r.URL.Path, "/versions") {
   232  				moduleVersions(w, r)
   233  				return
   234  			}
   235  
   236  			http.NotFound(w, r)
   237  		})),
   238  	)
   239  
   240  	mux.HandleFunc("/.well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
   241  		w.Header().Set("Content-Type", "application/json")
   242  		io.WriteString(w, `{"modules.v1":"http://localhost/v1/modules/", "providers.v1":"http://localhost/v1/providers/"}`)
   243  	})
   244  	return mux
   245  }
   246  
   247  // Registry returns an httptest server that mocks out some registry functionality.
   248  func Registry() *httptest.Server {
   249  	return httptest.NewServer(mockRegHandler())
   250  }
   251  
   252  // RegistryRetryableErrorsServer returns an httptest server that mocks out the
   253  // registry API to return 502 errors.
   254  func RegistryRetryableErrorsServer() *httptest.Server {
   255  	mux := http.NewServeMux()
   256  	mux.HandleFunc("/v1/modules/", func(w http.ResponseWriter, r *http.Request) {
   257  		http.Error(w, "mocked server error", http.StatusBadGateway)
   258  	})
   259  	mux.HandleFunc("/v1/providers/", func(w http.ResponseWriter, r *http.Request) {
   260  		http.Error(w, "mocked server error", http.StatusBadGateway)
   261  	})
   262  	return httptest.NewServer(mux)
   263  }