From 059c2f95a0942863c8b23a195ab646cc34a4d227 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 3 May 2024 16:21:24 -0700 Subject: [PATCH] terraform: Open and close ephemeral resource instances during graph walk --- internal/terraform/node_resource_ephemeral.go | 155 ++++++++++++++++++ .../terraform/node_resource_plan_instance.go | 17 +- 2 files changed, 159 insertions(+), 13 deletions(-) diff --git a/internal/terraform/node_resource_ephemeral.go b/internal/terraform/node_resource_ephemeral.go index 4d6f85138465..5a0dd6f47479 100644 --- a/internal/terraform/node_resource_ephemeral.go +++ b/internal/terraform/node_resource_ephemeral.go @@ -4,9 +4,119 @@ 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/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 data source %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() + + // TODO: Call provider.ValidateEphemeralConfig, once such a thing exists. + // For prototype we'll just let OpenEphemeral be the first line of validation. + + resp := provider.OpenEphemeral(providers.OpenEphemeralRequest{ + TypeName: inp.addr.ContainingResource().Resource.Type, + Config: unmarkedConfigVal, + }) + if resp.Deferred != nil { + // FIXME: Actually implement this. (Skipped for prototype only because + // deferred changes is under development concurrently with this + // prototype, and so don't want to conflict.) + diags = diags.Append(fmt.Errorf("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 { + // FIXME: Should turn these errors into suitable diagnostics. + diags = diags.Append(err) + } + if diags.HasErrors() { + return diags + } + + impl := &ephemeralResourceInstImpl{ + addr: inp.addr, + provider: provider, + closeData: 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? + resources.RegisterInstance(context.TODO(), inp.addr, ephemeral.ResourceInstanceRegistration{ + Value: resp.Result.MarkWithPaths(configMarks), + 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. // @@ -24,6 +134,51 @@ 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 + closeData []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.CloseEphemeral(providers.CloseEphemeralRequest{ + TypeName: impl.addr.Resource.Resource.Type, + InternalContext: impl.closeData, + }) + 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.RenewEphemeral(providers.RenewEphemeralRequest{ + TypeName: impl.addr.Resource.Resource.Type, + InternalContext: req.InternalContext, + }) + return resp.RenewAgain, resp.Diagnostics +} diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 97da9fa3f8df..6f05ca22d2a3 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -446,20 +446,11 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) } func (n *NodePlannableResourceInstance) ephemeralResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { - config := n.Config - addr := n.ResourceInstanceAddr() - var diagRng *hcl.Range - if config != nil { - diagRng = config.DeclRange.Ptr() - } - - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Ephemeral resources not yet supported", - Detail: fmt.Sprintf("There is not yet any implementation of planning for %s.", addr), - Subject: diagRng, + return ephemeralResourceOpen(ctx, ephemeralResourceInput{ + addr: n.Addr, + config: n.Config, + providerConfig: n.ResolvedProvider, }) - return diags } // replaceTriggered checks if this instance needs to be replace due to a change