Merge pull request #16480 from hashicorp/0.11-dev

0.11 dev
This commit is contained in:
James Bardin 2017-10-27 16:02:19 -04:00 committed by GitHub
commit 633d428c15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 2162 additions and 995 deletions

View File

@ -1,9 +1,31 @@
## 0.11.0-beta1 (Unreleased)
BACKWARDS INCOMPATIBILITIES / NOTES:
* Output interpolation errors are now fatal. Module configs with unused outputs
which contained errors will no longer be valid.
* Module configuration blocks have 2 new reserved attribute names, "providers"
and "version". Modules using these as input variables will need to be
updated.
* The module provider inheritance system has been updated. Providers declared
with configuration will no longer be merged, and named provider
configurations can be explicitly passed between modules. See documentation
for details. [TODO]
NEW FEATURES:
* modules: Module configuration blocks now have a "version" attribute, to set a version constraint for modules sourced from a registry. [GH-16466]
* modules: Module configuration blocks now have a "providers" attribute, to map a provider configuration from the current module into a submodule [GH-16379]
IMPROVEMENTS:
* cli: The `terraform versions` command now prints out the version numbers of initialized plugins as well as the version of Terraform core, so that they can be more easily shared when opening GitHub Issues, etc. [GH-16439]
BUG FIXES:
* config: Provider config in submodules will no longer be be overridden by parent providers with the same name. [GH-16379]
* core: Module outputs can now produce errors, preventing them from silently propagating through the config. [GH-16204]
PROVIDER FRAMEWORK CHANGES (not user-facing):
* helper/schema: Loosen validation for 'id' field [GH-16456]

View File

@ -55,6 +55,8 @@ type AtlasConfig struct {
type Module struct {
Name string
Source string
Version string
Providers map[string]string
RawConfig *RawConfig
}
@ -67,6 +69,15 @@ type ProviderConfig struct {
Alias string
Version string
RawConfig *RawConfig
// Path records where the Provider was declared in a module tree, so that
// 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.
@ -806,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
}

View File

@ -836,3 +836,21 @@ func TestResourceProviderFullName(t *testing.T) {
}
}
}
func TestConfigModuleProviders(t *testing.T) {
c := testConfig(t, "module-providers")
if len(c.Modules) != 1 {
t.Fatalf("expected 1 module, got %d", len(c.Modules))
}
expected := map[string]string{
"aws": "aws.foo",
}
got := c.Modules[0].Providers
if !reflect.DeepEqual(expected, got) {
t.Fatalf("exptected providers %#v, got providers %#v", expected, got)
}
}

View File

@ -393,9 +393,6 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) {
err)
}
// Remove the fields we handle specially
delete(config, "source")
rawConfig, err := NewRawConfig(config)
if err != nil {
return nil, fmt.Errorf(
@ -404,7 +401,11 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) {
err)
}
// If we have a count, then figure it out
// Remove the fields we handle specially
delete(config, "source")
delete(config, "version")
delete(config, "providers")
var source string
if o := listVal.Filter("source"); len(o.Items) > 0 {
err = hcl.DecodeObject(&source, o.Items[0].Val)
@ -416,9 +417,33 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) {
}
}
var version string
if o := listVal.Filter("version"); len(o.Items) > 0 {
err = hcl.DecodeObject(&version, o.Items[0].Val)
if err != nil {
return nil, fmt.Errorf(
"Error parsing version for %s: %s",
k,
err)
}
}
var providers map[string]string
if o := listVal.Filter("providers"); len(o.Items) > 0 {
err = hcl.DecodeObject(&providers, o.Items[0].Val)
if err != nil {
return nil, fmt.Errorf(
"Error parsing providers for %s: %s",
k,
err)
}
}
result = append(result, &Module{
Name: k,
Source: source,
Version: version,
Providers: providers,
RawConfig: rawConfig,
})
}

View File

@ -75,17 +75,19 @@ func (t *hcl2Configurable) Config() (*Config, error) {
Include *[]string `hcl:"include"`
Exclude *[]string `hcl:"exclude"`
}
type module struct {
Name string `hcl:"name,label"`
Source string `hcl:"source,attr"`
Config hcl2.Body `hcl:",remain"`
}
type provider struct {
Name string `hcl:"name,label"`
Alias *string `hcl:"alias,attr"`
Version *string `hcl:"version,attr"`
Config hcl2.Body `hcl:",remain"`
}
type module struct {
Name string `hcl:"name,label"`
Source string `hcl:"source,attr"`
Version *string `hcl:"version,attr"`
Providers *map[string]string `hcl:"providers,attr"`
Config hcl2.Body `hcl:",remain"`
}
type resourceLifecycle struct {
CreateBeforeDestroy *bool `hcl:"create_before_destroy,attr"`
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
@ -248,6 +250,15 @@ func (t *hcl2Configurable) Config() (*Config, error) {
Source: rawM.Source,
RawConfig: NewRawConfigHCL2(rawM.Config),
}
if rawM.Version != nil {
m.Version = *rawM.Version
}
if rawM.Providers != nil {
m.Providers = *rawM.Providers
}
config.Modules = append(config.Modules, m)
}

View File

@ -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)
}
})
}
}

View File

@ -1,18 +1,10 @@
package module
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"github.com/hashicorp/go-getter"
cleanhttp "github.com/hashicorp/go-cleanhttp"
)
// GetMode is an enum that describes how modules are loaded.
@ -65,143 +57,3 @@ func GetCopy(dst, src string) error {
// Copy to the final location
return copyDir(dst, tmpDir)
}
func getStorage(s getter.Storage, key string, src string, mode GetMode) (string, bool, error) {
// Get the module with the level specified if we were told to.
if mode > GetModeNone {
if err := s.Get(key, src, mode == GetModeUpdate); err != nil {
return "", false, err
}
}
// Get the directory where the module is.
return s.Dir(key)
}
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(localDetector),
new(registryDetector),
}
// these prefixes can't be registry IDs
// "http", "./", "/", "getter::"
var skipRegistry = regexp.MustCompile(`^(http|\./|/|[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 {
log.Printf("[WARN] error looking up module %q: %s", src, err)
return "", false, nil
}
defer resp.Body.Close()
// there should be no body, but save it for logging
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("[WARN] error reading response body from registry: %s", err)
return "", false, nil
}
switch resp.StatusCode {
case http.StatusOK, http.StatusNoContent:
// OK
case http.StatusNotFound:
log.Printf("[INFO] module %q not found in registry", src)
return "", false, nil
default:
// anything else is an error:
log.Printf("[WARN] error getting download location for %q: %s resp:%s", src, resp.Status, body)
return "", false, nil
}
// 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
}
// localDetector wraps the default getter.FileDetector and checks if the module
// exists in the local filesystem. The default FileDetector only converts paths
// into file URLs, and returns found. We want to first check for a local module
// before passing it off to the registryDetector so we don't inadvertently
// replace a local module with a registry module of the same name.
type localDetector struct{}
func (d localDetector) Detect(src, wd string) (string, bool, error) {
localSrc, ok, err := new(getter.FileDetector).Detect(src, wd)
if err != nil {
return src, ok, err
}
if !ok {
return "", false, nil
}
u, err := url.Parse(localSrc)
if err != nil {
return "", false, err
}
_, err = os.Stat(u.Path)
// just continue detection if it doesn't exist
if os.IsNotExist(err) {
return "", false, nil
}
// return any other errors
if err != nil {
return "", false, err
}
return localSrc, true, nil
}

View File

@ -1,19 +1,22 @@
package module
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
getter "github.com/hashicorp/go-getter"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/registry/regsrc"
"github.com/hashicorp/terraform/registry/response"
"github.com/hashicorp/terraform/svchost/disco"
)
// Map of module names and location of test modules.
@ -26,18 +29,28 @@ type testMod struct {
// All the locationes from the mockRegistry start with a file:// scheme. If
// the the location string here doesn't have a scheme, the mockRegistry will
// find the absolute path and return a complete URL.
var testMods = map[string]testMod{
"registry/foo/bar": {
var testMods = map[string][]testMod{
"registry/foo/bar": {{
location: "file:///download/registry/foo/bar/0.2.3//*?archive=tar.gz",
version: "0.2.3",
},
"registry/foo/baz": {
}},
"registry/foo/baz": {{
location: "file:///download/registry/foo/baz/1.10.0//*?archive=tar.gz",
version: "1.10.0",
},
"registry/local/sub": {
}},
"registry/local/sub": {{
location: "test-fixtures/registry-tar-subdir/foo.tgz//*?archive=tar.gz",
version: "0.1.2",
}},
"exists-in-registry/identifier/provider": {{
location: "file:///registry/exists",
version: "0.2.0",
}},
"test-versions/name/provider": {
{version: "2.2.0"},
{version: "2.1.1"},
{version: "1.2.2"},
{version: "1.2.1"},
},
}
@ -55,248 +68,131 @@ func latestVersion(versions []string) string {
return col[len(col)-1].String()
}
// Just enough like a registry to exercise our code.
// Returns the location of the latest version
func mockRegistry() *httptest.Server {
func mockRegHandler() http.Handler {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
download := func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimLeft(r.URL.Path, "/")
// handle download request
re := regexp.MustCompile(`^([-a-z]+/\w+/\w+).*/download$`)
// download lookup
matches := re.FindStringSubmatch(p)
if len(matches) != 2 {
w.WriteHeader(http.StatusBadRequest)
return
}
versions, ok := testMods[matches[1]]
if !ok {
http.NotFound(w, r)
return
}
mod := versions[0]
location := mod.location
if !strings.HasPrefix(location, "file:///") {
// we can't use filepath.Abs because it will clean `//`
wd, _ := os.Getwd()
location = fmt.Sprintf("file://%s/%s", wd, location)
}
w.Header().Set("X-Terraform-Get", location)
w.WriteHeader(http.StatusNoContent)
// no body
return
}
versions := func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimLeft(r.URL.Path, "/")
re := regexp.MustCompile(`^([-a-z]+/\w+/\w+)/versions$`)
matches := re.FindStringSubmatch(p)
if len(matches) != 2 {
w.WriteHeader(http.StatusBadRequest)
return
}
name := matches[1]
versions, ok := testMods[name]
if !ok {
http.NotFound(w, r)
return
}
// only adding the single requested module for now
// this is the minimal that any regisry is epected to support
mpvs := &response.ModuleProviderVersions{
Source: name,
}
for _, v := range versions {
mv := &response.ModuleVersion{
Version: v.version,
}
mpvs.Versions = append(mpvs.Versions, mv)
}
resp := response.ModuleVersions{
Modules: []*response.ModuleProviderVersions{mpvs},
}
js, err := json.Marshal(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
mux.Handle("/v1/modules/",
http.StripPrefix("/v1/modules/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimLeft(r.URL.Path, "/")
// handle download request
download := regexp.MustCompile(`^(\w+/\w+/\w+)/download$`)
// download lookup
matches := download.FindStringSubmatch(p)
if len(matches) != 2 {
w.WriteHeader(http.StatusBadRequest)
if strings.HasSuffix(r.URL.Path, "/download") {
download(w, r)
return
}
mod, ok := testMods[matches[1]]
if !ok {
w.WriteHeader(http.StatusNotFound)
if strings.HasSuffix(r.URL.Path, "/versions") {
versions(w, r)
return
}
location := mod.location
if !strings.HasPrefix(location, "file:///") {
// we can't use filepath.Abs because it will clean `//`
wd, _ := os.Getwd()
location = fmt.Sprintf("file://%s/%s", wd, location)
}
w.Header().Set(xTerraformGet, location)
w.WriteHeader(http.StatusNoContent)
// no body
return
http.NotFound(w, r)
})),
)
mux.HandleFunc("/.well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"modules.v1":"/v1/modules/"}`)
})
return mux
}
// Just enough like a registry to exercise our code.
// Returns the location of the latest version
func mockRegistry() *httptest.Server {
server := httptest.NewServer(mockRegHandler())
return server
}
func TestDetectRegistry(t *testing.T) {
server := mockRegistry()
defer server.Close()
detector := registryDetector{
api: server.URL + "/v1/modules",
client: server.Client(),
}
for _, tc := range []struct {
source string
location string
found bool
err bool
}{
{
source: "registry/foo/bar",
location: testMods["registry/foo/bar"].location,
found: true,
},
{
source: "registry/foo/baz",
location: testMods["registry/foo/baz"].location,
found: true,
},
// this should not be found, but not stop detection
{
source: "registry/foo/notfound",
found: false,
},
// a full url should not be detected
{
source: "http://example.com/registry/foo/notfound",
found: false,
},
// paths should not be detected
{
source: "./local/foo/notfound",
found: false,
},
{
source: "/local/foo/notfound",
found: false,
},
// wrong number of parts can't be regisry IDs
{
source: "something/registry/foo/notfound",
found: false,
},
} {
t.Run(tc.source, func(t *testing.T) {
loc, ok, err := detector.Detect(tc.source, "")
if (err == nil) == tc.err {
t.Fatalf("expected error? %t; got error :%v", tc.err, err)
}
if ok != tc.found {
t.Fatalf("expected OK == %t", tc.found)
}
loc = strings.TrimPrefix(loc, server.URL+"/")
if strings.TrimPrefix(loc, server.URL) != tc.location {
t.Fatalf("expected location: %q, got %q", tc.location, loc)
}
})
}
}
// check that the full set of detectors works as expected
func TestDetectors(t *testing.T) {
server := mockRegistry()
defer server.Close()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
regDetector := &registryDetector{
api: server.URL + "/v1/modules",
client: server.Client(),
}
detectors := []getter.Detector{
new(getter.GitHubDetector),
new(getter.BitBucketDetector),
new(getter.S3Detector),
new(localDetector),
regDetector,
}
for _, tc := range []struct {
source string
location string
fixture string
err bool
}{
{
source: "registry/foo/bar",
location: "file:///download/registry/foo/bar/0.2.3//*?archive=tar.gz",
},
// this should not be found, but not stop detection
{
source: "registry/foo/notfound",
err: true,
},
// a full url should be unchanged
{
source: "http://example.com/registry/foo/notfound?" +
"checksum=sha256:f19056b80a426d797ff9e470da069c171a6c6befa83e2da7f6c706207742acab",
location: "http://example.com/registry/foo/notfound?" +
"checksum=sha256:f19056b80a426d797ff9e470da069c171a6c6befa83e2da7f6c706207742acab",
},
// forced getters will return untouched
{
source: "git::http://example.com/registry/foo/notfound?param=value",
location: "git::http://example.com/registry/foo/notfound?param=value",
},
// local paths should be detected as such, even if they're match
// registry modules.
{
source: "./registry/foo/bar",
err: true,
},
{
source: "/registry/foo/bar",
err: true,
},
// wrong number of parts can't be regisry IDs
{
source: "something/registry/foo/notfound",
err: true,
},
// make sure a local module that looks like a registry id takes precedence
{
source: "namespace/identifier/provider",
fixture: "discover-subdirs",
// this should be found locally
location: "file://" + filepath.Join(wd, fixtureDir, "discover-subdirs/namespace/identifier/provider"),
},
} {
t.Run(tc.source, func(t *testing.T) {
dir := wd
if tc.fixture != "" {
dir = filepath.Join(wd, fixtureDir, tc.fixture)
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
defer os.Chdir(wd)
}
loc, err := getter.Detect(tc.source, dir, detectors)
if (err == nil) == tc.err {
t.Fatalf("expected error? %t; got error :%v", tc.err, err)
}
loc = strings.TrimPrefix(loc, server.URL+"/")
if strings.TrimPrefix(loc, server.URL) != tc.location {
t.Fatalf("expected location: %q, got %q", tc.location, loc)
}
})
}
func mockTLSRegistry() *httptest.Server {
server := httptest.NewTLSServer(mockRegHandler())
return server
}
// GitHub archives always contain the module source in a single subdirectory,
// so the registry will return a path with with a `//*` suffix. We need to make
// sure this doesn't intefere with our internal handling of `//` subdir.
func TestRegistryGitHubArchive(t *testing.T) {
server := mockRegistry()
server := mockTLSRegistry()
defer server.Close()
d := regDisco
regDetector := &registryDetector{
api: server.URL + "/v1/modules",
client: server.Client(),
}
origDetectors := detectors
regDisco = disco.NewDisco()
regDisco.Transport = mockTransport(server)
defer func() {
detectors = origDetectors
regDisco = d
}()
detectors = []getter.Detector{
new(getter.GitHubDetector),
new(getter.BitBucketDetector),
new(getter.S3Detector),
new(localDetector),
regDetector,
}
storage := testStorage(t)
tree := NewTree("", testConfig(t, "registry-tar-subdir"))
@ -331,13 +227,52 @@ func TestRegistryGitHubArchive(t *testing.T) {
}
}
// Test that the //subdir notation can be used with registry modules
func TestRegisryModuleSubdir(t *testing.T) {
server := mockTLSRegistry()
defer server.Close()
d := regDisco
regDisco = disco.NewDisco()
regDisco.Transport = mockTransport(server)
defer func() {
regDisco = d
}()
storage := testStorage(t)
tree := NewTree("", testConfig(t, "registry-subdir"))
if err := tree.Load(storage, GetModeGet); err != nil {
t.Fatalf("err: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
if err := tree.Load(storage, GetModeNone); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(tree.String())
expected := strings.TrimSpace(treeLoadRegistrySubdirStr)
if actual != expected {
t.Fatalf("got: \n\n%s\nexpected: \n\n%s", actual, expected)
}
}
func TestAccRegistryDiscover(t *testing.T) {
if os.Getenv("TF_ACC") == "" {
t.Skip("skipping ACC test")
}
// simply check that we get a valid github URL for this from the registry
loc, err := getter.Detect("hashicorp/consul/aws", "./", detectors)
module, err := regsrc.ParseModuleSource("hashicorp/consul/aws")
if err != nil {
t.Fatal(err)
}
loc, err := lookupModuleLocation(nil, module, "")
if err != nil {
t.Fatal(err)
}

View File

@ -2,6 +2,8 @@ package module
// Module represents the metadata for a single module.
type Module struct {
Name string
Source string
Name string
Source string
Version string
Providers map[string]string
}

196
config/module/registry.go Normal file
View File

@ -0,0 +1,196 @@
package module
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/disco"
"github.com/hashicorp/terraform/version"
)
const (
defaultRegistry = "registry.terraform.io"
defaultApiPath = "/v1/modules"
registryServiceID = "registry.v1"
xTerraformGet = "X-Terraform-Get"
xTerraformVersion = "X-Terraform-Version"
requestTimeout = 10 * time.Second
serviceID = "modules.v1"
)
var (
httpClient *http.Client
tfVersion = version.String()
regDisco = disco.NewDisco()
)
func init() {
httpClient = cleanhttp.DefaultPooledClient()
httpClient.Timeout = requestTimeout
}
type errModuleNotFound string
func (e errModuleNotFound) Error() string {
return `module "` + string(e) + `" not found`
}
func discoverRegURL(d *disco.Disco, module *regsrc.Module) *url.URL {
if d == nil {
d = regDisco
}
if module.RawHost == nil {
module.RawHost = regsrc.NewFriendlyHost(defaultRegistry)
}
regURL := d.DiscoverServiceURL(svchost.Hostname(module.RawHost.Normalized()), serviceID)
if regURL == nil {
regURL = &url.URL{
Scheme: "https",
Host: module.RawHost.String(),
Path: defaultApiPath,
}
}
if !strings.HasSuffix(regURL.Path, "/") {
regURL.Path += "/"
}
return regURL
}
// Lookup module versions in the registry.
func lookupModuleVersions(d *disco.Disco, module *regsrc.Module) (*response.ModuleVersions, error) {
service := discoverRegURL(d, module)
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
}
req.Header.Set(xTerraformVersion, tfVersion)
if d == nil {
d = regDisco
}
// if discovery required a custom transport, then we should use that too
client := httpClient
if d.Transport != nil {
client = &http.Client{
Transport: d.Transport,
Timeout: requestTimeout,
}
}
resp, err := 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, errModuleNotFound(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
}
return &versions, nil
}
// lookup the location of a specific module version in the registry
func lookupModuleLocation(d *disco.Disco, module *regsrc.Module, version string) (string, error) {
service := discoverRegURL(d, module)
var p *url.URL
var err error
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
}
req.Header.Set(xTerraformVersion, tfVersion)
// if discovery required a custom transport, then we should use that too
client := httpClient
if regDisco.Transport != nil {
client = &http.Client{
Transport: regDisco.Transport,
Timeout: requestTimeout,
}
}
resp, err := 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)
}
return location, nil
}

View File

@ -0,0 +1,154 @@
package module
import (
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
cleanhttp "github.com/hashicorp/go-cleanhttp"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/registry/regsrc"
"github.com/hashicorp/terraform/svchost/disco"
)
// Return a transport to use for this test server.
// This not only loads the tls.Config from the test server for proper cert
// validation, but also inserts a Dialer that resolves localhost and
// example.com to 127.0.0.1 with the correct port, since 127.0.0.1 on its own
// isn't a valid registry hostname.
// TODO: cert validation not working here, so we use don't verify for now.
func mockTransport(server *httptest.Server) *http.Transport {
u, _ := url.Parse(server.URL)
_, port, _ := net.SplitHostPort(u.Host)
transport := cleanhttp.DefaultTransport()
transport.TLSClientConfig = server.TLS
transport.TLSClientConfig.InsecureSkipVerify = true
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
switch host {
case "example.com", "localhost", "localhost.localdomain", "registry.terraform.io":
addr = "127.0.0.1"
if port != "" {
addr += ":" + port
}
}
return (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext(ctx, network, addr)
}
return transport
}
func TestMockDiscovery(t *testing.T) {
server := mockTLSRegistry()
defer server.Close()
regDisco := disco.NewDisco()
regDisco.Transport = mockTransport(server)
regURL := regDisco.DiscoverServiceURL("example.com", serviceID)
if regURL == nil {
t.Fatal("no registry service discovered")
}
if regURL.Host != "example.com" {
t.Fatal("expected registry host example.com, got:", regURL.Host)
}
}
func TestLookupModuleVersions(t *testing.T) {
server := mockTLSRegistry()
defer server.Close()
regDisco := disco.NewDisco()
regDisco.Transport = mockTransport(server)
// 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 := lookupModuleVersions(regDisco, 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 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",
defaultRegistry + "/terraform-aws-modules/vpc/aws",
} {
modsrc, err := regsrc.ParseModuleSource(src)
if err != nil {
t.Fatal(err)
}
resp, err := lookupModuleVersions(regDisco, 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)
}
}
}
}

302
config/module/storage.go Normal file
View File

@ -0,0 +1,302 @@
package module
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
getter "github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/registry/regsrc"
)
const manifestName = "modules.json"
// moduleManifest is the serialization structure used to record the stored
// module's metadata.
type moduleManifest struct {
Modules []moduleRecord
}
// moduleRecords represents the stored module's metadata.
// This is compared for equality using '==', so all fields needs to remain
// comparable.
type moduleRecord struct {
// Source is the module source string from the config, minus any
// subdirectory.
Source string
// Key is the locally unique identifier for this module.
Key string
// Version is the exact version string for the stored module.
Version string
// Dir is the directory name returned by the FileStorage. This is what
// allows us to correlate a particular module version with the location on
// disk.
Dir string
// Root is the root directory containing the module. If the module is
// unpacked from an archive, and not located in the root directory, this is
// used to direct the loader to the correct subdirectory. This is
// independent from any subdirectory in the original source string, which
// may traverse further into the module tree.
Root string
// url is the location of the module source
url string
// Registry is true if this module is sourced from a registry
registry bool
}
// moduleStorage implements methods to record and fetch metadata about the
// modules that have been fetched and stored locally. The getter.Storgae
// abstraction doesn't provide the information needed to know which versions of
// a module have been stored, or their location.
type moduleStorage struct {
getter.Storage
storageDir string
mode GetMode
}
func newModuleStorage(s getter.Storage, mode GetMode) moduleStorage {
return moduleStorage{
Storage: s,
storageDir: storageDir(s),
mode: mode,
}
}
// The Tree needs to know where to store the module manifest.
// Th Storage abstraction doesn't provide access to the storage root directory,
// so we extract it here.
func storageDir(s getter.Storage) string {
// get the StorageDir directly if possible
switch t := s.(type) {
case *getter.FolderStorage:
return t.StorageDir
case moduleStorage:
return t.storageDir
case nil:
return ""
}
// this should be our UI wrapper which is exported here, so we need to
// extract the FolderStorage via reflection.
fs := reflect.ValueOf(s).Elem().FieldByName("Storage").Interface()
return storageDir(fs.(getter.Storage))
}
// loadManifest returns the moduleManifest file from the parent directory.
func (m moduleStorage) loadManifest() (moduleManifest, error) {
manifest := moduleManifest{}
manifestPath := filepath.Join(m.storageDir, manifestName)
data, err := ioutil.ReadFile(manifestPath)
if err != nil && !os.IsNotExist(err) {
return manifest, err
}
if len(data) == 0 {
return manifest, nil
}
if err := json.Unmarshal(data, &manifest); err != nil {
return manifest, err
}
return manifest, nil
}
// Store the location of the module, along with the version used and the module
// root directory. The storage method loads the entire file and rewrites it
// each time. This is only done a few times during init, so efficiency is
// not a concern.
func (m moduleStorage) recordModule(rec moduleRecord) error {
manifest, err := m.loadManifest()
if err != nil {
// if there was a problem with the file, we will attempt to write a new
// one. Any non-data related error should surface there.
log.Printf("[WARN] error reading module manifest: %s", err)
}
// do nothing if we already have the exact module
for i, stored := range manifest.Modules {
if rec == stored {
return nil
}
// they are not equal, but if the storage path is the same we need to
// remove this rec to be replaced.
if rec.Dir == stored.Dir {
manifest.Modules[i] = manifest.Modules[len(manifest.Modules)-1]
manifest.Modules = manifest.Modules[:len(manifest.Modules)-1]
break
}
}
manifest.Modules = append(manifest.Modules, rec)
js, err := json.Marshal(manifest)
if err != nil {
panic(err)
}
manifestPath := filepath.Join(m.storageDir, manifestName)
return ioutil.WriteFile(manifestPath, js, 0644)
}
// load the manifest from dir, and return all module versions matching the
// provided source. Records with no version info will be skipped, as they need
// to be uniquely identified by other means.
func (m moduleStorage) moduleVersions(source string) ([]moduleRecord, error) {
manifest, err := m.loadManifest()
if err != nil {
return manifest.Modules, err
}
var matching []moduleRecord
for _, m := range manifest.Modules {
if m.Source == source && m.Version != "" {
matching = append(matching, m)
}
}
return matching, nil
}
func (m moduleStorage) moduleDir(key string) (string, error) {
manifest, err := m.loadManifest()
if err != nil {
return "", err
}
for _, m := range manifest.Modules {
if m.Key == key {
return m.Dir, nil
}
}
return "", nil
}
// return only the root directory of the module stored in dir.
func (m moduleStorage) getModuleRoot(dir string) (string, error) {
manifest, err := m.loadManifest()
if err != nil {
return "", err
}
for _, mod := range manifest.Modules {
if mod.Dir == dir {
return mod.Root, nil
}
}
return "", nil
}
// record only the Root directory for the module stored at dir.
// TODO: remove this compatibility function to store the full moduleRecord.
func (m moduleStorage) recordModuleRoot(dir, root string) error {
rec := moduleRecord{
Dir: dir,
Root: root,
}
return m.recordModule(rec)
}
func (m moduleStorage) getStorage(key string, src string) (string, bool, error) {
// Get the module with the level specified if we were told to.
if m.mode > GetModeNone {
log.Printf("[DEBUG] fetching %q with key %q", src, key)
if err := m.Storage.Get(key, src, m.mode == GetModeUpdate); err != nil {
return "", false, err
}
}
// Get the directory where the module is.
dir, found, err := m.Storage.Dir(key)
log.Printf("[DEBUG] found %q in %q: %t", src, dir, found)
return dir, found, err
}
// find a stored module that's not from a registry
func (m moduleStorage) findModule(key string) (string, error) {
if m.mode == GetModeUpdate {
return "", nil
}
return m.moduleDir(key)
}
// find a registry module
func (m moduleStorage) findRegistryModule(mSource, constraint string) (moduleRecord, error) {
rec := moduleRecord{
Source: mSource,
}
// detect if we have a registry source
mod, err := regsrc.ParseModuleSource(mSource)
switch err {
case nil:
//ok
case regsrc.ErrInvalidModuleSource:
return rec, nil
default:
return rec, err
}
rec.registry = true
log.Printf("[TRACE] %q is a registry module", mod.Module())
versions, err := m.moduleVersions(mod.String())
if err != nil {
log.Printf("[ERROR] error looking up versions for %q: %s", mod.Module(), err)
return rec, err
}
match, err := newestRecord(versions, constraint)
if err != nil {
// TODO: does this allow previously unversioned modules?
log.Printf("[INFO] no matching version for %q<%s>, %s", mod.Module(), constraint, err)
}
rec.Dir = match.Dir
rec.Version = match.Version
found := rec.Dir != ""
// we need to lookup available versions
// Only on Get if it's not found, on unconditionally on Update
if (m.mode == GetModeGet && !found) || (m.mode == GetModeUpdate) {
resp, err := lookupModuleVersions(nil, mod)
if err != nil {
return rec, err
}
if len(resp.Modules) == 0 {
return rec, fmt.Errorf("module %q not found in registry", mod.Module())
}
match, err := newestVersion(resp.Modules[0].Versions, constraint)
if err != nil {
return rec, err
}
if match == nil {
return rec, fmt.Errorf("no versions for %q found matching %q", mod.Module(), constraint)
}
rec.Version = match.Version
rec.url, err = lookupModuleLocation(nil, mod, rec.Version)
if err != nil {
return rec, err
}
}
return rec, nil
}

View File

@ -0,0 +1,13 @@
provider "top" {}
provider "bottom" {
alias = "foo"
value = "from bottom"
}
module "c" {
source = "../c"
providers = {
"bottom" = "bottom.foo"
}
}

View File

@ -0,0 +1,2 @@
# Hello
provider "bottom" {}

View File

@ -0,0 +1,11 @@
provider "top" {
alias = "foo"
value = "from top"
}
module "a" {
source = "./a"
providers = {
"top" = "top.foo"
}
}

View File

@ -0,0 +1 @@
resource "test_resource" "a-b" {}

View File

@ -0,0 +1,3 @@
module "b" {
source = "./b"
}

View File

@ -0,0 +1 @@
resource "test_resource" "c-b" {}

View File

@ -0,0 +1,3 @@
module "b" {
source = "./b"
}

View File

@ -0,0 +1,3 @@
module "a" {
source = "./a"
}

View File

@ -0,0 +1,3 @@
module "a" {
source = "./c"
}

View File

@ -0,0 +1 @@
resource "test_instance" "a-c" {}

View File

@ -0,0 +1,3 @@
module "c" {
source = "./c"
}

View File

@ -0,0 +1 @@
resource "test_instance" "b-c" {}

View File

@ -0,0 +1,3 @@
module "c" {
source = "./c"
}

View File

@ -0,0 +1,7 @@
module "a" {
source = "./a"
}
module "b" {
source = "./b"
}

View File

@ -0,0 +1,3 @@
output "local" {
value = "test"
}

View File

@ -0,0 +1,3 @@
module "provider" {
source = "exists-in-registry/identifier/provider"
}

View File

@ -0,0 +1,2 @@
resource "bar_resource" "in_grandchild" {}

View File

@ -0,0 +1,9 @@
resource "foo_resource" "in_child" {}
provider "bar" {
value = "from child"
}
module "grandchild" {
source = "./grandchild"
}

View File

@ -0,0 +1,7 @@
provider "foo" {
value = "from root"
}
module "child" {
source = "./child"
}

View File

@ -0,0 +1 @@
resource "foo_instance" "bar" {}

View File

@ -0,0 +1,7 @@
provider "foo" {
value = "from root"
}
module "child" {
source = "./child"
}

View File

@ -0,0 +1,4 @@
module "foo" {
// the mock test registry will redirect this to the local tar file
source = "registry/local/sub//baz"
}

View File

