terraform/repl/session_test.go

289 lines
6.9 KiB
Go
Raw Normal View History

2016-11-14 07:04:21 +01:00
package repl
import (
"flag"
"io/ioutil"
"log"
"os"
2016-11-14 07:04:21 +01:00
"strings"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/logging"
command: "terraform init" can partially initialize for 0.12upgrade There are a few constructs from 0.11 and prior that cause 0.12 parsing to fail altogether, which previously created a chicken/egg problem because we need to install the providers in order to run "terraform 0.12upgrade" and thus fix the problem. This changes "terraform init" to use the new "early configuration" loader for module and provider installation. This is built on the more permissive parser in the terraform-config-inspect package, and so it allows us to read out the top-level blocks from the configuration while accepting legacy HCL syntax. In the long run this will let us do version compatibility detection before attempting a "real" config load, giving us better error messages for any future syntax additions, but in the short term the key thing is that it allows us to install the dependencies even if the configuration isn't fully valid. Because backend init still requires full configuration, this introduces a new mode of terraform init where it detects heuristically if it seems like we need to do a configuration upgrade and does a partial init if so, before finally directing the user to run "terraform 0.12upgrade" before running any other commands. The heuristic here is based on two assumptions: - If the "early" loader finds no errors but the normal loader does, the configuration is likely to be valid for Terraform 0.11 but not 0.12. - If there's already a version constraint in the configuration that excludes Terraform versions prior to v0.12 then the configuration is probably _already_ upgraded and so it's just a normal syntax error, even if the early loader didn't detect it. Once the upgrade process is removed in 0.13.0 (users will be required to go stepwise 0.11 -> 0.12 -> 0.13 to upgrade after that), some of this can be simplified to remove that special mode, but the idea of doing the dependency version checks against the liberal parser will remain valuable to increase our chances of reporting version-based incompatibilities rather than syntax errors as we add new features in future.
2019-01-14 20:11:00 +01:00
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
2016-11-14 07:04:21 +01:00
"github.com/hashicorp/terraform/terraform"
)
func TestMain(m *testing.M) {
flag.Parse()
if testing.Verbose() {
// if we're verbose, use the logging requested by TF_LOG
logging.SetOutput()
} else {
// otherwise silence all logs
log.SetOutput(ioutil.Discard)
}
os.Exit(m.Run())
}
2016-11-14 07:04:21 +01:00
func TestSession_basicState(t *testing.T) {
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
2016-11-14 07:04:21 +01:00
},
Initial steps towards AbsProviderConfig/LocalProviderConfig separation (#23978) * Introduce "Local" terminology for non-absolute provider config addresses In a future change AbsProviderConfig and LocalProviderConfig are going to become two entirely distinct types, rather than Abs embedding Local as written here. This naming change is in preparation for that subsequent work, which will also include introducing a new "ProviderConfig" type that is an interface that AbsProviderConfig and LocalProviderConfig both implement. This is intended to be largely just a naming change to get started, so we can deal with all of the messy renaming. However, this did also require a slight change in modeling where the Resource.DefaultProviderConfig method has become Resource.DefaultProvider returning a Provider address directly, because this method doesn't have enough information to construct a true and accurate LocalProviderConfig -- it would need to refer to the configuration to know what this module is calling the provider it has selected. In order to leave a trail to follow for subsequent work, all of the changes here are intended to ensure that remaining work will become obvious via compile-time errors when all of the following changes happen: - The concept of "legacy" provider addresses is removed from the addrs package, including removing addrs.NewLegacyProvider and addrs.Provider.LegacyString. - addrs.AbsProviderConfig stops having addrs.LocalProviderConfig embedded in it and has an addrs.Provider and a string alias directly instead. - The provider-schema-handling parts of Terraform core are updated to work with addrs.Provider to identify providers, rather than legacy strings. In particular, there are still several codepaths here making legacy provider address assumptions (in order to limit the scope of this change) but I've made sure each one is doing something that relies on at least one of the above changes not having been made yet. * addrs: ProviderConfig interface In a (very) few special situations in the main "terraform" package we need to make runtime decisions about whether a provider config is absolute or local. We currently do that by exploiting the fact that AbsProviderConfig has LocalProviderConfig nested inside of it and so in the local case we can just ignore the wrapping AbsProviderConfig and use the embedded value. In a future change we'll be moving away from that embedding and making these two types distinct in order to represent that mapping between them requires consulting a lookup table in the configuration, and so here we introduce a new interface type ProviderConfig that can represent either AbsProviderConfig or LocalProviderConfig decided dynamically at runtime. This also includes the Config.ResolveAbsProviderAddr method that will eventually be responsible for that local-to-absolute translation, so that callers with access to the configuration can normalize to an addrs.AbsProviderConfig given a non-nil addrs.ProviderConfig. That's currently unused because existing callers are still relying on the simplistic structural transform, but we'll switch them over in a later commit. * rename LocalType to LocalName Co-authored-by: Kristin Laemmert <mildwonkey@users.noreply.github.com>
2020-01-31 14:23:07 +01:00
addrs.LocalProviderConfig{
LocalName: "test",
}.Absolute(addrs.RootModuleInstance),
)
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("module", addrs.NoKey)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
Initial steps towards AbsProviderConfig/LocalProviderConfig separation (#23978) * Introduce "Local" terminology for non-absolute provider config addresses In a future change AbsProviderConfig and LocalProviderConfig are going to become two entirely distinct types, rather than Abs embedding Local as written here. This naming change is in preparation for that subsequent work, which will also include introducing a new "ProviderConfig" type that is an interface that AbsProviderConfig and LocalProviderConfig both implement. This is intended to be largely just a naming change to get started, so we can deal with all of the messy renaming. However, this did also require a slight change in modeling where the Resource.DefaultProviderConfig method has become Resource.DefaultProvider returning a Provider address directly, because this method doesn't have enough information to construct a true and accurate LocalProviderConfig -- it would need to refer to the configuration to know what this module is calling the provider it has selected. In order to leave a trail to follow for subsequent work, all of the changes here are intended to ensure that remaining work will become obvious via compile-time errors when all of the following changes happen: - The concept of "legacy" provider addresses is removed from the addrs package, including removing addrs.NewLegacyProvider and addrs.Provider.LegacyString. - addrs.AbsProviderConfig stops having addrs.LocalProviderConfig embedded in it and has an addrs.Provider and a string alias directly instead. - The provider-schema-handling parts of Terraform core are updated to work with addrs.Provider to identify providers, rather than legacy strings. In particular, there are still several codepaths here making legacy provider address assumptions (in order to limit the scope of this change) but I've made sure each one is doing something that relies on at least one of the above changes not having been made yet. * addrs: ProviderConfig interface In a (very) few special situations in the main "terraform" package we need to make runtime decisions about whether a provider config is absolute or local. We currently do that by exploiting the fact that AbsProviderConfig has LocalProviderConfig nested inside of it and so in the local case we can just ignore the wrapping AbsProviderConfig and use the embedded value. In a future change we'll be moving away from that embedding and making these two types distinct in order to represent that mapping between them requires consulting a lookup table in the configuration, and so here we introduce a new interface type ProviderConfig that can represent either AbsProviderConfig or LocalProviderConfig decided dynamically at runtime. This also includes the Config.ResolveAbsProviderAddr method that will eventually be responsible for that local-to-absolute translation, so that callers with access to the configuration can normalize to an addrs.AbsProviderConfig given a non-nil addrs.ProviderConfig. That's currently unused because existing callers are still relying on the simplistic structural transform, but we'll switch them over in a later commit. * rename LocalType to LocalName Co-authored-by: Kristin Laemmert <mildwonkey@users.noreply.github.com>
2020-01-31 14:23:07 +01:00
addrs.LocalProviderConfig{
LocalName: "test",
}.Absolute(addrs.RootModuleInstance),
)
})
2016-11-14 07:04:21 +01:00
t.Run("basic", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "test_instance.foo.id",
Output: "bar",
},
},
})
})
t.Run("missing resource", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "test_instance.bar.id",
Error: true,
core: Static-validate resource references against schemas In the initial move to HCL2 we started relying only on full expression evaluation to catch attribute errors, but that's not sufficient for resource attributes in practice because during validation we can't know yet whether a resource reference evaluates to a single object or to a list of objects (if count is set). To address this, here we reinstate some static validation of resource references by analyzing directly the reference objects, disregarding any instance index if present, and produce errors if the remaining subsequent traversal steps do not correspond to items within the resource type schema. This also allows us to produce some more specialized error messages for certain situations. In particular, we can recognize a reference like aws_instance.foo.count, which in 0.11 and prior was a weird special case for determining the count value of a resource block, and offer a helpful error showing the new length(aws_instance.foo) usage pattern. This eventually delegates to the static traversal validation logic that was added to the configschema package in a previous commit, which also includes some specialized error messages that distinguish between attributes and block types in the schema so that the errors relate more directly to constructs the user can see in the configuration. In future we could potentially move more of the checks from the dynamic schema construction step to the static validation step, but resources are the reference type that most needs this immediately due to the ambiguity caused by the instance indexing syntax. We can safely refactor other reference types to be statically validated in later releases. This is verified by two pre-existing context validate tests which we temporarily disabled during earlier work (now re-enabled) and also by a new validate test aimed specifically at the special case for the "count" attribute.
2018-11-21 02:25:05 +01:00
ErrorContains: `A managed resource "test_instance" "bar" has not been declared`,
2016-11-14 07:04:21 +01:00
},
},
})
})
t.Run("missing module", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "module.child",
Error: true,
ErrorContains: `No module call named "child" is declared in the root module.`,
},
},
})
})
t.Run("missing module referencing just one output", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "module.child.foo",
Error: true,
ErrorContains: `No module call named "child" is declared in the root module.`,
},
},
})
})
t.Run("missing module output", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "module.module.foo",
Error: true,
ErrorContains: `An output value with the name "foo" has not been declared in module.module`,
},
},
})
})
2016-11-14 07:04:21 +01:00
}
func TestSession_stateless(t *testing.T) {
t.Run("exit", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "exit",
Exit: true,
2016-11-14 07:04:21 +01:00
},
},
})
})
t.Run("help", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "help",
OutputContains: "allows you to",
},
},
})
})
t.Run("help with spaces", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "help ",
OutputContains: "allows you to",
},
},
})
})
t.Run("basic math", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "1 + 5",
Output: "6",
},
},
})
})
t.Run("missing resource", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "test_instance.bar.id",
Error: true,
ErrorContains: `resource "test_instance" "bar" has not been declared`,
2016-11-14 07:04:21 +01:00
},
},
})
})
}
func testSession(t *testing.T, test testSessionTest) {
core: Static-validate resource references against schemas In the initial move to HCL2 we started relying only on full expression evaluation to catch attribute errors, but that's not sufficient for resource attributes in practice because during validation we can't know yet whether a resource reference evaluates to a single object or to a list of objects (if count is set). To address this, here we reinstate some static validation of resource references by analyzing directly the reference objects, disregarding any instance index if present, and produce errors if the remaining subsequent traversal steps do not correspond to items within the resource type schema. This also allows us to produce some more specialized error messages for certain situations. In particular, we can recognize a reference like aws_instance.foo.count, which in 0.11 and prior was a weird special case for determining the count value of a resource block, and offer a helpful error showing the new length(aws_instance.foo) usage pattern. This eventually delegates to the static traversal validation logic that was added to the configschema package in a previous commit, which also includes some specialized error messages that distinguish between attributes and block types in the schema so that the errors relate more directly to constructs the user can see in the configuration. In future we could potentially move more of the checks from the dynamic schema construction step to the static validation step, but resources are the reference type that most needs this immediately due to the ambiguity caused by the instance indexing syntax. We can safely refactor other reference types to be statically validated in later releases. This is verified by two pre-existing context validate tests which we temporarily disabled during earlier work (now re-enabled) and also by a new validate test aimed specifically at the special case for the "count" attribute.
2018-11-21 02:25:05 +01:00
t.Helper()
p := &terraform.MockProvider{}
p.GetSchemaReturn = &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
}
config, _, cleanup, configDiags := initwd.LoadConfigForTests(t, "testdata/config-fixture")
defer cleanup()
if configDiags.HasErrors() {
t.Fatalf("unexpected problems loading config: %s", configDiags.Err())
}
2016-11-14 07:04:21 +01:00
// Build the TF context
ctx, diags := terraform.NewContext(&terraform.ContextOpts{
State: test.State,
ProviderResolver: providers.ResolverFixed(map[addrs.Provider]providers.Factory{
addrs.NewLegacyProvider("test"): providers.FactoryFixed(p),
}),
Config: config,
2016-11-14 07:04:21 +01:00
})
if diags.HasErrors() {
t.Fatalf("failed to create context: %s", diags.Err())
}
scope, diags := ctx.Eval(addrs.RootModuleInstance)
if diags.HasErrors() {
t.Fatalf("failed to create scope: %s", diags.Err())
2016-11-14 07:04:21 +01:00
}
// Build the session
s := &Session{
Scope: scope,
2016-11-14 07:04:21 +01:00
}
// Test the inputs. We purposely don't use subtests here because
// the inputs don't represent subtests, but a sequence of stateful
2016-11-14 07:04:21 +01:00
// operations.
for _, input := range test.Inputs {
result, exit, diags := s.Handle(input.Input)
if exit != input.Exit {
t.Fatalf("incorrect 'exit' result %t; want %t", exit, input.Exit)
}
if (diags.HasErrors()) != input.Error {
t.Fatalf("%q: unexpected errors: %s", input.Input, diags.Err())
2016-11-14 07:04:21 +01:00
}
if diags.HasErrors() {
2016-11-14 07:04:21 +01:00
if input.ErrorContains != "" {
if !strings.Contains(diags.Err().Error(), input.ErrorContains) {
2016-11-14 07:04:21 +01:00
t.Fatalf(
"%q: diagnostics should contain: %q\n\n%s",
input.Input, input.ErrorContains, diags.Err(),
)
2016-11-14 07:04:21 +01:00
}
}
continue
}
if input.Output != "" && result != input.Output {
t.Fatalf(
"%q: expected:\n\n%s\n\ngot:\n\n%s",
input.Input, input.Output, result)
}
if input.OutputContains != "" && !strings.Contains(result, input.OutputContains) {
t.Fatalf(
"%q: expected contains:\n\n%s\n\ngot:\n\n%s",
input.Input, input.OutputContains, result)
}
}
}
type testSessionTest struct {
State *states.State // State to use
Module string // Module name in testdata to load
2016-11-14 07:04:21 +01:00
// Inputs are the list of test inputs that are run in order.
// Each input can test the output of each step.
Inputs []testSessionInput
}
// testSessionInput is a single input to test for a session.
type testSessionInput struct {
Input string // Input string
Output string // Exact output string to check
OutputContains string
Error bool // Error is true if error is expected
Exit bool // Exit is true if exiting is expected
2016-11-14 07:04:21 +01:00
ErrorContains string
}