Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] Ephemeral Values prototype #35077

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3ec4e9b
providers: Allow providers to "defer" certain requests
apparentlymart Feb 9, 2023
b5296a4
addrs: Deferrable address types
apparentlymart Feb 9, 2023
0c595bf
plans/deferring: Some helpers to track deferred actions
apparentlymart Mar 1, 2023
c7d76f0
core: Register unknown count and for_each with instance expander
apparentlymart Mar 9, 2023
f7f210e
addrs: Getting the parent of a PartialExpandedModule
apparentlymart Mar 9, 2023
40d525f
terraform: Initial implementation of partial-expanded input variables
apparentlymart Mar 9, 2023
b000ef5
core: Graph walk is aware of partial-expanded module paths
apparentlymart Mar 9, 2023
cd4a221
core: use namedvals.State to track input variable evaluation
apparentlymart Mar 9, 2023
52bafe3
core: Remove variable-specific methods from EvalContext
apparentlymart Mar 9, 2023
60fdd21
core: Start of integrating "partial eval" mode into the evaluator
apparentlymart Mar 9, 2023
36307c2
states: Local values no longer live in state
apparentlymart Mar 9, 2023
73a82df
core: Evaluate placeholders for local values in unexpanded modules
apparentlymart Mar 10, 2023
33e909e
states: Only track root module output values
apparentlymart Mar 10, 2023
c1593dc
core: The expression evaluator has access to the instances.Expander
apparentlymart Mar 10, 2023
0d994a5
addrs: InstanceKeyType.String method
apparentlymart Mar 11, 2023
6f577d2
core: Expression evaluator can handle partial-eval in GetModule
apparentlymart Mar 11, 2023
14f54c5
core: Beginnings of placeholders for resources with unknown expansion
apparentlymart Mar 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
534 changes: 534 additions & 0 deletions docs/plugin-protocol/tfplugin6.5.proto

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions internal/addrs/deferrable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package addrs

import (
"strings"
)

// Deferable represents addresses of types of objects that can have actions
// that might be deferred for execution in a later run.
//
// When an action is defered we still need to describe an approximation of
// the effects of that action so that users can get early feedback about
// whether applying the non-deferred changes will move them closer to their
// desired state. Deferable addresses are how we communicate which object
// each deferred action relates to.
type Deferrable interface {
deferrableSigil()
UniqueKeyer

// DeferrableString is like String but returns an address form specifically
// tailored for a UI that is describing deferred changes, which clearly
// distinguishes between the different possible deferable address types.
DeferrableString() string
}

// ConfigResource is deferable because sometimes we must defer even the
// expansion of a resource due to either its own repetition argument or that
// of one of its containing modules being unknown.
func (ConfigResource) deferrableSigil() {}

func (r ConfigResource) DeferrableString() string {
// Because deferred unexpanded resources will be shown in the same context
// as expanded resource instances, we'll use a special format here to
// make it explicit that we're talking about all instances of a particular
// resource or module, rather than the _unkeyed instance_ of each.
// This follows a similar convention to how we display "move endpoints"
// in the UI, like [MoveEndpointInModule.String]:
// module.foo[*].aws_instance.bar[*], to differentiate from
// module.foo.aws_instance.bar the single instance.
var buf strings.Builder
for _, name := range r.Module {
buf.WriteString("module.")
buf.WriteString(name)
buf.WriteString("[*].")
}
buf.WriteString(r.Resource.String())
buf.WriteString("[*]")
return buf.String()
}

var _ Deferrable = ConfigResource{}

// AbsResourceInstance is deferable for situations where we have already
// succeeded in expanding a resource but one of its instances must be
// defered either for provider-specific reasons or because it is downstream
// of some other deferred action.
func (AbsResourceInstance) deferrableSigil() {}

var _ Deferrable = AbsResourceInstance{}

func (r AbsResourceInstance) DeferrableString() string {
// Our "deferrable" string format for AbsResourceInstance is just its
// normal string format, because this is the main case.
return r.String()
}
88 changes: 88 additions & 0 deletions internal/addrs/deferrable_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package addrs

import (
"fmt"
"testing"
)