@ -3,11 +3,8 @@ package module
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"sync"
@ -30,6 +27,17 @@ type Tree struct {
children map[string]*Tree
path []string
lock sync.RWMutex
// version is the final version of the config loaded for the Tree's module
version string
// source is the "source" string used to load this module. It's possible
// for a module source to change, but the path remains the same, preventing
// it from being reloaded.
source string
// parent allows us to walk back up the tree and determine if there are any
// versioned ancestor modules which may effect the stored location of
// submodules
parent *Tree
}
// NewTree returns a new Tree for the given config structure.
@ -130,8 +138,10 @@ func (t *Tree) Modules() []*Module {
result := make([]*Module, len(t.config.Modules))
for i, m := range t.config.Modules {
result[i] = &Module{
Name: m.Name,
Source: m.Source,
Name: m.Name,
Version: m.Version,
Source: m.Source,
Providers: m.Providers,
}
}
@ -159,20 +169,42 @@ func (t *Tree) Name() string {
// module trees inherently require the configuration to be in a reasonably
// sane state: no circular dependencies, proper module sources, etc. A full
// suite of validations can be done by running Validate (after loading).
func (t *Tree) Load(s getter.Storage, mode GetMode) error {
func (t *Tree) Load(storage getter.Storage, mode GetMode) error {
t.lock.Lock()
defer t.lock.Unlock()
// Reset the children if we have any
t.children = nil
s := newModuleStorage(storage, mode)
modules := t.Modules()
children, err := t.getChildren(s)
if err != nil {
return err
}
// Go through all the children and load them.
for _, c := range children {
if err := c.Load(storage, mode); err != nil {
return err
}
}
// Set our tree up
t.children = children
// if we're the root module, we can now set the provider inheritance
if len(t.path) == 0 {
t.inheritProviderConfigs(nil)
}
return nil
}
func (t *Tree) getChildren(s moduleStorage) (map[string]*Tree, error) {
children := make(map[string]*Tree)
// Go through all the modules and get the directory for them.
for _, m := range modules {
for _, m := range t.Modules() {
if _, ok := children[m.Name]; ok {
return fmt.Errorf(
return nil, fmt.Errorf(
"module %s: duplicated. module names must be unique", m.Name)
}
@ -181,54 +213,68 @@ func (t *Tree) Load(s getter.Storage, mode GetMode) error {
copy(path, t.path)
path = append(path, m.Name)
// The key is the string that will be hashed to uniquely id the Source.
// The leading digit can be incremented to force re-fetch all existing
// modules.
key := fmt.Sprintf("0.root.%s-%s", strings.Join(path, "."), m.Source)
log.Printf("[TRACE] module source: %q", m.Source)
log.Printf("[TRACE] module source %q", m.Source)
// Split out the subdir if we have one.
// Terraform keeps the entire requested tree for now, so that modules can
// reference sibling modules from the same archive or repo.
source, subDir := getter.SourceDirSubdir(m.Source)
// First check if we we need to download anything.
// This is also checked by the getter.Storage implementation, but we
// want to be able to short-circuit the detection as well, since some
// detectors may need to make external calls.
dir, found, err := s.Dir(key)
// Lookup the local location of the module.
// dir is the local directory where the module is stored
mod, err := s.findRegistryModule(m.Source, m.Version)
if err != nil {
return err
return nil, err
}
// looks like we already have it
// In order to load the Tree we need to find out if there was another
// subDir stored from discovery.
if found && mode != GetModeUpdate {
subDir, err := t.getSubdir(dir)
// The key is the string that will be used to uniquely id the Source in
// the local storage. The prefix digit can be incremented to
// invalidate the local module storage.
key := "1." + t.versionedPathKey(m)
if mod.Version != "" {
key += "." + mod.Version
}
// Check for the exact key if it's not a registry module
if !mod.registry {
mod.Dir, err = s.findModule(key)
if err != nil {
return nil, err
}
}
if mod.Dir != "" {
// We found it locally, but in order to load the Tree we need to
// find out if there was another subDir stored from detection.
subDir, err := s.getModuleRoot(mod.Dir)
if err != nil {
// If there's a problem with the subdir record, we'll let the
// recordSubdir method fix it up. Any other errors filesystem
// errors will turn up again below.
// recordSubdir method fix it up. Any other filesystem errors
// will turn up again below.
log.Println("[WARN] error reading subdir record:", err)
} else {
dir := filepath.Join(dir, subDir)
// Load the configurations.Dir(source)
children[m.Name], err = NewTreeModule(m.Name, dir)
fullDir := filepath.Join(mod.Dir, subDir)
child, err := NewTreeModule(m.Name, fullDir)
if err != nil {
return fmt.Errorf("module %s: %s", m.Name, err)
return nil, fmt.Errorf("module %s: %s", m.Name, err)
}
// Set the path of this child
children[m.Name].path = path
child.path = path
child.parent = t
child.version = mod.Version
child.source = m.Source
children[m.Name] = child
continue
}
}
log.Printf("[TRACE] module source: %q", source)
// Split out the subdir if we have one.
// Terraform keeps the entire requested tree, so that modules can
// reference sibling modules from the same archive or repo.
rawSource, subDir := getter.SourceDirSubdir(m.Source)
source, err = getter.Detect(source, t.config.Dir, detectors)
if err != nil {
return fmt.Errorf("module %s: %s", m.Name, err)
// we haven't found a source, so fallback to the go-getter detectors
source := mod.url
if source == "" {
source, err = getter.Detect(rawSource, t.config.Dir, getter.Detectors)
if err != nil {
return nil, fmt.Errorf("module %s: %s", m.Name, err)
}
}
log.Printf("[TRACE] detected module source %q", source)
@ -237,126 +283,190 @@ func (t *Tree) Load(s getter.Storage, mode GetMode) error {
// For example, the registry always adds a subdir of `//*`,
// indicating that we need to strip off the first component from the
// tar archive, though we may not yet know what it is called.
//
// TODO: This can cause us to lose the previously detected subdir. It
// was never an issue before, since none of the supported detectors
// previously had this behavior, but we may want to add this ability to
// registry modules.
source, subDir2 := getter.SourceDirSubdir(source)
if subDir2 != "" {
subDir = subDir2
source, detectedSubDir := getter.SourceDirSubdir(source)
if detectedSubDir != "" {
subDir = filepath.Join(detectedSubDir, subDir)
}
log.Printf("[TRACE] getting module source %q", source)
dir, ok, err := getStorage(s, key, source, mode)
dir, ok, err := s.getStorage(key, source)
if err != nil {
return err
return nil, err
}
if !ok {
return fmt.Errorf(
"module %s: not found, may need to be downloaded using 'terraform get'", m.Name)
return nil, fmt.Errorf("module %s: not found, may need to run 'terraform init'", m.Name)
}
log.Printf("[TRACE] %q stored in %q", source, dir)
// expand and record the subDir for later
fullDir := dir
if subDir != "" {
fullDir, err := getter.SubdirGlob(dir, subDir)
fullDir, err = getter.SubdirGlob(dir, subDir)
if err != nil {
return err
return nil, err
}
// +1 to account for the pathsep
if len(dir)+1 > len(fullDir) {
return fmt.Errorf("invalid module storage path %q", fullDir)
return nil, fmt.Errorf("invalid module storage path %q", fullDir)
}
subDir = fullDir[len(dir)+1:]
if err := t.recordSubdir(dir, subDir); err != nil {
return err
}
dir = fullDir
}
// Load the configurations.Dir(source)
children[m.Name], err = NewTreeModule(m.Name, dir)
// add new info to the module record
mod.Key = key
mod.Dir = dir
mod.Root = subDir
// record the module in our manifest
if err := s.recordModule(mod); err != nil {
return nil, err
}
child, err := NewTreeModule(m.Name, fullDir)
if err != nil {
return fmt.Errorf("module %s: %s", m.Name, err)
return nil, fmt.Errorf("module %s: %s", m.Name, err)
}
// Set the path of this child
children[m.Name].path = path
child.path = path
child.parent = t
child.version = mod.Version
child.source = m.Source
children[m.Name] = child
}
// Go through all the children and load them.
for _, c := range children {
if err := c.Load(s, mode); err != nil {
return err
return children, nil
}
// inheritProviderConfig resolves all provider config inheritance after the
// tree is loaded.
//
// If there is a provider block without a config, look in the parent's Module
// block for a provider, and fetch that provider's configuration. If that
// doesn't exist, assume a default empty config. Implicit providers can still
// inherit their config all the way up from the root, so walk up the tree and
// copy the first matching provider into the module.
func (t *Tree) inheritProviderConfigs(stack []*Tree) {
// the recursive calls only append, so we don't need to worry about copying
// this slice.
stack = append(stack, t)
for _, c := range t.children {
c.inheritProviderConfigs(stack)
}
providers := make(map[string]*config.ProviderConfig)
missingProviders := make(map[string]bool)
for _, p := range t.config.ProviderConfigs {
providers[p.FullName()] = p
}
for _, r := range t.config.Resources {
p := r.ProviderFullName()
if _, ok := providers[p]; !(ok || strings.Contains(p, ".")) {
missingProviders[p] = true
}
}
// Set our tree up
t.children = children
// Search for implicit provider configs
// This adds an empty config is no inherited config is found, so that
// there is always a provider config present.
// This is done in the root module as well, just to set the providers.
for missing := range missingProviders {
// first create an empty provider config
pc := &config.ProviderConfig{
Name: missing,
}
return nil
}
// walk up the stack looking for matching providers
for i := len(stack) - 2; i >= 0; i-- {
pt := stack[i]
var parentProvider *config.ProviderConfig
for _, p := range pt.config.ProviderConfigs {
if p.FullName() == missing {
parentProvider = p
break
}
}
func subdirRecordsPath(dir string) string {
const filename = "module-subdir.json"
// Get the parent directory.
// The current FolderStorage implementation needed to be able to create
// this directory, so we can be reasonably certain we can use it.
parent := filepath.Dir(filepath.Clean(dir))
return filepath.Join(parent, filename)
}
if parentProvider == nil {
continue
}
// unmarshal the records file in the parent directory. Always returns a valid map.
func loadSubdirRecords(dir string) (map[string]string, error) {
records := map[string]string{}
pc.Path = pt.Path()
pc.Path = append([]string{RootName}, pt.path...)
pc.RawConfig = parentProvider.RawConfig
pc.Inherited = true
log.Printf("[TRACE] provider %q inheriting config from %q",
strings.Join(append(t.Path(), pc.FullName()), "."),
strings.Join(append(pt.Path(), parentProvider.FullName()), "."),
)
break
}
recordsPath := subdirRecordsPath(dir)
data, err := ioutil.ReadFile(recordsPath)
if err != nil && !os.IsNotExist(err) {
return records, err
// always set a provider config
if pc.RawConfig == nil {
pc.RawConfig, _ = config.NewRawConfig(map[string]interface{}{})
}
t.config.ProviderConfigs = append(t.config.ProviderConfigs, pc)
}
if len(data) == 0 {
return records, nil
// After allowing the empty implicit configs to be created in root, there's nothing left to inherit
if len(stack) == 1 {
return
}
if err := json.Unmarshal(data, &records); err != nil {
return records, err
}
return records, nil
}
func (t *Tree) getSubdir(dir string) (string, error) {
records, err := loadSubdirRecords(dir)
if err != nil {
return "", err
// get our parent's module config block
parent := stack[len(stack)-2]
var parentModule *config.Module
for _, m := range parent.config.Modules {
if m.Name == t.name {
parentModule = m
break
}
}
return records[dir], nil
}
// Mark the location of a detected subdir in a top-level file so we
// can skip detection when not updating the module.
func (t *Tree) recordSubdir(dir, subdir string) error {
records, err := loadSubdirRecords(dir)
if err != nil {
// if there was a problem with the file, we will attempt to write a new
// one. Any non-data related error should surface there.
log.Printf("[WARN] error reading subdir records: %s", err)
if parentModule == nil {
panic("can't be a module without a parent module config")
}
records[dir] = subdir
// now look for providers that need a config
for p, pc := range providers {
if len(pc.RawConfig.RawMap()) > 0 {
log.Printf("[TRACE] provider %q has a config, continuing", p)
continue
}
js, err := json.Marshal(records)
if err != nil {
return err
// this provider has no config yet, check for one being passed in
parentProviderName, ok := parentModule.Providers[p]
if !ok {
continue
}
var parentProvider *config.ProviderConfig
// there's a config for us in the parent module
for _, pp := range parent.config.ProviderConfigs {
if pp.FullName() == parentProviderName {
parentProvider = pp
break
}
}
if parentProvider == nil {
// no config found, assume defaults
continue
}
// Copy it in, but set an interpolation Scope.
// An interpolation Scope always need to have "root"
pc.Path = append([]string{RootName}, parent.path...)
pc.RawConfig = parentProvider.RawConfig
log.Printf("[TRACE] provider %q inheriting config from %q",
strings.Join(append(t.Path(), pc.FullName()), "."),
strings.Join(append(parent.Path(), parentProvider.FullName()), "."),
)
}
recordsPath := subdirRecordsPath(dir)
return ioutil.WriteFile(recordsPath, js, 0644)
}
// Path is the full path to this tree.
@ -524,6 +634,47 @@ func (t *Tree) Validate() error {
return newErr.ErrOrNil()
}
// versionedPathKey returns a path string with every levels full name, version
// and source encoded. This is to provide a unique key for our module storage,
// since submodules need to know which versions of their ancestor modules they
// are loaded from.
// For example, if module A has a subdirectory B, if module A's source or
// version is updated B's storage key must reflect this change in order for the
// correct version of B's source to be loaded.
func (t *Tree) versionedPathKey(m *Module) string {
path := make([]string, len(t.path)+1)
path[len(path)-1] = m.Name + ";" + m.Source
// We're going to load these in order for easier reading and debugging, but
// in practice they only need to be unique and consistent.
p := t
i := len(path) - 2
for ; i >= 0; i-- {
if p == nil {
break
}
// we may have been loaded under a blank Tree, so always check for a name
// too.
if p.name == "" {
break
}
seg := p.name
if p.version != "" {
seg += "#" + p.version
}
if p.source != "" {
seg += ";" + p.source
}
path[i] = seg
p = p.parent
}
key := strings.Join(path, "|")
return key
}
// treeError is an error use by Tree.Validate to accumulates all
// validation errors.
type treeError struct {

View File

@ -3,6 +3,7 @@ package module
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
@ -263,24 +264,24 @@ func TestTreeLoad_subdir(t *testing.T) {
}
}
func TestTree_recordSubDir(t *testing.T) {
func TestTree_recordManifest(t *testing.T) {
td, err := ioutil.TempDir("", "tf-module")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
storage := moduleStorage{storageDir: td}
dir := filepath.Join(td, "0131bf0fef686e090b16bdbab4910ddf")
subDir := "subDirName"
tree := Tree{}
// record and read the subdir path
if err := tree.recordSubdir(dir, subDir); err != nil {
if err := storage.recordModuleRoot(dir, subDir); err != nil {
t.Fatal(err)
}
actual, err := tree.getSubdir(dir)
actual, err := storage.getModuleRoot(dir)
if err != nil {
t.Fatal(err)
}
@ -291,10 +292,10 @@ func TestTree_recordSubDir(t *testing.T) {
// overwrite the path, and nmake sure we get the new one
subDir = "newSubDir"
if err := tree.recordSubdir(dir, subDir); err != nil {
if err := storage.recordModuleRoot(dir, subDir); err != nil {
t.Fatal(err)
}
actual, err = tree.getSubdir(dir)
actual, err = storage.getModuleRoot(dir)
if err != nil {
t.Fatal(err)
}
@ -304,21 +305,21 @@ func TestTree_recordSubDir(t *testing.T) {
}
// create a fake entry
if err := ioutil.WriteFile(subdirRecordsPath(dir), []byte("BAD DATA"), 0644); err != nil {
if err := ioutil.WriteFile(filepath.Join(td, manifestName), []byte("BAD DATA"), 0644); err != nil {
t.Fatal(err)
}
// this should fail because there aare now 2 entries
actual, err = tree.getSubdir(dir)
actual, err = storage.getModuleRoot(dir)
if err == nil {
t.Fatal("expected multiple subdir entries")
}
// writing the subdir entry should remove the incorrect value
if err := tree.recordSubdir(dir, subDir); err != nil {
if err := storage.recordModuleRoot(dir, subDir); err != nil {
t.Fatal(err)
}
actual, err = tree.getSubdir(dir)
actual, err = storage.getModuleRoot(dir)
if err != nil {
t.Fatal(err)
}
@ -518,6 +519,280 @@ func TestTreeValidate_unknownModule(t *testing.T) {
}
}
func TestTreeProviders_basic(t *testing.T) {
storage := testStorage(t)
tree := NewTree("", testConfig(t, "basic-parent-providers"))
if err := tree.Load(storage, GetModeGet); err != nil {
t.Fatalf("err: %s", err)
}
var a, b *Tree
for _, child := range tree.Children() {
if child.Name() == "a" {
a = child
}
}
rootProviders := tree.config.ProviderConfigsByFullName()
topRaw := rootProviders["top.foo"]
if a == nil {
t.Fatal("could not find module 'a'")
}
for _, child := range a.Children() {
if child.Name() == "c" {
b = child
}
}
if b == nil {
t.Fatal("could not find module 'c'")
}
aProviders := a.config.ProviderConfigsByFullName()
bottomRaw := aProviders["bottom.foo"]
bProviders := b.config.ProviderConfigsByFullName()
bBottom := bProviders["bottom"]
// compare the configs
// top.foo should have been copied to a.top
aTop := aProviders["top"]
if !reflect.DeepEqual(aTop.RawConfig.RawMap(), topRaw.RawConfig.RawMap()) {
log.Fatalf("expected config %#v, got %#v",
topRaw.RawConfig.RawMap(),
aTop.RawConfig.RawMap(),
)
}
if !reflect.DeepEqual(aTop.Path, []string{RootName}) {
log.Fatalf(`expected scope for "top": {"root"}, got %#v`, aTop.Path)
}
if !reflect.DeepEqual(bBottom.RawConfig.RawMap(), bottomRaw.RawConfig.RawMap()) {
t.Fatalf("expected config %#v, got %#v",
bottomRaw.RawConfig.RawMap(),
bBottom.RawConfig.RawMap(),
)
}
if !reflect.DeepEqual(bBottom.Path, []string{RootName, "a"}) {
t.Fatalf(`expected scope for "bottom": {"root", "a"}, got %#v`, bBottom.Path)
}
}
func TestTreeProviders_implicit(t *testing.T) {
storage := testStorage(t)
tree := NewTree("", testConfig(t, "implicit-parent-providers"))
if err := tree.Load(storage, GetModeGet); err != nil {
t.Fatalf("err: %s", err)
}
var child *Tree
for _, c := range tree.Children() {
if c.Name() == "child" {
child = c
}
}
if child == nil {
t.Fatal("could not find module 'child'")
}
// child should have inherited foo
providers := child.config.ProviderConfigsByFullName()
foo := providers["foo"]
if foo == nil {
t.Fatal("could not find provider 'foo' in child module")
}
if !reflect.DeepEqual([]string{RootName}, foo.Path) {
t.Fatalf(`expected foo scope of {"root"}, got %#v`, foo.Path)
}
expected := map[string]interface{}{
"value": "from root",
}
if !reflect.DeepEqual(expected, foo.RawConfig.RawMap()) {
t.Fatalf(`expected "foo" config %#v, got: %#v`, expected, foo.RawConfig.RawMap())
}
}
func TestTreeProviders_implicitMultiLevel(t *testing.T) {
storage := testStorage(t)
tree := NewTree("", testConfig(t, "implicit-grandparent-providers"))
if err := tree.Load(storage, GetModeGet); err != nil {
t.Fatalf("err: %s", err)
}
var child, grandchild *Tree
for _, c := range tree.Children() {
if c.Name() == "child" {
child = c
}
}
if child == nil {
t.Fatal("could not find module 'child'")
}
for _, c := range child.Children() {
if c.Name() == "grandchild" {
grandchild = c
}
}
if grandchild == nil {
t.Fatal("could not find module 'grandchild'")
}
// child should have inherited foo
providers := child.config.ProviderConfigsByFullName()
foo := providers["foo"]
if foo == nil {
t.Fatal("could not find provider 'foo' in child module")
}
if !reflect.DeepEqual([]string{RootName}, foo.Path) {
t.Fatalf(`expected foo scope of {"root"}, got %#v`, foo.Path)
}
expected := map[string]interface{}{
"value": "from root",
}
if !reflect.DeepEqual(expected, foo.RawConfig.RawMap()) {
t.Fatalf(`expected "foo" config %#v, got: %#v`, expected, foo.RawConfig.RawMap())
}
// grandchild should have inherited bar
providers = grandchild.config.ProviderConfigsByFullName()
bar := providers["bar"]
if bar == nil {
t.Fatal("could not find provider 'bar' in grandchild module")
}
if !reflect.DeepEqual([]string{RootName, "child"}, bar.Path) {
t.Fatalf(`expected bar scope of {"root", "child"}, got %#v`, bar.Path)
}
expected = map[string]interface{}{
"value": "from child",
}
if !reflect.DeepEqual(expected, bar.RawConfig.RawMap()) {
t.Fatalf(`expected "bar" config %#v, got: %#v`, expected, bar.RawConfig.RawMap())
}
}
func TestTreeLoad_conflictingSubmoduleNames(t *testing.T) {
storage := testStorage(t)
tree := NewTree("", testConfig(t, "conficting-submodule-names"))
if err := tree.Load(storage, GetModeGet); err != nil {
t.Fatalf("load failed: %s", err)
}
if !tree.Loaded() {
t.Fatal("should be loaded")
}
// Try to reload
if err := tree.Load(storage, GetModeNone); err != nil {
t.Fatalf("reload failed: %s", err)
}
// verify that the grand-children are correctly loaded
for _, c := range tree.Children() {
for _, gc := range c.Children() {
if len(gc.config.Resources) != 1 {
t.Fatalf("expected 1 resource in %s, got %d", gc.name, len(gc.config.Resources))
}
res := gc.config.Resources[0]
switch gc.path[0] {
case "a":
if res.Name != "a-c" {
t.Fatal("found wrong resource in a/c:", res.Name)
}
case "b":
if res.Name != "b-c" {
t.Fatal("found wrong resource in b/c:", res.Name)
}
}
}
}
}
// changing the source for a module but not the module "path"
func TestTreeLoad_changeIntermediateSource(t *testing.T) {
// copy the config to our tempdir this time, since we're going to edit it
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
if err := copyDir(td, filepath.Join(fixtureDir, "change-intermediate-source")); err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(td); err != nil {
t.Fatal(err)
}
defer os.Chdir(wd)
if err := os.MkdirAll(".terraform/modules", 0777); err != nil {
t.Fatal(err)
}
storage := &getter.FolderStorage{StorageDir: ".terraform/modules"}
cfg, err := config.LoadDir("./")
if err != nil {
t.Fatal(err)
}
tree := NewTree("", cfg)
if err := tree.Load(storage, GetModeGet); err != nil {
t.Fatalf("load failed: %s", err)
}
// now we change the source of our module, without changing its path
if err := os.Rename("main.tf.disabled", "main.tf"); err != nil {
t.Fatal(err)
}
// reload the tree
cfg, err = config.LoadDir("./")
if err != nil {
t.Fatal(err)
}
tree = NewTree("", cfg)
if err := tree.Load(storage, GetModeGet); err != nil {
t.Fatalf("load failed: %s", err)
}
// check for our resource in b
for _, c := range tree.Children() {
for _, gc := range c.Children() {
if len(gc.config.Resources) != 1 {
t.Fatalf("expected 1 resource in %s, got %d", gc.name, len(gc.config.Resources))
}
res := gc.config.Resources[0]
expected := "c-b"
if res.Name != expected {
t.Fatalf("expexted resource %q, got %q", expected, res.Name)
}
}
}
}
const treeLoadStr = `
root
foo (path: foo)
@ -533,3 +808,8 @@ root
foo (path: foo)
bar (path: foo, bar)
`
const treeLoadRegistrySubdirStr = `
root
foo (path: foo)
`

95
config/module/versions.go Normal file
View File

@ -0,0 +1,95 @@
package module
import (
"errors"
"fmt"
"sort"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/registry/response"
)
const anyVersion = ">=0.0.0"
// return the newest version that satisfies the provided constraint
func newest(versions []string, constraint string) (string, error) {
if constraint == "" {
constraint = anyVersion
}
cs, err := version.NewConstraint(constraint)
if err != nil {
return "", err
}
switch len(versions) {
case 0:
return "", errors.New("no versions found")
case 1:
v, err := version.NewVersion(versions[0])
if err != nil {
return "", err
}
if !cs.Check(v) {
return "", fmt.Errorf("no version found matching constraint %q", constraint)
}
return versions[0], nil
}
sort.Slice(versions, func(i, j int) bool {
// versions should have already been validated
// sort invalid version strings to the end
iv, err := version.NewVersion(versions[i])
if err != nil {
return true
}
jv, err := version.NewVersion(versions[j])
if err != nil {
return true
}
return iv.GreaterThan(jv)
})
// versions are now in order, so just find the first which satisfies the
// constraint
for i := range versions {
v, err := version.NewVersion(versions[i])
if err != nil {
continue
}
if cs.Check(v) {
return versions[i], nil
}
}
return "", nil
}
// return the newest *moduleVersion that matches the given constraint
// TODO: reconcile these two types and newest* functions
func newestVersion(moduleVersions []*response.ModuleVersion, constraint string) (*response.ModuleVersion, error) {
var versions []string
modules := make(map[string]*response.ModuleVersion)
for _, m := range moduleVersions {
versions = append(versions, m.Version)
modules[m.Version] = m
}
match, err := newest(versions, constraint)
return modules[match], err
}
// return the newest moduleRecord that matches the given constraint
func newestRecord(moduleVersions []moduleRecord, constraint string) (moduleRecord, error) {
var versions []string
modules := make(map[string]moduleRecord)
for _, m := range moduleVersions {
versions = append(versions, m.Version)
modules[m.Version] = m
}
match, err := newest(versions, constraint)
return modules[match], err
}

View File

@ -0,0 +1,60 @@
package module
import (
"testing"
"github.com/hashicorp/terraform/registry/response"
)
func TestNewestModuleVersion(t *testing.T) {
mpv := &response.ModuleProviderVersions{
Source: "registry/test/module",
Versions: []*response.ModuleVersion{
{Version: "0.0.4"},
{Version: "0.3.1"},
{Version: "2.0.1"},
{Version: "1.2.0"},
},
}
m, err := newestVersion(mpv.Versions, "")
if err != nil {
t.Fatal(err)
}
expected := "2.0.1"
if m.Version != expected {
t.Fatalf("expected version %q, got %q", expected, m.Version)
}
// now with a constraint
m, err = newestVersion(mpv.Versions, "~>1.0")
if err != nil {
t.Fatal(err)
}
expected = "1.2.0"
if m.Version != expected {
t.Fatalf("expected version %q, got %q", expected, m.Version)
}
}
func TestNewestInvalidModuleVersion(t *testing.T) {
mpv := &response.ModuleProviderVersions{
Source: "registry/test/module",
Versions: []*response.ModuleVersion{
{Version: "WTF"},
{Version: "2.0.1"},
},
}
m, err := newestVersion(mpv.Versions, "")
if err != nil {
t.Fatal(err)
}
expected := "2.0.1"
if m.Version != expected {
t.Fatalf("expected version %q, got %q", expected, m.Version)
}
}

View File

@ -0,0 +1,7 @@
module "child" {
source = "./child"
version = "0.1.2"
providers = {
"aws" = "aws.foo"
}
}

View File

@ -132,6 +132,12 @@ func (m *Module) String() string {
return m.formatWithPrefix(hostPrefix, true)
}
// Module returns just the registry ID of the module, without a hostname or
// suffix.
func (m *Module) Module() string {
return fmt.Sprintf("%s/%s/%s", m.RawNamespace, m.RawName, m.RawProvider)
}
// Equal compares the module source against another instance taking
// normalization into account.
func (m *Module) Equal(other *Module) bool {

View File

@ -2639,105 +2639,107 @@ module.child:
`)
}
func TestContext2Apply_moduleOrphanProvider(t *testing.T) {
m := testModule(t, "apply-module-orphan-provider-inherit")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
//// FIXME: how do we handle this one?
//func TestContext2Apply_moduleOrphanProvider(t *testing.T) {
// m := testModule(t, "apply-module-orphan-provider-inherit")
// p := testProvider("aws")
// p.ApplyFn = testApplyFn
// p.DiffFn = testDiffFn
p.ConfigureFn = func(c *ResourceConfig) error {
if _, ok := c.Get("value"); !ok {
return fmt.Errorf("value is not found")
}
// p.ConfigureFn = func(c *ResourceConfig) error {
// if _, ok := c.Get("value"); !ok {
// return fmt.Errorf("value is not found")
// }
return nil
}
// return nil
// }
// Create a state with an orphan module
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
},
},
}
// // Create a state with an orphan module
// state := &State{
// Modules: []*ModuleState{
// &ModuleState{
// Path: []string{"root", "child"},
// Resources: map[string]*ResourceState{
// "aws_instance.bar": &ResourceState{
// Type: "aws_instance",
// Primary: &InstanceState{
// ID: "bar",
// },
// },
// },
// },
// },
// }
ctx := testContext2(t, &ContextOpts{
Module: m,
State: state,
ProviderResolver: ResourceProviderResolverFixed(
map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
),
})
// ctx := testContext2(t, &ContextOpts{
// Module: m,
// State: state,
// ProviderResolver: ResourceProviderResolverFixed(
// map[string]ResourceProviderFactory{
// "aws": testProviderFuncFixed(p),
// },
// ),
// })
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
// if _, err := ctx.Plan(); err != nil {
// t.Fatalf("err: %s", err)
// }
if _, err := ctx.Apply(); err != nil {
t.Fatalf("err: %s", err)
}
}
// if _, err := ctx.Apply(); err != nil {
// t.Fatalf("err: %s", err)
// }
//}
func TestContext2Apply_moduleOrphanGrandchildProvider(t *testing.T) {
m := testModule(t, "apply-module-orphan-provider-inherit")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
//// FIXME: how do we handle this one?
//func TestContext2Apply_moduleOrphanGrandchildProvider(t *testing.T) {
// m := testModule(t, "apply-module-orphan-provider-inherit")
// p := testProvider("aws")
// p.ApplyFn = testApplyFn
// p.DiffFn = testDiffFn
p.ConfigureFn = func(c *ResourceConfig) error {
if _, ok := c.Get("value"); !ok {
return fmt.Errorf("value is not found")
}
// p.ConfigureFn = func(c *ResourceConfig) error {
// if _, ok := c.Get("value"); !ok {
// return fmt.Errorf("value is not found")
// }
return nil
}
// return nil
// }
// Create a state with an orphan module that is nested (grandchild)
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root", "parent", "child"},
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
},
},
}
// // Create a state with an orphan module that is nested (grandchild)
// state := &State{
// Modules: []*ModuleState{
// &ModuleState{
// Path: []string{"root", "parent", "child"},
// Resources: map[string]*ResourceState{
// "aws_instance.bar": &ResourceState{
// Type: "aws_instance",
// Primary: &InstanceState{
// ID: "bar",
// },
// },
// },
// },
// },
// }
ctx := testContext2(t, &ContextOpts{
Module: m,
State: state,
ProviderResolver: ResourceProviderResolverFixed(
map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
),
})
// ctx := testContext2(t, &ContextOpts{
// Module: m,
// State: state,
// ProviderResolver: ResourceProviderResolverFixed(
// map[string]ResourceProviderFactory{
// "aws": testProviderFuncFixed(p),
// },
// ),
// })
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
// if _, err := ctx.Plan(); err != nil {
// t.Fatalf("err: %s", err)
// }
if _, err := ctx.Apply(); err != nil {
t.Fatalf("err: %s", err)
}
}
// if _, err := ctx.Apply(); err != nil {
// t.Fatalf("err: %s", err)
// }
//}
func TestContext2Apply_moduleGrandchildProvider(t *testing.T) {
m := testModule(t, "apply-module-grandchild-provider-inherit")

View File

@ -226,10 +226,10 @@ func TestContextImport_moduleProvider(t *testing.T) {
}
}
// Test that import sets up the graph properly for provider inheritance
func TestContextImport_providerInherit(t *testing.T) {
// Importing into a module requires a provider config in that module.
func TestContextImport_providerModule(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "import-provider-inherit")
m := testModule(t, "import-provider-module")
ctx := testContext2(t, &ContextOpts{
Module: m,
ProviderResolver: ResourceProviderResolverFixed(

View File

@ -1,6 +1,7 @@
package terraform
import (
"errors"
"reflect"
"strings"
"sync"
@ -211,16 +212,25 @@ func TestContext2Input_providerOnce(t *testing.T) {
count := 0
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
count++
return nil, nil
_, set := c.Config["from_input"]
if count == 1 {
if set {
return nil, errors.New("from_input should not be set")
}
c.Config["from_input"] = "x"
}
if count > 1 && !set {
return nil, errors.New("from_input should be set")
}
return c, nil
}
if err := ctx.Input(InputModeStd); err != nil {
t.Fatalf("err: %s", err)
}
if count != 1 {
t.Fatalf("should only be called once: %d", count)
}
}
func TestContext2Input_providerId(t *testing.T) {

View File

@ -643,67 +643,6 @@ func TestContext2Plan_moduleProviderInheritDeep(t *testing.T) {
}
}
func TestContext2Plan_moduleProviderDefaults(t *testing.T) {
var l sync.Mutex
var calls []string
toCount := 0
m := testModule(t, "plan-module-provider-defaults")
ctx := testContext2(t, &ContextOpts{
Module: m,
ProviderResolver: ResourceProviderResolverFixed(
map[string]ResourceProviderFactory{
"aws": func() (ResourceProvider, error) {
l.Lock()
defer l.Unlock()
p := testProvider("aws")
p.ConfigureFn = func(c *ResourceConfig) error {
if v, ok := c.Get("from"); !ok || v.(string) != "root" {
return fmt.Errorf("bad")
}
if v, ok := c.Get("to"); ok && v.(string) == "child" {
toCount++
}
return nil
}
p.DiffFn = func(
info *InstanceInfo,
state *InstanceState,
c *ResourceConfig) (*InstanceDiff, error) {
v, _ := c.Get("from")
l.Lock()
defer l.Unlock()
calls = append(calls, v.(string))
return testDiffFn(info, state, c)
}
return p, nil
},
},
),
})
_, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}
if toCount != 1 {
t.Fatalf(
"provider in child didn't set proper config\n\n"+
"toCount: %d", toCount)
}
actual := calls
sort.Strings(actual)
expected := []string{"child", "root"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestContext2Plan_moduleProviderDefaultsVar(t *testing.T) {
var l sync.Mutex
var calls []string
@ -749,10 +688,14 @@ func TestContext2Plan_moduleProviderDefaultsVar(t *testing.T) {
expected := []string{
"root\n",
"root\nchild\n",
// this test originally verified that a parent provider config can
// partially override a child. That's no longer the case, so the child
// config is used in its entirety here.
//"root\nchild\n",
"child\nchild\n",
}
if !reflect.DeepEqual(calls, expected) {
t.Fatalf("BAD: %#v", calls)
t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, calls)
}
}
@ -3650,15 +3593,8 @@ output "out" {
}
_, err = ctx.Plan()
switch {
case featureOutputErrors:
if err == nil {
t.Fatal("expected error")
}
default:
if err != nil {
t.Fatalf("plan err: %s", err)
}
if err == nil {
t.Fatal("expected error")
}
}
@ -3705,15 +3641,7 @@ resource "aws_instance" "foo" {
}
_, err = ctx.Plan()
switch {
case featureOutputErrors:
if err == nil {
t.Fatal("expected error")
}
default:
if err != nil {
t.Fatalf("plan err: %s", err)
}
if err == nil {
t.Fatal("expected error")
}
}

View File

@ -321,78 +321,55 @@ func TestContext2Validate_moduleDepsShouldNotCycle(t *testing.T) {
}
}
func TestContext2Validate_moduleProviderInherit(t *testing.T) {
m := testModule(t, "validate-module-pc-inherit")
p := testProvider("aws")
c := testContext2(t, &ContextOpts{
Module: m,
ProviderResolver: ResourceProviderResolverFixed(
map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
),
})
//// FIXME: provider must still exist in config, but we should be able to locate
//// it elsewhere
//func TestContext2Validate_moduleProviderInheritOrphan(t *testing.T) {
// m := testModule(t, "validate-module-pc-inherit-orphan")
// p := testProvider("aws")
// c := testContext2(t, &ContextOpts{
// Module: m,
// ProviderResolver: ResourceProviderResolverFixed(
// map[string]ResourceProviderFactory{
// "aws": testProviderFuncFixed(p),
// },
// ),
// State: &State{
// Modules: []*ModuleState{
// &ModuleState{
// Path: []string{"root", "child"},
// Resources: map[string]*ResourceState{
// "aws_instance.bar": &ResourceState{
// Type: "aws_instance",
// Primary: &InstanceState{
// ID: "bar",
// },
// },
// },
// },
// },
// },
// })
p.ValidateFn = func(c *ResourceConfig) ([]string, []error) {
return nil, c.CheckSet([]string{"set"})
}
// p.ValidateFn = func(c *ResourceConfig) ([]string, []error) {
// v, ok := c.Get("set")
// if !ok {
// return nil, []error{fmt.Errorf("not set")}
// }
// if v != "bar" {
// return nil, []error{fmt.Errorf("bad: %#v", v)}
// }
w, e := c.Validate()
if len(w) > 0 {
t.Fatalf("bad: %#v", w)
}
if len(e) > 0 {
t.Fatalf("bad: %s", e)
}
}
// return nil, nil
// }
func TestContext2Validate_moduleProviderInheritOrphan(t *testing.T) {
m := testModule(t, "validate-module-pc-inherit-orphan")
p := testProvider("aws")
c := testContext2(t, &ContextOpts{
Module: m,
ProviderResolver: ResourceProviderResolverFixed(
map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
),
State: &State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
},
},
},
},
},
})
p.ValidateFn = func(c *ResourceConfig) ([]string, []error) {
v, ok := c.Get("set")
if !ok {
return nil, []error{fmt.Errorf("not set")}
}
if v != "bar" {
return nil, []error{fmt.Errorf("bad: %#v", v)}
}
return nil, nil
}
w, e := c.Validate()
if len(w) > 0 {
t.Fatalf("bad: %#v", w)
}
if len(e) > 0 {
t.Fatalf("bad: %s", e)
}
}
// w, e := c.Validate()
// if len(w) > 0 {
// t.Fatalf("bad: %#v", w)
// }
// if len(e) > 0 {
// t.Fatalf("bad: %s", e)
// }
//}
func TestContext2Validate_moduleProviderVar(t *testing.T) {
m := testModule(t, "validate-module-pc-vars")

View File

@ -40,8 +40,6 @@ type EvalContext interface {
// is used to store the provider configuration for inheritance lookups
// with ParentProviderConfig().
ConfigureProvider(string, *ResourceConfig) error
SetProviderConfig(string, *ResourceConfig) error
ParentProviderConfig(string) *ResourceConfig
// ProviderInput and SetProviderInput are used to configure providers
// from user input.
@ -69,6 +67,13 @@ type EvalContext interface {
// that is currently being acted upon.
Interpolate(*config.RawConfig, *Resource) (*ResourceConfig, error)
// InterpolateProvider takes a ProviderConfig and interpolates it with the
// stored interpolation scope. Since provider configurations can be
// inherited, the interpolation scope may be different from the current
// context path. Interplation is otherwise executed the same as in the
// Interpolation method.
InterpolateProvider(*config.ProviderConfig, *Resource) (*ResourceConfig, error)
// SetVariables sets the variables for the module within
// this context with the name n. This function call is additive:
// the second parameter is merged with any previous call.

View File

@ -34,7 +34,6 @@ type BuiltinEvalContext struct {
Hooks []Hook
InputValue UIInput
ProviderCache map[string]ResourceProvider
ProviderConfigCache map[string]*ResourceConfig
ProviderInputConfig map[string]map[string]interface{}
ProviderLock *sync.Mutex
ProvisionerCache map[string]ResourceProvisioner
@ -149,28 +148,9 @@ func (ctx *BuiltinEvalContext) ConfigureProvider(
if p == nil {
return fmt.Errorf("Provider '%s' not initialized", n)
}
if err := ctx.SetProviderConfig(n, cfg); err != nil {
return nil
}
return p.Configure(cfg)
}
func (ctx *BuiltinEvalContext) SetProviderConfig(
n string, cfg *ResourceConfig) error {
providerPath := make([]string, len(ctx.Path())+1)
copy(providerPath, ctx.Path())
providerPath[len(providerPath)-1] = n
// Save the configuration
ctx.ProviderLock.Lock()
ctx.ProviderConfigCache[PathCacheKey(providerPath)] = cfg
ctx.ProviderLock.Unlock()
return nil
}
func (ctx *BuiltinEvalContext) ProviderInput(n string) map[string]interface{} {
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
@ -203,27 +183,6 @@ func (ctx *BuiltinEvalContext) SetProviderInput(n string, c map[string]interface
ctx.ProviderLock.Unlock()
}
func (ctx *BuiltinEvalContext) ParentProviderConfig(n string) *ResourceConfig {
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
// Make a copy of the path so we can safely edit it
path := ctx.Path()
pathCopy := make([]string, len(path)+1)
copy(pathCopy, path)
// Go up the tree.
for i := len(path) - 1; i >= 0; i-- {
pathCopy[i+1] = n
k := PathCacheKey(pathCopy[:i+2])
if v, ok := ctx.ProviderConfigCache[k]; ok {
return v
}
}
return nil
}
func (ctx *BuiltinEvalContext) InitProvisioner(
n string) (ResourceProvisioner, error) {
ctx.once.Do(ctx.init)
@ -289,6 +248,7 @@ func (ctx *BuiltinEvalContext) CloseProvisioner(n string) error {
func (ctx *BuiltinEvalContext) Interpolate(
cfg *config.RawConfig, r *Resource) (*ResourceConfig, error) {
if cfg != nil {
scope := &InterpolationScope{
Path: ctx.Path(),
@ -311,6 +271,40 @@ func (ctx *BuiltinEvalContext) Interpolate(
return result, nil
}
func (ctx *BuiltinEvalContext) InterpolateProvider(
pc *config.ProviderConfig, r *Resource) (*ResourceConfig, error) {
var cfg *config.RawConfig
if pc != nil && pc.RawConfig != nil {
path := pc.Path
if len(path) == 0 {
path = ctx.Path()
}
scope := &InterpolationScope{
Path: path,
Resource: r,
}
cfg = pc.RawConfig
vs, err := ctx.Interpolater.Values(scope, cfg.Variables)
if err != nil {
return nil, err
}
// Do the interpolation
if err := cfg.Interpolate(vs); err != nil {
return nil, err
}
}
result := NewResourceConfig(cfg)
result.interpolateForce()
return result, nil
}
func (ctx *BuiltinEvalContext) Path() []string {
return ctx.PathValue
}

View File

@ -45,14 +45,6 @@ type MockEvalContext struct {
ConfigureProviderConfig *ResourceConfig
ConfigureProviderError error
SetProviderConfigCalled bool
SetProviderConfigName string
SetProviderConfigConfig *ResourceConfig
ParentProviderConfigCalled bool
ParentProviderConfigName string
ParentProviderConfigConfig *ResourceConfig
InitProvisionerCalled bool
InitProvisionerName string
InitProvisionerProvisioner ResourceProvisioner
@ -72,6 +64,12 @@ type MockEvalContext struct {
InterpolateConfigResult *ResourceConfig
InterpolateError error
InterpolateProviderCalled bool
InterpolateProviderConfig *config.ProviderConfig
InterpolateProviderResource *Resource
InterpolateProviderConfigResult *ResourceConfig
InterpolateProviderError error
PathCalled bool
PathPath []string
@ -134,20 +132,6 @@ func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error
return c.ConfigureProviderError
}
func (c *MockEvalContext) SetProviderConfig(
n string, cfg *ResourceConfig) error {
c.SetProviderConfigCalled = true
c.SetProviderConfigName = n
c.SetProviderConfigConfig = cfg
return nil
}
func (c *MockEvalContext) ParentProviderConfig(n string) *ResourceConfig {
c.ParentProviderConfigCalled = true
c.ParentProviderConfigName = n
return c.ParentProviderConfigConfig
}
func (c *MockEvalContext) ProviderInput(n string) map[string]interface{} {
c.ProviderInputCalled = true
c.ProviderInputName = n
@ -186,6 +170,14 @@ func (c *MockEvalContext) Interpolate(
return c.InterpolateConfigResult, c.InterpolateError
}
func (c *MockEvalContext) InterpolateProvider(
config *config.ProviderConfig, resource *Resource) (*ResourceConfig, error) {
c.InterpolateProviderCalled = true
c.InterpolateProviderConfig = config
c.InterpolateProviderResource = resource
return c.InterpolateProviderConfigResult, c.InterpolateError
}
func (c *MockEvalContext) Path() []string {
c.PathCalled = true
return c.PathPath

View File

@ -31,3 +31,26 @@ func (n *EvalInterpolate) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
// EvalInterpolateProvider is an EvalNode implementation that takes a
// ProviderConfig and interpolates it. Provider configurations are the only
// "inherited" type of configuration we have, and the original raw config may
// have a different interpolation scope.
type EvalInterpolateProvider struct {
Config *config.ProviderConfig
Resource *Resource
Output **ResourceConfig
}
func (n *EvalInterpolateProvider) Eval(ctx EvalContext) (interface{}, error) {
rc, err := ctx.InterpolateProvider(n.Config, n.Resource)
if err != nil {
return nil, err
}
if n.Output != nil {
*n.Output = rc
}
return nil, nil
}

View File

@ -68,22 +68,17 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
// handling the interpolation error
if err != nil {
switch {
case featureOutputErrors:
if n.ContinueOnErr {
log.Printf("[ERROR] Output interpolation %q failed: %s", n.Name, err)
// if we're continueing, make sure the output is included, and
// marked as unknown
mod.Outputs[n.Name] = &OutputState{
Type: "string",
Value: config.UnknownVariableValue,
}
return nil, EvalEarlyExitError{}
if n.ContinueOnErr {
log.Printf("[ERROR] Output interpolation %q failed: %s", n.Name, err)
// if we're continueing, make sure the output is included, and
// marked as unknown
mod.Outputs[n.Name] = &OutputState{
Type: "string",
Value: config.UnknownVariableValue,
}
return nil, err
default:
log.Printf("[WARN] Output interpolation %q failed: %s", n.Name, err)
return nil, EvalEarlyExitError{}
}
return nil, err
}
// Get the value from the config

View File

@ -6,17 +6,6 @@ import (
"github.com/hashicorp/terraform/config"
)
// EvalSetProviderConfig sets the parent configuration for a provider
// without configuring that provider, validating it, etc.
type EvalSetProviderConfig struct {
Provider string
Config **ResourceConfig
}
func (n *EvalSetProviderConfig) Eval(ctx EvalContext) (interface{}, error) {
return nil, ctx.SetProviderConfig(n.Provider, *n.Config)
}
// EvalBuildProviderConfig outputs a *ResourceConfig that is properly
// merged with parents and inputs on top of what is configured in the file.
type EvalBuildProviderConfig struct {
@ -28,7 +17,7 @@ type EvalBuildProviderConfig struct {
func (n *EvalBuildProviderConfig) Eval(ctx EvalContext) (interface{}, error) {
cfg := *n.Config
// If we have a configuration set, then merge that in
// If we have an Input configuration set, then merge that in
if input := ctx.ProviderInput(n.Provider); input != nil {
// "input" is a map of the subset of config values that were known
// during the input walk, set by EvalInputProvider. Note that
@ -40,13 +29,7 @@ func (n *EvalBuildProviderConfig) Eval(ctx EvalContext) (interface{}, error) {
return nil, err
}
merged := cfg.raw.Merge(rc)
cfg = NewResourceConfig(merged)
}
// Get the parent configuration if there is one
if parent := ctx.ParentProviderConfig(n.Provider); parent != nil {
merged := cfg.raw.Merge(parent.raw)
merged := rc.Merge(cfg.raw)
cfg = NewResourceConfig(merged)
}
@ -116,12 +99,8 @@ type EvalInputProvider struct {
}
func (n *EvalInputProvider) Eval(ctx EvalContext) (interface{}, error) {
// If we already configured this provider, then don't do this again
if v := ctx.ProviderInput(n.Name); v != nil {
return nil, nil
}
rc := *n.Config
orig := rc.DeepCopy()
// Wrap the input into a namespace
input := &PrefixUIInput{
@ -138,27 +117,19 @@ func (n *EvalInputProvider) Eval(ctx EvalContext) (interface{}, error) {
"Error configuring %s: %s", n.Name, err)
}
// Set the input that we received so that child modules don't attempt
// to ask for input again.
// We only store values that have changed through Input.
// The goal is to cache cache input responses, not to provide a complete
// config for other providers.
confMap := make(map[string]interface{})
if config != nil && len(config.Config) > 0 {
// This repository of provider input results on the context doesn't
// retain config.ComputedKeys, so we need to filter those out here
// in order that later users of this data won't try to use the unknown
// value placeholder as if it were a literal value. This map is just
// of known values we've been able to complete so far; dynamic stuff
// will be merged in by EvalBuildProviderConfig on subsequent
// (post-input) walks.
confMap := config.Config
if config.ComputedKeys != nil {
for _, key := range config.ComputedKeys {
delete(confMap, key)
// any values that weren't in the original ResourcConfig will be cached
for k, v := range config.Config {
if _, ok := orig.Config[k]; !ok {
confMap[k] = v
}
}
ctx.SetProviderInput(n.Name, confMap)
} else {
ctx.SetProviderInput(n.Name, map[string]interface{}{})
}
ctx.SetProviderInput(n.Name, confMap)
return nil, nil
}

View File

@ -26,10 +26,6 @@ func TestEvalBuildProviderConfig(t *testing.T) {
}
ctx := &MockEvalContext{
ParentProviderConfigConfig: testResourceConfig(t, map[string]interface{}{
"inherited_from_parent": "parent",
"set_in_config_and_parent": "parent",
}),
ProviderInputConfig: map[string]interface{}{
"set_in_config": "input",
"set_by_input": "input",
@ -39,51 +35,15 @@ func TestEvalBuildProviderConfig(t *testing.T) {
t.Fatalf("err: %s", err)
}
// This is a merger of the following, with later items taking precedence:
// - "config" (the config as written in the current module, with all
// interpolation expressions resolved)
// - ProviderInputConfig (mock of config produced by the input walk, after
// prompting the user interactively for values unspecified in config)
// - ParentProviderConfigConfig (mock of config inherited from a parent module)
// We expect the provider config with the added input value
expected := map[string]interface{}{
"set_in_config": "input", // in practice, input map contains identical literals from config
"set_in_config_and_parent": "parent",
"inherited_from_parent": "parent",
"set_in_config": "config",
"set_in_config_and_parent": "config",
"computed_in_config": "config",
"set_by_input": "input",
}
if !reflect.DeepEqual(config.Raw, expected) {
t.Fatalf("incorrect merged config %#v; want %#v", config.Raw, expected)
}
}
func TestEvalBuildProviderConfig_parentPriority(t *testing.T) {
config := testResourceConfig(t, map[string]interface{}{})
provider := "foo"
n := &EvalBuildProviderConfig{
Provider: provider,
Config: &config,
Output: &config,
}
ctx := &MockEvalContext{
ParentProviderConfigConfig: testResourceConfig(t, map[string]interface{}{
"foo": "bar",
}),
ProviderInputConfig: map[string]interface{}{
"foo": "baz",
},
}
if _, err := n.Eval(ctx); err != nil {
t.Fatalf("err: %s", err)
}
expected := map[string]interface{}{
"foo": "bar",
}
if !reflect.DeepEqual(config.Raw, expected) {
t.Fatalf("bad: %#v", config.Raw)
t.Fatalf("incorrect merged config:\n%#v\nwanted:\n%#v", config.Raw, expected)
}
}
@ -177,9 +137,7 @@ func TestEvalInputProvider(t *testing.T) {
}
rawConfig, err := config.NewRawConfig(map[string]interface{}{
"set_in_config": "input",
"set_by_input": "input",
"computed": "fake_computed",
"set_by_input": "input",
})
if err != nil {
return nil, err
@ -192,7 +150,8 @@ func TestEvalInputProvider(t *testing.T) {
}
ctx := &MockEvalContext{ProviderProvider: provider}
rawConfig, err := config.NewRawConfig(map[string]interface{}{
"mock_config": "mock",
"mock_config": "mock",
"set_in_config": "input",
})
if err != nil {
t.Fatalf("NewRawConfig failed: %s", err)
@ -222,12 +181,12 @@ func TestEvalInputProvider(t *testing.T) {
}
inputCfg := ctx.SetProviderInputConfig
// we should only have the value that was set during Input
want := map[string]interface{}{
"set_in_config": "input",
"set_by_input": "input",
// "computed" is omitted because it value isn't known at input time
"set_by_input": "input",
}
if !reflect.DeepEqual(inputCfg, want) {
t.Errorf("got incorrect input config %#v; want %#v", inputCfg, want)
t.Errorf("got incorrect input config:\n%#v\nwant:\n%#v", inputCfg, want)
}
}

View File

@ -6,7 +6,7 @@ import (
// ProviderEvalTree returns the evaluation tree for initializing and
// configuring providers.
func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {
func ProviderEvalTree(n string, config *config.ProviderConfig) EvalNode {
var provider ResourceProvider
var resourceConfig *ResourceConfig
@ -22,7 +22,7 @@ func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {
Name: n,
Output: &provider,
},
&EvalInterpolate{
&EvalInterpolateProvider{
Config: config,
Output: &resourceConfig,
},
@ -48,7 +48,7 @@ func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {
Name: n,
Output: &provider,
},
&EvalInterpolate{
&EvalInterpolateProvider{
Config: config,
Output: &resourceConfig,
},
@ -61,10 +61,6 @@ func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {
Provider: &provider,
Config: &resourceConfig,
},
&EvalSetProviderConfig{
Provider: n,
Config: &resourceConfig,
},
},
},
})
@ -78,7 +74,7 @@ func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {
Name: n,
Output: &provider,
},
&EvalInterpolate{
&EvalInterpolateProvider{
Config: config,
Output: &resourceConfig,
},
@ -87,10 +83,6 @@ func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {
Config: &resourceConfig,
Output: &resourceConfig,
},
&EvalSetProviderConfig{
Provider: n,
Config: &resourceConfig,
},
},
},
})

View File

@ -1,9 +1,3 @@
package terraform
import (
"os"
)
// This file holds feature flags for the next release
var featureOutputErrors = os.Getenv("TF_OUTPUT_ERRORS") != ""

View File

@ -32,7 +32,6 @@ type ContextGraphWalker struct {
interpolaterVars map[string]map[string]interface{}
interpolaterVarLock sync.Mutex
providerCache map[string]ResourceProvider
providerConfigCache map[string]*ResourceConfig
providerLock sync.Mutex
provisionerCache map[string]ResourceProvisioner
provisionerLock sync.Mutex
@ -67,13 +66,13 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext {
w.interpolaterVarLock.Unlock()
ctx := &BuiltinEvalContext{
StopContext: w.StopContext,
PathValue: path,
Hooks: w.Context.hooks,
InputValue: w.Context.uiInput,
Components: w.Context.components,
ProviderCache: w.providerCache,
ProviderConfigCache: w.providerConfigCache,
StopContext: w.StopContext,
PathValue: path,
Hooks: w.Context.hooks,
InputValue: w.Context.uiInput,
Components: w.Context.components,
ProviderCache: w.providerCache,
//ProviderConfigCache: w.providerConfigCache,
ProviderInputConfig: w.Context.providerInputConfig,
ProviderLock: &w.providerLock,
ProvisionerCache: w.provisionerCache,
@ -151,7 +150,7 @@ func (w *ContextGraphWalker) ExitEvalTree(
func (w *ContextGraphWalker) init() {
w.contexts = make(map[string]*BuiltinEvalContext, 5)
w.providerCache = make(map[string]ResourceProvider, 5)
w.providerConfigCache = make(map[string]*ResourceConfig, 5)
//w.providerConfigCache = make(map[string]*ResourceConfig, 5)
w.provisionerCache = make(map[string]ResourceProvisioner, 5)
w.interpolaterVars = make(map[string]map[string]interface{}, 5)
}

View File

@ -73,7 +73,8 @@ func TestModuleTreeDependencies(t *testing.T) {
Providers: moduledeps.Providers{
"foo": moduledeps.ProviderDependency{
Constraints: discovery.AllVersions,
Reason: moduledeps.ProviderDependencyImplicit,
//Reason: moduledeps.ProviderDependencyImplicit,
Reason: moduledeps.ProviderDependencyExplicit,
},
"foo.baz": moduledeps.ProviderDependency{
Constraints: discovery.AllVersions,
@ -118,11 +119,13 @@ func TestModuleTreeDependencies(t *testing.T) {
Providers: moduledeps.Providers{
"foo": moduledeps.ProviderDependency{
Constraints: discovery.AllVersions,
Reason: moduledeps.ProviderDependencyInherited,
//Reason: moduledeps.ProviderDependencyInherited,
Reason: moduledeps.ProviderDependencyExplicit,
},
"baz": moduledeps.ProviderDependency{
Constraints: discovery.AllVersions,
Reason: moduledeps.ProviderDependencyImplicit,
//Reason: moduledeps.ProviderDependencyImplicit,
Reason: moduledeps.ProviderDependencyExplicit,
},
},
Children: []*moduledeps.Module{
@ -135,7 +138,8 @@ func TestModuleTreeDependencies(t *testing.T) {
},
"bar": moduledeps.ProviderDependency{
Constraints: discovery.AllVersions,
Reason: moduledeps.ProviderDependencyInherited,
//Reason: moduledeps.ProviderDependencyInherited,
Reason: moduledeps.ProviderDependencyExplicit,
},
},
},

View File

@ -60,12 +60,12 @@ func (n *NodeAbstractProvider) ProviderName() string {
}
// GraphNodeProvider
func (n *NodeAbstractProvider) ProviderConfig() *config.RawConfig {
func (n *NodeAbstractProvider) ProviderConfig() *config.ProviderConfig {
if n.Config == nil {
return nil
}
return n.Config.RawConfig
return n.Config
}
// GraphNodeAttachProvider

View File

@ -20,7 +20,7 @@ func (n *NodeDisabledProvider) EvalTree() EvalNode {
var resourceConfig *ResourceConfig
return &EvalSequence{
Nodes: []EvalNode{
&EvalInterpolate{
&EvalInterpolateProvider{
Config: n.ProviderConfig(),
Output: &resourceConfig,
},
@ -29,10 +29,6 @@ func (n *NodeDisabledProvider) EvalTree() EvalNode {
Config: &resourceConfig,
Output: &resourceConfig,
},
&EvalSetProviderConfig{
Provider: n.ProviderName(),
Config: &resourceConfig,
},
},
}
}

View File

@ -1,24 +1,10 @@
package terraform
import (
"crypto/md5"
"encoding/hex"
"strings"
)
// PathCacheKey returns a cache key for a module path.
//
// TODO: test
func PathCacheKey(path []string) string {
// There is probably a better way to do this, but this is working for now.
// We just create an MD5 hash of all the MD5 hashes of all the path
// elements. This gets us the property that it is unique per ordering.
hash := md5.New()
for _, p := range path {
single := md5.Sum([]byte(p))
if _, err := hash.Write(single[:]); err != nil {
panic(err)
}
}
return hex.EncodeToString(hash.Sum(nil))
return strings.Join(path, "|")
}

View File

@ -1,5 +0,0 @@
provider "aws" {
foo = "bar"
}
module "child" { source = "./child" }

View File

@ -0,0 +1,2 @@
# Empty
provider "aws" {}

View File

@ -0,0 +1,10 @@
provider "aws" {
foo = "bar"
}
module "child" {
source = "./child"
providers {
"aws" = "aws"
}
}