Skip to content

Commit

Permalink
terraform: Expression evaluator can deal with ephemeral resource refs
Browse files Browse the repository at this point in the history
Ephemeral resources work quite differently than managed or data resources
in that their instances live only in memory and are never persisted, and
in that we need to handle the possibility of the object having become
invalid by the time we're evaluating a reference expression.

Since we're just prototyping ephemeral resources for now, this works as
a totally separate codepath in the evaluator. The resource reference
handling in the evaluator is long overdue for being reworked so that it
doesn't depend so directly on the implementation details of how we
keep track of resources, and the new ephemeral codepath is perhaps a
simplified example of what that might look like in future, but for now it's
used only for ephemeral resources to limit the invasiveness of this
prototype.
  • Loading branch information
apparentlymart committed May 6, 2024
1 parent 5377e5e commit cd5191b
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 10 deletions.
107 changes: 107 additions & 0 deletions internal/terraform/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/hashicorp/terraform/internal/namedvals"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/deferring"
"github.com/hashicorp/terraform/internal/resources/ephemeral"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
Expand All @@ -48,6 +49,10 @@ type Evaluator struct {
// we're evaluating expressions that refer to it.
Instances *instances.Expander

// EphemeralResources tracks the currently-open instances of any ephemeral
// resources.
EphemeralResources *ephemeral.Resources

// NamedValues is where we keep the values of already-evaluated input
// variables, local values, and output values.
NamedValues *namedvals.State
Expand Down Expand Up @@ -629,6 +634,19 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
}
ty := schema.ImpliedType()

if addr.Mode == addrs.EphemeralResourceMode {
// FIXME: Ephemeral resources need very different handling. For
// prototype purposes we just branch off into an entirely separate
// codepath here, but in a real implementation it would be nice
// to find some way to refactor this so that the following code
// is not so tethered to the current implementation details and
// instead has a more abstract idea of first determining what
// instances the resource has (using d.Evaluator.Instances.ResourceInstanceKeys)
// and then retrieving the value for each instance to assemble into the
// result, using some per-resource-mode logic maintained elsewhere.
return d.getEphemeralResource(addr, rng, schema, config)
}

rs := d.Evaluator.State.Resource(addr.Absolute(d.ModulePath))

if rs == nil {
Expand Down Expand Up @@ -837,6 +855,95 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
return ret, diags
}

func (d *evaluationStateData) getEphemeralResource(addr addrs.Resource, rng tfdiags.SourceRange, schema *configschema.Block, config *configs.Resource) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

if d.Operation == walkValidate {
// Ephemeral instances are never live during the validate walk
return cty.DynamicVal.Mark(marks.Ephemeral), diags
}

absAddr := addr.Absolute(d.ModulePath)
keyType, keys, haveUnknownKeys := d.Evaluator.Instances.ResourceInstanceKeys(absAddr)
if haveUnknownKeys {
// We can probably do better than totally unknown at least for a
// single-instance resource, but we'll just keep it simple for now.
// Result must be marked as ephemeral so that we can still catch
// attempts to use the results in non-ephemeral locations, so that
// the operator doesn't end up trapped with an error on a subsequent
// plan/apply round.
return cty.DynamicVal.Mark(marks.Ephemeral), diags
}

ephems := d.Evaluator.EphemeralResources
getInstValue := func(addr addrs.AbsResourceInstance) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
val, isLive := ephems.InstanceValue(addr)
if !isLive {
// If the instance is no longer "live" by the time we're accessing
// it then that suggests that it needed renewal and renewal has
// failed, and so the object's value is no longer usable. We'll
// still return the value in case it's somehow useful for diagnosis,
// but we return an error to prevent further evaluation of whatever
// other expression depended on the liveness of this object.
//
// This error message is written on the assumption that it will
// always appear alongside the provider's renewal error, but that'll
// be exposed only once the (now-zombied) ephemeral resource is
// eventually closed, so that we can avoid returning the same error
// multiple times.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral resource instance has expired",
Detail: fmt.Sprintf(
"The remote object for %s is no longer available due to a renewal error, so Terraform cannot evaluate this expression.",
addr,
),
Subject: rng.ToHCL().Ptr(),
})
}
if val == cty.NilVal {
val = cty.DynamicVal.Mark(marks.Ephemeral)
}
return val, diags
}

switch keyType {
case addrs.NoKeyType:
// For "no key" we're returning just a single object representing
// the single instance of this resource.
instVal, moreDiags := getInstValue(absAddr.Instance(addrs.NoKey))
diags = diags.Append(moreDiags)
return instVal, diags
case addrs.IntKeyType:
// For integer keys we're returning a tuple-typed value whose
// indices are the keys.
elems := make([]cty.Value, len(keys))
for _, key := range keys {
idx := int(key.(addrs.IntKey))
instAddr := absAddr.Instance(key)
instVal, moreDiags := getInstValue(instAddr)
diags = diags.Append(moreDiags)
elems[idx] = instVal
}
return cty.TupleVal(elems), diags
case addrs.StringKeyType:
// For string keys we're returning an object-typed value whose
// attributes are the keys.
attrs := make(map[string]cty.Value, len(keys))
for _, key := range keys {
attrName := string(key.(addrs.StringKey))
instAddr := absAddr.Instance(key)
instVal, moreDiags := getInstValue(instAddr)
diags = diags.Append(moreDiags)
attrs[attrName] = instVal
}
return cty.ObjectVal(attrs), diags
default:
panic(fmt.Sprintf("unhandled instance key type %#v", keyType))
}
}

func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.Provider) *configschema.Block {
schema, _, err := d.Evaluator.Plugins.ResourceTypeSchema(providerAddr, addr.Mode, addr.Type)
if err != nil {
Expand Down
21 changes: 11 additions & 10 deletions internal/terraform/graph_walk_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,17 @@ func (w *ContextGraphWalker) EvalContext() EvalContext {
// so that we can safely run multiple evaluations at once across
// different modules.
evaluator := &Evaluator{
Meta: w.Context.meta,
Config: w.Config,
Operation: w.Operation,
State: w.State,
Changes: w.Changes,
Plugins: w.Context.plugins,
Instances: w.InstanceExpander,
NamedValues: w.NamedValues,
Deferrals: w.Deferrals,
PlanTimestamp: w.PlanTimestamp,
Meta: w.Context.meta,
Config: w.Config,
Operation: w.Operation,
State: w.State,
Changes: w.Changes,
Plugins: w.Context.plugins,
Instances: w.InstanceExpander,
EphemeralResources: w.EphemeralResources,
NamedValues: w.NamedValues,
Deferrals: w.Deferrals,
PlanTimestamp: w.PlanTimestamp,
}

ctx := &BuiltinEvalContext{
Expand Down

0 comments on commit cd5191b

Please sign in to comment.