Create a registry.Client
This moves the registry specific functionality out of the module.Storage and into its own package.
This commit is contained in:
parent
23d21b373e
commit
91bd72f22b
|
@ -0,0 +1,234 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||||
|
"github.com/hashicorp/terraform/registry/regsrc"
|
||||||
|
"github.com/hashicorp/terraform/registry/response"
|
||||||
|
"github.com/hashicorp/terraform/svchost"
|
||||||
|
"github.com/hashicorp/terraform/svchost/auth"
|
||||||
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
|
"github.com/hashicorp/terraform/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xTerraformGet = "X-Terraform-Get"
|
||||||
|
xTerraformVersion = "X-Terraform-Version"
|
||||||
|
requestTimeout = 10 * time.Second
|
||||||
|
serviceID = "modules.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tfVersion = version.String()
|
||||||
|
|
||||||
|
// Client provides methods to query Terraform Registries.
|
||||||
|
type Client struct {
|
||||||
|
// this is the client to be used for all requests.
|
||||||
|
client *http.Client
|
||||||
|
|
||||||
|
// services is a required *disco.Disco, which may have services and
|
||||||
|
// credentials pre-loaded.
|
||||||
|
services *disco.Disco
|
||||||
|
|
||||||
|
// Creds optionally provides credentials for communicating with service
|
||||||
|
// providers.
|
||||||
|
creds auth.CredentialsSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(services *disco.Disco, creds auth.CredentialsSource, client *http.Client) *Client {
|
||||||
|
if services == nil {
|
||||||
|
services = disco.NewDisco()
|
||||||
|
}
|
||||||
|
|
||||||
|
services.SetCredentialsSource(creds)
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
client = cleanhttp.DefaultPooledClient()
|
||||||
|
client.Timeout = requestTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
services.Transport = client.Transport.(*http.Transport)
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
client: client,
|
||||||
|
services: services,
|
||||||
|
creds: creds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover qeuries the host, and returns the url for the registry.
|
||||||
|
func (c *Client) Discover(host svchost.Hostname) *url.URL {
|
||||||
|
return c.services.DiscoverServiceURL(host, serviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Versions queries the registry for a module, and returns the available versions.
|
||||||
|
func (c *Client) Versions(module *regsrc.Module) (*response.ModuleVersions, error) {
|
||||||
|
host, err := module.SvcHost()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
service := c.Discover(host)
|
||||||
|
if service == nil {
|
||||||
|
return nil, fmt.Errorf("host %s does not provide Terraform modules", host)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(service.Path, "/") {
|
||||||
|
service.Path += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := url.Parse(path.Join(module.Module(), "versions"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
service = service.ResolveReference(p)
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] fetching module versions from %q", service)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", service.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.addRequestCreds(host, req)
|
||||||
|
req.Header.Set(xTerraformVersion, tfVersion)
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
// OK
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return nil, fmt.Errorf("module %q not found", module.String())
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("error looking up module versions: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var versions response.ModuleVersions
|
||||||
|
|
||||||
|
dec := json.NewDecoder(resp.Body)
|
||||||
|
if err := dec.Decode(&versions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mod := range versions.Modules {
|
||||||
|
for _, v := range mod.Versions {
|
||||||
|
log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &versions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) {
|
||||||
|
if c.creds == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := c.creds.ForHost(host)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARNING] Failed to get credentials for %s: %s (ignoring)", host, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if creds != nil {
|
||||||
|
creds.PrepareRequest(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location find the download location for a specific version module.
|
||||||
|
// This returns a string, because the final location may contain special go-getter syntax.
|
||||||
|
func (c *Client) Location(module *regsrc.Module, version string) (string, error) {
|
||||||
|
host, err := module.SvcHost()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
service := c.Discover(host)
|
||||||
|
if service == nil {
|
||||||
|
return "", fmt.Errorf("host %s does not provide Terraform modules", host.ForDisplay())
|
||||||
|
}
|
||||||
|
|
||||||
|
var p *url.URL
|
||||||
|
if version == "" {
|
||||||
|
p, err = url.Parse(path.Join(module.Module(), "download"))
|
||||||
|
} else {
|
||||||
|
p, err = url.Parse(path.Join(module.Module(), version, "download"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
download := service.ResolveReference(p)
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] looking up module location from %q", download)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", download.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.addRequestCreds(host, req)
|
||||||
|
req.Header.Set(xTerraformVersion, tfVersion)
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// there should be no body, but save it for logging
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error reading response body from registry: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK, http.StatusNoContent:
|
||||||
|
// OK
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return "", fmt.Errorf("module %q version %q not found", module, version)
|
||||||
|
default:
|
||||||
|
// anything else is an error:
|
||||||
|
return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the download location is in the X-Terraform-Get header
|
||||||
|
location := resp.Header.Get(xTerraformGet)
|
||||||
|
if location == "" {
|
||||||
|
return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If location looks like it's trying to be a relative URL, treat it as
|
||||||
|
// one.
|
||||||
|
//
|
||||||
|
// We don't do this for just _any_ location, since the X-Terraform-Get
|
||||||
|
// header is a go-getter location rather than a URL, and so not all
|
||||||
|
// possible values will parse reasonably as URLs.)
|
||||||
|
//
|
||||||
|
// When used in conjunction with go-getter we normally require this header
|
||||||
|
// to be an absolute URL, but we are more liberal here because third-party
|
||||||
|
// registry implementations may not "know" their own absolute URLs if
|
||||||
|
// e.g. they are running behind a reverse proxy frontend, or such.
|
||||||
|
if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") {
|
||||||
|
locationURL, err := url.Parse(location)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid relative URL for %q: %s", module, err)
|
||||||
|
}
|
||||||
|
locationURL = download.ResolveReference(locationURL)
|
||||||
|
location = locationURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return location, nil
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/terraform/registry/regsrc"
|
||||||
|
"github.com/hashicorp/terraform/registry/test"
|
||||||
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLookupModuleVersions(t *testing.T) {
|
||||||
|
server := test.Registry()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(test.Disco(server), nil, nil)
|
||||||
|
|
||||||
|
// test with and without a hostname
|
||||||
|
for _, src := range []string{
|
||||||
|
"example.com/test-versions/name/provider",
|
||||||
|
"test-versions/name/provider",
|
||||||
|
} {
|
||||||
|
modsrc, err := regsrc.ParseModuleSource(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Versions(modsrc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Modules) != 1 {
|
||||||
|
t.Fatal("expected 1 module, got", len(resp.Modules))
|
||||||
|
}
|
||||||
|
|
||||||
|
mod := resp.Modules[0]
|
||||||
|
name := "test-versions/name/provider"
|
||||||
|
if mod.Source != name {
|
||||||
|
t.Fatalf("expected module name %q, got %q", name, mod.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mod.Versions) != 4 {
|
||||||
|
t.Fatal("expected 4 versions, got", len(mod.Versions))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range mod.Versions {
|
||||||
|
_, err := version.NewVersion(v.Version)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("invalid version %q: %s", v.Version, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryAuth(t *testing.T) {
|
||||||
|
server := test.Registry()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(test.Disco(server), nil, nil)
|
||||||
|
|
||||||
|
src := "private/name/provider"
|
||||||
|
mod, err := regsrc.ParseModuleSource(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// both should fail without auth
|
||||||
|
_, err = client.Versions(mod)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
_, err = client.Location(mod, "1.0.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
client = NewClient(test.Disco(server), test.Credentials, nil)
|
||||||
|
|
||||||
|
_, err = client.Versions(mod)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = client.Location(mod, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLookupModuleLocationRelative(t *testing.T) {
|
||||||
|
server := test.Registry()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(test.Disco(server), nil, nil)
|
||||||
|
|
||||||
|
src := "relative/foo/bar"
|
||||||
|
mod, err := regsrc.ParseModuleSource(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := client.Location(mod, "0.2.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := server.URL + "/relative-path"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("wrong location %s; want %s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccLookupModuleVersions(t *testing.T) {
|
||||||
|
if os.Getenv("TF_ACC") == "" {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
regDisco := disco.NewDisco()
|
||||||
|
|
||||||
|
// test with and without a hostname
|
||||||
|
for _, src := range []string{
|
||||||
|
"terraform-aws-modules/vpc/aws",
|
||||||
|
regsrc.PublicRegistryHost.String() + "/terraform-aws-modules/vpc/aws",
|
||||||
|
} {
|
||||||
|
modsrc, err := regsrc.ParseModuleSource(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := NewClient(regDisco, nil, nil)
|
||||||
|
resp, err := s.Versions(modsrc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Modules) != 1 {
|
||||||
|
t.Fatal("expected 1 module, got", len(resp.Modules))
|
||||||
|
}
|
||||||
|
|
||||||
|
mod := resp.Modules[0]
|
||||||
|
name := "terraform-aws-modules/vpc/aws"
|
||||||
|
if mod.Source != name {
|
||||||
|
t.Fatalf("expected module name %q, got %q", name, mod.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mod.Versions) == 0 {
|
||||||
|
t.Fatal("expected multiple versions, got 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range mod.Versions {
|
||||||
|
_, err := version.NewVersion(v.Version)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("invalid version %q: %s", v.Version, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the error should reference the config source exatly, not the discovered path.
|
||||||
|
func TestLookupLookupModuleError(t *testing.T) {
|
||||||
|
server := test.Registry()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(test.Disco(server), nil, nil)
|
||||||
|
|
||||||
|
// this should not be found in teh registry
|
||||||
|
src := "bad/local/path"
|
||||||
|
mod, err := regsrc.ParseModuleSource(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Location(mod, "0.2.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for the exact quoted string to ensure we didn't prepend a hostname.
|
||||||
|
if !strings.Contains(err.Error(), `"bad/local/path"`) {
|
||||||
|
t.Fatal("error should not include the hostname. got:", err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue