From 0d3530c8157b96df19e47e103419d3894c8a7c08 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 19 Sep 2024 15:29:10 -0400 Subject: [PATCH] core ephemeral node implementation Implement the core nodes for ephemeral resources. This constitutes what's needed for execution, but does not yet include all the necessary graph transformations. --- internal/terraform/node_resource_abstract.go | 25 +-- internal/terraform/node_resource_apply.go | 2 +- internal/terraform/node_resource_ephemeral.go | 205 ++++++++++++++++++ .../terraform/node_resource_partial_plan.go | 2 +- internal/terraform/node_resource_plan.go | 29 ++- .../terraform/node_resource_plan_instance.go | 10 + internal/terraform/node_resource_validate.go | 3 + 7 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 internal/terraform/node_resource_ephemeral.go diff --git a/internal/terraform/node_resource_abstract.go b/internal/terraform/node_resource_abstract.go index b5f940d877..f6950610c8 100644 --- a/internal/terraform/node_resource_abstract.go +++ b/internal/terraform/node_resource_abstract.go @@ -390,16 +390,10 @@ func (n *NodeAbstractResource) DotNode(name string, opts *dag.DotOpts) *dag.DotN } } -// writeResourceState ensures that a suitable resource-level state record is -// present in the state, if that's required for the "each mode" of that -// resource. -// -// This is important primarily for the situation where count = 0, since this -// eval is the only change we get to set the resource "each mode" to list -// in that case, allowing expression evaluation to see it as a zero-element list -// rather than as not set at all. -func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.AbsResource) (diags tfdiags.Diagnostics) { - state := ctx.State() +// recordResourceData records some metadata for the resource as a whole in +// various locations. This currently includes adding resource expansion info to +// the instance expander, and recording the provider used in the state. +func (n *NodeAbstractResource) recordResourceData(ctx EvalContext, addr addrs.AbsResource) (diags tfdiags.Diagnostics) { // We'll record our expansion decision in the shared "expander" object // so that later operations (i.e. DynamicExpand and expression evaluation) @@ -422,7 +416,6 @@ func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.Ab return diags } - state.SetResourceProvider(addr, n.ResolvedProvider) if count >= 0 { expander.SetResourceCount(addr.Module, n.Addr.Resource, count) } else { @@ -439,7 +432,6 @@ func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.Ab // This method takes care of all of the business logic of updating this // while ensuring that any existing instances are preserved, etc. - state.SetResourceProvider(addr, n.ResolvedProvider) if known { expander.SetResourceForEach(addr.Module, n.Addr.Resource, forEach) } else { @@ -447,10 +439,17 @@ func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.Ab } default: - state.SetResourceProvider(addr, n.ResolvedProvider) expander.SetResourceSingle(addr.Module, n.Addr.Resource) } + if addr.Resource.Mode == addrs.EphemeralResourceMode { + // ephemeral resources are not included in the state + return diags + } + + state := ctx.State() + state.SetResourceProvider(addr, n.ResolvedProvider) + return diags } diff --git a/internal/terraform/node_resource_apply.go b/internal/terraform/node_resource_apply.go index ff9492cc55..2a0eed7c6f 100644 --- a/internal/terraform/node_resource_apply.go +++ b/internal/terraform/node_resource_apply.go @@ -54,7 +54,7 @@ func (n *nodeExpandApplyableResource) Execute(globalCtx EvalContext, op walkOper moduleInstances := expander.ExpandModule(n.Addr.Module, false) for _, module := range moduleInstances { moduleCtx := evalContextForModuleInstance(globalCtx, module) - diags = diags.Append(n.writeResourceState(moduleCtx, n.Addr.Resource.Absolute(module))) + diags = diags.Append(n.recordResourceData(moduleCtx, n.Addr.Resource.Absolute(module))) } return diags diff --git a/internal/terraform/node_resource_ephemeral.go b/internal/terraform/node_resource_ephemeral.go new file mode 100644 index 0000000000..3e9bc6394c --- /dev/null +++ b/internal/terraform/node_resource_ephemeral.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "context" + "fmt" + "log" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans/objchange" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/resources/ephemeral" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type ephemeralResourceInput struct { + addr addrs.AbsResourceInstance + config *configs.Resource + providerConfig addrs.AbsProviderConfig +} + +// ephemeralResourceOpen implements the "open" step of the ephemeral resource +// instance lifecycle, which behaves the same way in both the plan and apply +// walks. +func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) tfdiags.Diagnostics { + log.Printf("[TRACE] ephemeralResourceOpen: opening %s", inp.addr) + var diags tfdiags.Diagnostics + + provider, providerSchema, err := getProvider(ctx, inp.providerConfig) + if err != nil { + diags = diags.Append(err) + return diags + } + + config := inp.config + schema, _ := providerSchema.SchemaForResourceAddr(inp.addr.ContainingResource().Resource) + if schema == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append( + fmt.Errorf("provider %q does not support ephemeral resource %q", + inp.providerConfig, inp.addr.ContainingResource().Resource.Type, + ), + ) + return diags + } + + resources := ctx.EphemeralResources() + allInsts := ctx.InstanceExpander() + keyData := allInsts.GetResourceInstanceRepetitionData(inp.addr) + + checkDiags := evalCheckRules( + addrs.ResourcePrecondition, + config.Preconditions, + ctx, inp.addr, keyData, + tfdiags.Error, + ) + diags = diags.Append(checkDiags) + if diags.HasErrors() { + return diags // failed preconditions prevent further evaluation + } + + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) + diags = diags.Append(configDiags) + if diags.HasErrors() { + return diags + } + unmarkedConfigVal, configMarks := configVal.UnmarkDeepWithPaths() + + diags = diags.Append(provider.ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest{ + TypeName: inp.addr.Resource.Resource.Type, + Config: unmarkedConfigVal, + })) + + if diags.HasErrors() { + return diags + } + + resp := provider.OpenEphemeralResource(providers.OpenEphemeralResourceRequest{ + TypeName: inp.addr.ContainingResource().Resource.Type, + Config: unmarkedConfigVal, + }) + if resp.Deferred != nil { + // FIXME: Actually implement this. + diags = diags.Append(fmt.Errorf("we don't support deferral of ephemeral resource instances yet")) + } + diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, inp.addr.String())) + if diags.HasErrors() { + return diags + } + resultVal := resp.Result.MarkWithPaths(configMarks) + + errs := objchange.AssertPlanValid(schema, cty.NullVal(schema.ImpliedType()), configVal, resultVal) + for _, err := range errs { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider produced invalid ephemeral resource instance", + fmt.Sprintf( + "The provider for %s produced an inconsistent result: %s.", + inp.addr.Resource.Resource.Type, + tfdiags.FormatError(err), + ), + nil, + )).InConfigBody(config.Config, inp.addr.String()) + } + if diags.HasErrors() { + return diags + } + + // We are going to wholesale mark the entire resource as ephemeral. This + // simplifies the model as any references to ephemeral resources can be + // considered as such. Any input values that don't need to be ephemeral can + // be referenced directly. + resultVal = resultVal.Mark(marks.Ephemeral) + + impl := &ephemeralResourceInstImpl{ + addr: inp.addr, + provider: provider, + internal: resp.InternalContext, + } + // TODO: What can we use as a signal to cancel the context we're passing in + // here, so that the object will stop renewing things when we start shutting + // down? + // TODO: The context Stopped channel should probably be updated + // finally to a Context + resources.RegisterInstance(context.TODO(), inp.addr, ephemeral.ResourceInstanceRegistration{ + Value: resultVal, + ConfigBody: config.Config, + Impl: impl, + FirstRenewal: resp.Renew, + }) + + return diags +} + +// nodeEphemeralResourceClose is the node type for closing the previously-opened +// instances of a particular ephemeral resource. +// +// Although ephemeral resource instances will always all get closed once a +// graph walk has completed anyway, the inclusion of explicit nodes for this +// allows closing ephemeral resource instances more promptly after all work +// that uses them has been completed, rather than always just waiting until +// the end of the graph walk. +// +// This is scoped to config-level resources rather than dynamic resource +// instances as a concession to allow using the same node type in both the plan +// and apply graphs, where the former only deals in whole resources while the +// latter contains individual instances. +type nodeEphemeralResourceClose struct { + addr addrs.ConfigResource +} + +var _ GraphNodeExecutable = (*nodeEphemeralResourceClose)(nil) +var _ GraphNodeModulePath = (*nodeEphemeralResourceClose)(nil) + +func (n *nodeEphemeralResourceClose) Name() string { + return n.addr.String() + " (close)" +} + +// ModulePath implements GraphNodeModulePath. +func (n *nodeEphemeralResourceClose) ModulePath() addrs.Module { + return n.addr.Module +} + +// Execute implements GraphNodeExecutable. +func (n *nodeEphemeralResourceClose) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + log.Printf("[TRACE] nodeEphemeralResourceClose: closing all instances of %s", n.addr) + resources := ctx.EphemeralResources() + return resources.CloseInstances(context.TODO(), n.addr) +} + +// ephemeralResourceInstImpl implements ephemeral.ResourceInstance as an +// adapter to the relevant provider API calls. +type ephemeralResourceInstImpl struct { + addr addrs.AbsResourceInstance + provider providers.Interface + internal []byte +} + +var _ ephemeral.ResourceInstance = (*ephemeralResourceInstImpl)(nil) + +// Close implements ephemeral.ResourceInstance. +func (impl *ephemeralResourceInstImpl) Close(ctx context.Context) tfdiags.Diagnostics { + log.Printf("[TRACE] ephemeralResourceInstImpl: closing %s", impl.addr) + resp := impl.provider.CloseEphemeralResource(providers.CloseEphemeralResourceRequest{ + TypeName: impl.addr.Resource.Resource.Type, + InternalContext: impl.internal, + }) + return resp.Diagnostics +} + +// Renew implements ephemeral.ResourceInstance. +func (impl *ephemeralResourceInstImpl) Renew(ctx context.Context, req providers.EphemeralRenew) (nextRenew *providers.EphemeralRenew, diags tfdiags.Diagnostics) { + log.Printf("[TRACE] ephemeralResourceInstImpl: renewing %s", impl.addr) + resp := impl.provider.RenewEphemeralResource(providers.RenewEphemeralResourceRequest{ + TypeName: impl.addr.Resource.Resource.Type, + InternalContext: req.InternalContext, + }) + return resp.RenewAgain, resp.Diagnostics +} diff --git a/internal/terraform/node_resource_partial_plan.go b/internal/terraform/node_resource_partial_plan.go index 73c5643986..b7a1672516 100644 --- a/internal/terraform/node_resource_partial_plan.go +++ b/internal/terraform/node_resource_partial_plan.go @@ -216,7 +216,7 @@ func (n *nodeExpandPlannableResource) expandKnownModule(globalCtx EvalContext, r moduleCtx := evalContextForModuleInstance(globalCtx, resAddr.Module) - moreDiags := n.writeResourceState(moduleCtx, resAddr) + moreDiags := n.recordResourceData(moduleCtx, resAddr) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, nil, nil, diags diff --git a/internal/terraform/node_resource_plan.go b/internal/terraform/node_resource_plan.go index e7ea285938..8714990c20 100644 --- a/internal/terraform/node_resource_plan.go +++ b/internal/terraform/node_resource_plan.go @@ -305,14 +305,18 @@ func (n *nodeExpandPlannableResource) validateExpandedImportTargets(expandedImpo return diags } -func (n *nodeExpandPlannableResource) dynamicExpand(ctx EvalContext, moduleInstances []addrs.ModuleInstance, imports addrs.Map[addrs.AbsResourceInstance, cty.Value]) (*Graph, tfdiags.Diagnostics) { - var g Graph - var diags tfdiags.Diagnostics +func (n *nodeExpandPlannableResource) findOrphans(ctx EvalContext, moduleInstances []addrs.ModuleInstance) []*states.Resource { + if n.Addr.Resource.Mode == addrs.EphemeralResourceMode { + // ephemeral resources don't exist in state + return nil + } + + var orphans []*states.Resource // Lock the state while we inspect it - state := ctx.State().Lock() + sMgr := ctx.State() + state := sMgr.Lock() - var orphans []*states.Resource for _, res := range state.Resources(n.Addr) { found := false for _, m := range moduleInstances { @@ -327,11 +331,16 @@ func (n *nodeExpandPlannableResource) dynamicExpand(ctx EvalContext, moduleInsta orphans = append(orphans, res) } } + sMgr.Unlock() + + return orphans +} + +func (n *nodeExpandPlannableResource) dynamicExpand(ctx EvalContext, moduleInstances []addrs.ModuleInstance, imports addrs.Map[addrs.AbsResourceInstance, cty.Value]) (*Graph, tfdiags.Diagnostics) { + var g Graph + var diags tfdiags.Diagnostics - // We'll no longer use the state directly here, and the other functions - // we'll call below may use it so we'll release the lock. - state = nil - ctx.State().Unlock() + orphans := n.findOrphans(ctx, moduleInstances) for _, res := range orphans { for key := range res.Instances { @@ -402,7 +411,7 @@ func (n *nodeExpandPlannableResource) expandResourceInstances(globalCtx EvalCont // writeResourceState is responsible for informing the expander of what // repetition mode this resource has, which allows expander.ExpandResource // to work below. - moreDiags := n.writeResourceState(moduleCtx, resAddr) + moreDiags := n.recordResourceData(moduleCtx, resAddr) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, diags diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index deac2dff10..f07be0f340 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -76,6 +76,8 @@ func (n *NodePlannableResourceInstance) Execute(ctx EvalContext, op walkOperatio return n.managedResourceExecute(ctx) case addrs.DataResourceMode: return n.dataResourceExecute(ctx) + case addrs.EphemeralResourceMode: + return n.ephemeralResourceExecute(ctx) default: panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode)) } @@ -152,6 +154,14 @@ func (n *NodePlannableResourceInstance) dataResourceExecute(ctx EvalContext) (di return diags } +func (n *NodePlannableResourceInstance) ephemeralResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { + return ephemeralResourceOpen(ctx, ephemeralResourceInput{ + addr: n.Addr, + config: n.Config, + providerConfig: n.ResolvedProvider, + }) +} + func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { config := n.Config addr := n.ResourceInstanceAddr() diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index fcf808edf6..7e92f80e1f 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -440,6 +440,9 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag resp := provider.ValidateDataResourceConfig(req) diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) + case addrs.EphemeralResourceMode: + // TODO!! + panic("not implemented") } return diags