2016-11-14 07:04:21 +01:00
|
|
|
package repl
|
|
|
|
|
|
|
|
import (
|
2018-10-03 02:02:48 +02:00
|
|
|
"flag"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
2016-11-14 07:04:21 +01:00
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
2018-10-03 02:21:50 +02:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
|
2018-09-29 02:15:35 +02:00
|
|
|
"github.com/hashicorp/terraform/addrs"
|
2018-10-03 02:21:50 +02:00
|
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
2018-10-03 02:02:48 +02:00
|
|
|
"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"
|
2018-09-29 02:15:35 +02:00
|
|
|
"github.com/hashicorp/terraform/providers"
|
|
|
|
"github.com/hashicorp/terraform/states"
|
2016-11-14 07:04:21 +01:00
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
|
|
)
|
|
|
|
|
2018-10-03 02:02:48 +02:00
|
|
|
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) {
|
2018-10-03 02:02:48 +02:00
|
|
|
state := states.BuildState(func(s *states.SyncState) {
|
2018-09-29 02:15:35 +02:00
|
|
|
s.SetResourceInstanceCurrent(
|
|
|
|
addrs.Resource{
|
|
|
|
Mode: addrs.ManagedResourceMode,
|
|
|
|
Type: "test_instance",
|
|
|
|
Name: "foo",
|
|
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
|
|
&states.ResourceInstanceObjectSrc{
|
2018-10-03 02:02:48 +02:00
|
|
|
Status: states.ObjectReady,
|
2018-09-29 02:15:35 +02:00
|
|
|
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",
|
2018-09-29 02:15:35 +02:00
|
|
|
}.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{
|
2018-10-03 02:02:48 +02:00
|
|
|
Status: states.ObjectReady,
|
2018-09-29 02:15:35 +02:00
|
|
|
AttrsJSON: []byte(`{"id":"bar"}`),
|
2016-11-14 07:17:51 +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",
|
2018-09-29 02:15:35 +02:00
|
|
|
}.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,
|
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
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
2016-11-14 07:17:51 +01:00
|
|
|
|
|
|
|
t.Run("missing module", func(t *testing.T) {
|
2018-11-28 20:25:44 +01:00
|
|
|
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) {
|
2016-11-14 07:17:51 +01:00
|
|
|
testSession(t, testSessionTest{
|
|
|
|
State: state,
|
|
|
|
Inputs: []testSessionInput{
|
|
|
|
{
|
|
|
|
Input: "module.child.foo",
|
|
|
|
Error: true,
|
2018-11-28 20:25:44 +01:00
|
|
|
ErrorContains: `No module call named "child" is declared in the root module.`,
|
2016-11-14 07:17:51 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("missing module output", func(t *testing.T) {
|
|
|
|
testSession(t, testSessionTest{
|
|
|
|
State: state,
|
|
|
|
Inputs: []testSessionInput{
|
|
|
|
{
|
|
|
|
Input: "module.module.foo",
|
|
|
|
Error: true,
|
2018-10-03 02:21:50 +02:00
|
|
|
ErrorContains: `An output value with the name "foo" has not been declared in module.module`,
|
2016-11-14 07:17:51 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
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{
|
|
|
|
{
|
2018-09-29 02:15:35 +02:00
|
|
|
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,
|
2018-09-29 02:15:35 +02:00
|
|
|
ErrorContains: `resource "test_instance" "bar" has not been declared`,
|
2016-11-14 07:04:21 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func testSession(t *testing.T, test testSessionTest) {
|
2018-11-21 02:25:05 +01:00
|
|
|
t.Helper()
|
|
|
|
|
2018-09-29 02:15:35 +02:00
|
|
|
p := &terraform.MockProvider{}
|
2018-10-03 02:21:50 +02:00
|
|
|
p.GetSchemaReturn = &terraform.ProviderSchema{
|
|
|
|
ResourceTypes: map[string]*configschema.Block{
|
|
|
|
"test_instance": {
|
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"id": {Type: cty.String, Computed: true},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2019-01-09 03:39:14 +01:00
|
|
|
config, _, cleanup, configDiags := initwd.LoadConfigForTests(t, "testdata/config-fixture")
|
2018-10-03 02:21:50 +02:00
|
|
|
defer cleanup()
|
|
|
|
if configDiags.HasErrors() {
|
2019-01-09 03:39:14 +01:00
|
|
|
t.Fatalf("unexpected problems loading config: %s", configDiags.Err())
|
2018-10-03 02:21:50 +02:00
|
|
|
}
|
2018-09-29 02:15:35 +02:00
|
|
|
|
2016-11-14 07:04:21 +01:00
|
|
|
// Build the TF context
|
2018-09-29 02:15:35 +02:00
|
|
|
ctx, diags := terraform.NewContext(&terraform.ContextOpts{
|
2018-10-03 02:02:48 +02:00
|
|
|
State: test.State,
|
2019-12-04 17:30:20 +01:00
|
|
|
ProviderResolver: providers.ResolverFixed(map[addrs.Provider]providers.Factory{
|
|
|
|
addrs.NewLegacyProvider("test"): providers.FactoryFixed(p),
|
2018-09-29 02:15:35 +02:00
|
|
|
}),
|
2018-10-03 02:21:50 +02:00
|
|
|
Config: config,
|
2016-11-14 07:04:21 +01:00
|
|
|
})
|
2018-09-29 02:15:35 +02: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{
|
2018-09-29 02:15:35 +02:00
|
|
|
Scope: scope,
|
2016-11-14 07:04:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Test the inputs. We purposely don't use subtests here because
|
2018-09-29 02:15:35 +02:00
|
|
|
// 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 {
|
2018-09-29 02:15:35 +02:00
|
|
|
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
|
|
|
}
|
2018-09-29 02:15:35 +02:00
|
|
|
if diags.HasErrors() {
|
2016-11-14 07:04:21 +01:00
|
|
|
if input.ErrorContains != "" {
|
2018-09-29 02:15:35 +02:00
|
|
|
if !strings.Contains(diags.Err().Error(), input.ErrorContains) {
|
2016-11-14 07:04:21 +01:00
|
|
|
t.Fatalf(
|
2018-09-29 02:15:35 +02:00
|
|
|
"%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 {
|
2018-09-29 02:15:35 +02:00
|
|
|
State *states.State // State to use
|
2019-06-30 09:38:36 +02:00
|
|
|
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
|
2018-09-29 02:15:35 +02:00
|
|
|
Exit bool // Exit is true if exiting is expected
|
2016-11-14 07:04:21 +01:00
|
|
|
ErrorContains string
|
|
|
|
}
|