func TestDeferrableString(t *testing.T) {
tests := []struct {
Addr Deferrable
Want string
}{
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.Instance(NoKey).Absolute(RootModuleInstance),
`foo.bar`,
},
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.Instance(IntKey(2)).Absolute(RootModuleInstance),
`foo.bar[2]`,
},
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.Instance(StringKey("blub")).Absolute(RootModuleInstance),
`foo.bar["blub"]`,
},
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.Instance(NoKey).Absolute(RootModuleInstance.Child("boop", NoKey)),
`module.boop.foo.bar`,
},
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.Instance(NoKey).Absolute(RootModuleInstance.Child("boop", IntKey(6))),
`module.boop[6].foo.bar`,
},
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.Instance(NoKey).Absolute(RootModuleInstance.Child("boop", StringKey("a"))),
`module.boop["a"].foo.bar`,
},
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.InModule(RootModule),
`foo.bar[*]`,
},
{
Resource{
Mode: ManagedResourceMode,
Type: "foo",
Name: "bar",
}.InModule(RootModule.Child("boop")),
`module.boop[*].foo.bar[*]`,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Addr), func(t *testing.T) {
got := test.Addr.DeferrableString()

if got != test.Want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.Want)
}
})
}
}
2 changes: 2 additions & 0 deletions internal/addrs/instance_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ func instanceKeyType(k InstanceKey) InstanceKeyType {
// of those types.
type InstanceKeyType rune

//go:generate go run golang.org/x/tools/cmd/stringer -type=InstanceKeyType instance_key.go

const (
NoKeyType InstanceKeyType = 0
IntKeyType InstanceKeyType = 'I'
Expand Down
37 changes: 37 additions & 0 deletions internal/addrs/instancekeytype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions internal/addrs/module_scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package addrs

// ModuleEvalScope represents the different kinds of module scope that
// Terraform Core can evaluate expressions within.
//
// Its only two implementations are [ModuleInstance] and
// [PartialExpandedModule], where the latter represents the evaluation context
// for the "partial evaluation" mode which produces placeholder values for
// not-yet-expanded modules.
//
// A nil ModuleEvalScope represents no evaluation scope at all, whereas a
// typed ModuleEvalScope represents either an exact expanded module or a
// partial-expanded module.
type ModuleEvalScope interface {
moduleEvalScopeSigil()
}

func (ModuleInstance) moduleEvalScopeSigil() {}

func (PartialExpandedModule) moduleEvalScopeSigil() {}
90 changes: 90 additions & 0 deletions internal/addrs/partial_expanded.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,57 @@ func (pem PartialExpandedModule) Resource(resource Resource) PartialExpandedReso
}
}

// ParentIsModuleInstance returns true if
// [PartialExpandedModule.ParentModuleInstance] would succeed, or false if
// [PartialExpandedModule.ParentPartialExpandedModule] would succeed.
//
// In other words, it determines whether accessing the parent module would
// cross the boundary between the unexpanded and expanded portions of the
// address, which means that any further traversal upwards should be done
// using [ModuleInstance] values rather than [PartialExpandedModule]
// values.
func (pem PartialExpandedModule) ParentIsModuleInstance() bool {
// NOTE: We don't handle the case where unexpandedSuffix is zero-length
// here because standalone PartialExpandedModule values should always have
// at least one unexpanded part.
//
// This isn't true for the special PartialExpandedModule values hidden in
// the internals of PartialExpandedResource, so don't use this method with
// those.
return len(pem.unexpandedSuffix) == 1
}

// ParentModuleInstance returns the fully-expanded module instance that is the
// parent of this module if and only if the last step is the only step in the
// path that is unexpanded.
//
// If the receiever does not meet that criteria, the second return value is
// false. Use ParentPartialExpandedModule instead in that case, to get the
// parent represented as a [PartialExpandedModule].
func (pem PartialExpandedModule) ParentModuleInstance() (ModuleInstance, bool) {
if len(pem.unexpandedSuffix) != 1 {
return nil, false
}
return pem.expandedPrefix, true
}

// ParentPartialExpandedModule is like ParentModuleInstance but deals with the
// situation where the parent is also partially-expanded and so needs to still
// be described as a PartialExpandedModule.
//
// If the reciever's parent is already exact then the second return value is
// false. Use ParentModuleInstance instead in that case, to get the parent
// represented as a [ModuleInstance].
func (pem PartialExpandedModule) ParentPartialExpandedModule() (PartialExpandedModule, bool) {
if len(pem.unexpandedSuffix) < 2 {
return PartialExpandedModule{}, false
}
return PartialExpandedModule{
expandedPrefix: pem.expandedPrefix,
unexpandedSuffix: pem.unexpandedSuffix[: len(pem.unexpandedSuffix)-1 : len(pem.unexpandedSuffix)-1],
}, true
}

// String returns a string representation of the pattern where the known
// prefix uses the normal module instance address syntax and the unknown
// suffix steps use a similar syntax but with "[*]" as a placeholder to
Expand Down Expand Up @@ -295,6 +346,45 @@ func (per PartialExpandedResource) AbsResource() (AbsResource, bool) {
}, true
}

