Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module versions #16466

Merged
merged 13 commits into from
Oct 27, 2017
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",
notRegistry: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this style return an error? I can't think of what useful behavior this could have if we don't treat it as a registry URL, and so I think probably better to give the user the feedback that this isn't a valid source string. (Notwithstanding the special exceptions for github.com/etc handled elsewhere, of course.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, it actually does now, but notRegistry is checked first. I'll update the test.

},
{ // 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