Skip to content

Commit

Permalink
Merge pull request #16466 from hashicorp/jbardin/module-versions
Browse files Browse the repository at this point in the history
Module versions
  • Loading branch information
jbardin authored Oct 27, 2017
2 parents 5b8093a + 68bf48e commit d174b36
Show file tree
Hide file tree
Showing 18 changed files with 1,120 additions and 405 deletions.
8 changes: 8 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ type ProviderConfig struct {
// it can be copied into child module providers yet still interpolated in
// the correct scope.
Path []string

// Inherited is used to skip validation of this config, since any
// interpolated variables won't be declared at this level.
Inherited bool
}

// A resource represents a single Terraform resource in the configuration.
Expand Down Expand Up @@ -813,6 +817,10 @@ func (c *Config) rawConfigs() map[string]*RawConfig {
}

for _, pc := range c.ProviderConfigs {
// this was an inherited config, so we don't validate it at this level.
if pc.Inherited {
continue
}
source := fmt.Sprintf("provider config '%s'", pc.Name)
result[source] = pc.RawConfig
}
Expand Down
112 changes: 112 additions & 0 deletions config/module/detector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package module

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/registry/regsrc"
)

func TestParseRegistrySource(t *testing.T) {
for _, tc := range []struct {
source string
host string
id string
err bool
notRegistry bool
}{
{ // simple source id
source: "namespace/id/provider",
id: "namespace/id/provider",
},
{ // source with hostname
source: "registry.com/namespace/id/provider",
host: "registry.com",
id: "namespace/id/provider",
},
{ // source with hostname and port
source: "registry.com:4443/namespace/id/provider",
host: "registry.com:4443",
id: "namespace/id/provider",
},
{ // too many parts
source: "registry.com/namespace/id/provider/extra",
err: true,
},
{ // local path
source: "./local/file/path",
notRegistry: true,
},
{ // local path with hostname
source: "./registry.com/namespace/id/provider",
notRegistry: true,
},
{ // full URL
source: "https://example.com/foo/bar/baz",
notRegistry: true,
},
{ // punycode host not allowed in source
source: "xn--80akhbyknj4f.com/namespace/id/provider",
err: true,
},
{ // simple source id with subdir
source: "namespace/id/provider//subdir",
id: "namespace/id/provider",
},
{ // source with hostname and subdir
source: "registry.com/namespace/id/provider//subdir",
host: "registry.com",
id: "namespace/id/provider",
},
{ // source with hostname
source: "registry.com/namespace/id/provider",
host: "registry.com",
id: "namespace/id/provider",
},
{ // we special case github
source: "github.com/namespace/id/provider",
notRegistry: true,
},
{ // we special case github ssh
source: "git@github.com:namespace/id/provider",
notRegistry: true,
},
{ // we special case bitbucket
source: "bitbucket.org/namespace/id/provider",
notRegistry: true,
},
} {
t.Run(tc.source, func(t *testing.T) {
mod, err := regsrc.ParseModuleSource(tc.source)
if tc.notRegistry {
if err != regsrc.ErrInvalidModuleSource {
t.Fatalf("%q should not be a registry source, got err %v", tc.source, err)
}
return
}

if tc.err {
if err == nil {
t.Fatal("expected error")
}
return
}

if err != nil {
t.Fatal(err)
}

id := fmt.Sprintf("%s/%s/%s", mod.RawNamespace, mod.RawName, mod.RawProvider)

if tc.host != "" {
if mod.RawHost.Normalized() != tc.host {
t.Fatalf("expected host %q, got %q", tc.host, mod.RawHost)
}
}

if tc.id != id {
t.Fatalf("expected id %q, got %q", tc.id, id)
}
})
}
}
93 changes: 0 additions & 93 deletions config/module/get.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
package module

import (
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"

"github.com/hashicorp/go-getter"

cleanhttp "github.com/hashicorp/go-cleanhttp"
)

// GetMode is an enum that describes how modules are loaded.
Expand Down Expand Up @@ -63,90 +57,3 @@ func GetCopy(dst, src string) error {
// Copy to the final location
return copyDir(dst, tmpDir)
}

const (
registryAPI = "https://registry.terraform.io/v1/modules"
xTerraformGet = "X-Terraform-Get"
)

var detectors = []getter.Detector{
new(getter.GitHubDetector),
new(getter.BitBucketDetector),
new(getter.S3Detector),
new(registryDetector),
new(getter.FileDetector),
}

// these prefixes can't be registry IDs
// "http", "../", "./", "/", "getter::", etc
var skipRegistry = regexp.MustCompile(`^(http|[.]{1,2}/|/|[A-Za-z0-9]+::)`).MatchString

// registryDetector implements getter.Detector to detect Terraform Registry modules.
// If a path looks like a registry module identifier, attempt to locate it in
// the registry. If it's not found, pass it on in case it can be found by
// other means.
type registryDetector struct {
// override the default registry URL
api string

client *http.Client
}

func (d registryDetector) Detect(src, _ string) (string, bool, error) {
// the namespace can't start with "http", a relative or absolute path, or
// contain a go-getter "forced getter"
if skipRegistry(src) {
return "", false, nil
}

// there are 3 parts to a registry ID
if len(strings.Split(src, "/")) != 3 {
return "", false, nil
}

return d.lookupModule(src)
}

// Lookup the module in the registry.
func (d registryDetector) lookupModule(src string) (string, bool, error) {
if d.api == "" {
d.api = registryAPI
}

if d.client == nil {
d.client = cleanhttp.DefaultClient()
}

// src is already partially validated in Detect. We know it's a path, and
// if it can be parsed as a URL we will hand it off to the registry to
// determine if it's truly valid.
resp, err := d.client.Get(fmt.Sprintf("%s/%s/download", d.api, src))
if err != nil {
return "", false, fmt.Errorf("error looking up module %q: %s", src, 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 "", false, fmt.Errorf("error reading response body from registry: %s", err)
}

switch resp.StatusCode {
case http.StatusOK, http.StatusNoContent:
// OK
case http.StatusNotFound:
return "", false, fmt.Errorf("module %q not found in registry", src)
default:
// anything else is an error:
return "", false, fmt.Errorf("error getting download location for %q: %s resp:%s", src, resp.Status, body)
}

// the download location is in the X-Terraform-Get header
location := resp.Header.Get(xTerraformGet)
if location == "" {
return "", false, fmt.Errorf("failed to get download URL for %q: %s resp:%s", src, resp.Status, body)
}

return location, true, nil
}
Loading

0 comments on commit d174b36

Please sign in to comment.