// FullyExpandedModuleInstance returns the [ModuleInstance] that the receiver
// belongs to if and only if it belongs to a fully-known module path.
//
// The second return value is true only if the first return value is valid.
func (per PartialExpandedResource) FullyExpandedModuleInstance() (ModuleInstance, bool) {
if len(per.module.unexpandedSuffix) != 0 {
return nil, false
}
return per.module.expandedPrefix, true
}

// PartialExpandedModuleInstance returns the [PartialExpandedModule] that the
// receiver belongs to if and only if the module path is not fully known.
// For a fully-known path use [PartialExpandedResource.FullyExpandedModuleInstance]
// instead, to obtain a [ModuleInstance]
func (per PartialExpandedResource) PartialExpandedModuleInstance() (PartialExpandedModule, bool) {
// We can only reveal our module field's value if it has at least one
// expanded element, because otherwise it will violate the assumptions
// made in the exported API of PartialExpandedModule.
if len(per.module.unexpandedSuffix) == 0 {
return PartialExpandedModule{}, false
}
return per.module, true
}

// ModuleEvalScope returns the [ModuleEvalScope] that the receiver should
// have its expressions evaluated in.
func (per PartialExpandedResource) ModuleEvalScope() ModuleEvalScope {
if addr, ok := per.FullyExpandedModuleInstance(); ok {
return addr
} else if addr, ok := per.PartialExpandedModuleInstance(); ok {
return addr
} else {
// Should never get here because we should always have exactly one
// of the two address types above.
panic("unexpected ModuleEvalScope type for PartialExpandedResource")
}
}

// ConfigResource returns the unexpanded resource address that this
// partially-expanded resource address originates from.
func (per PartialExpandedResource) ConfigResource() ConfigResource {
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/remote/backend_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (r *remoteClient) Put(state []byte) error {
return fmt.Errorf("error reading state: %s", err)
}

ov, err := jsonstate.MarshalOutputs(stateFile.State.RootModule().OutputValues)
ov, err := jsonstate.MarshalOutputs(stateFile.State.RootOutputValues)
if err != nil {
return fmt.Errorf("error reading output values: %s", err)
}
Expand Down
7 changes: 2 additions & 5 deletions internal/builtin/providers/terraform/data_source_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,8 @@ func dataSourceRemoteStateRead(d cty.Value) (cty.Value, tfdiags.Diagnostics) {
newState["outputs"] = cty.EmptyObjectVal
return cty.ObjectVal(newState), diags
}
mod := remoteState.RootModule()
if mod != nil { // should always have a root module in any valid state
for k, os := range mod.OutputValues {
outputs[k] = os.Value
}
for k, os := range remoteState.RootOutputValues {
outputs[k] = os.Value
}

newState["outputs"] = cty.ObjectVal(outputs)
Expand Down
4 changes: 2 additions & 2 deletions internal/cloud/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func (s *State) PersistState(schemas *terraform.Schemas) error {
return fmt.Errorf("failed to read state: %w", err)
}

ov, err := jsonstate.MarshalOutputs(stateFile.State.RootModule().OutputValues)
ov, err := jsonstate.MarshalOutputs(stateFile.State.RootOutputValues)
if err != nil {
return fmt.Errorf("failed to translate outputs: %w", err)
}
Expand Down Expand Up @@ -547,7 +547,7 @@ func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) {
return nil, ErrStateVersionUnauthorizedUpgradeState
}

return state.RootModule().OutputValues, nil
return state.RootOutputValues, nil
}

if output.Sensitive {
Expand Down
2 changes: 1 addition & 1 deletion internal/command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int {
if rb, isRemoteBackend := be.(BackendWithRemoteTerraformVersion); !isRemoteBackend || rb.IsLocalOperations() {
view.ResourceCount(args.State.StateOutPath)
if !c.Destroy && op.State != nil {
view.Outputs(op.State.RootModule().OutputValues)
view.Outputs(op.State.RootOutputValues)
}
}

Expand Down
7 changes: 5 additions & 2 deletions internal/command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,15 +355,18 @@ func testStateMgrCurrentLineage(mgr statemgr.Persistent) string {
// // (do stuff to the state)
// assertStateHasMarker(state, mark)
func markStateForMatching(state *states.State, mark string) string {
state.RootModule().SetOutputValue("testing_mark", cty.StringVal(mark), false)
state.SetOutputValue(
addrs.OutputValue{Name: "testing_mark"}.Absolute(addrs.RootModuleInstance),
cty.StringVal(mark), false,
)
return mark
}

// getStateMatchingMarker is used with markStateForMatching to retrieve the
// mark string previously added to the given state. If no such mark is present,
// the result is an empty string.
func getStateMatchingMarker(state *states.State) string {
os := state.RootModule().OutputValues["testing_mark"]
os := state.RootOutputValues["testing_mark"]
if os == nil {
return ""
}
Expand Down
Loading