mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-05-16 16:01:49 -04:00
241 lines
7.0 KiB
Go
241 lines
7.0 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package shared
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
type EphemeralResourceHooks struct {
|
|
PreOpen func(addrs.AbsResourceInstance)
|
|
PostOpen func(addrs.AbsResourceInstance, tfdiags.Diagnostics)
|
|
PreRenew func(addrs.AbsResourceInstance)
|
|
PostRenew func(addrs.AbsResourceInstance, tfdiags.Diagnostics)
|
|
PreClose func(addrs.AbsResourceInstance)
|
|
PostClose func(addrs.AbsResourceInstance, tfdiags.Diagnostics)
|
|
}
|
|
|
|
type EphemeralCloseFunc func(context.Context) tfdiags.Diagnostics
|
|
|
|
func OpenEphemeralResourceInstance(
|
|
ctx context.Context,
|
|
// TODO once we remove the old engine, this can be condensed using the new engine types
|
|
addr addrs.AbsResourceInstance,
|
|
schema *configschema.Block,
|
|
providerAddr addrs.AbsProviderInstanceCorrect,
|
|
provider providers.Interface,
|
|
configVal cty.Value,
|
|
hooks EphemeralResourceHooks,
|
|
) (cty.Value, EphemeralCloseFunc, tfdiags.Diagnostics) {
|
|
var newVal cty.Value
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// Unmark before sending to provider, will re-mark before returning
|
|
configVal, pvm := configVal.UnmarkDeepWithPaths()
|
|
|
|
log.Printf("[TRACE] OpenEphemeralResourceInstance: Re-validating config for %s", addr)
|
|
validateResp := provider.ValidateEphemeralConfig(
|
|
ctx,
|
|
providers.ValidateEphemeralConfigRequest{
|
|
TypeName: addr.ContainingResource().Resource.Type,
|
|
Config: configVal,
|
|
},
|
|
)
|
|
diags = diags.Append(validateResp.Diagnostics)
|
|
if diags.HasErrors() {
|
|
return newVal, nil, diags
|
|
}
|
|
|
|
// If we get down here then our configuration is complete and we're ready
|
|
// to actually call the provider to open the ephemeral resource.
|
|
log.Printf("[TRACE] OpenEphemeralResourceInstance: %s configuration is complete, so calling the provider", addr)
|
|
|
|
if hooks.PreOpen != nil {
|
|
hooks.PreOpen(addr)
|
|
}
|
|
|
|
openReq := providers.OpenEphemeralResourceRequest{
|
|
TypeName: addr.ContainingResource().Resource.Type,
|
|
Config: configVal,
|
|
}
|
|
openResp := provider.OpenEphemeralResource(ctx, openReq)
|
|
diags = diags.Append(openResp.Diagnostics)
|
|
if diags.HasErrors() {
|
|
return newVal, nil, diags
|
|
}
|
|
|
|
newVal = openResp.Result
|
|
|
|
// Encapsulate validation for easier close handling
|
|
func() {
|
|
for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced invalid object",
|
|
fmt.Sprintf(
|
|
"Provider %q produced an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
providerAddr, tfdiags.FormatErrorPrefixed(err, addr.String()),
|
|
),
|
|
))
|
|
}
|
|
if diags.HasErrors() {
|
|
return
|
|
}
|
|
|
|
if newVal.IsNull() {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced null object",
|
|
fmt.Sprintf(
|
|
"Provider %q produced a null value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
providerAddr, addr,
|
|
),
|
|
))
|
|
return
|
|
}
|
|
|
|
if !newVal.IsNull() && !newVal.IsWhollyKnown() {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Provider produced invalid object",
|
|
fmt.Sprintf(
|
|
"Provider %q produced a value for %s that is not wholly known.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
|
|
providerAddr, addr,
|
|
),
|
|
))
|
|
return
|
|
}
|
|
}()
|
|
|
|
if diags.HasErrors() {
|
|
// We have an open ephemeral resource, but don't plan to use it due to validation errors
|
|
// It needs to be closed before we can return
|
|
|
|
closReq := providers.CloseEphemeralResourceRequest{
|
|
TypeName: addr.Resource.Resource.Type,
|
|
Private: openResp.Private,
|
|
}
|
|
closeResp := provider.CloseEphemeralResource(ctx, closReq)
|
|
diags = diags.Append(closeResp.Diagnostics)
|
|
|
|
return newVal, nil, diags
|
|
}
|
|
|
|
// TODO see if this conflicts with anything in the new engine?
|
|
if len(pvm) > 0 {
|
|
newVal = newVal.MarkWithPaths(pvm)
|
|
}
|
|
|
|
if hooks.PostOpen != nil {
|
|
hooks.PostOpen(addr, diags)
|
|
}
|
|
|
|
// Initialize the closing channel and the channel that sends diagnostics back to the close caller.
|
|
closeCh := make(chan context.Context, 1)
|
|
diagsCh := make(chan tfdiags.Diagnostics, 1)
|
|
go func() {
|
|
var diags tfdiags.Diagnostics
|
|
renewAt := openResp.RenewAt
|
|
privateData := openResp.Private
|
|
|
|
closeCtx := ctx
|
|
|
|
// We have two exit paths that should take the same route
|
|
func() {
|
|
for {
|
|
// This is necessary to block on the select statement in 2 cases:
|
|
// - if renewAt == nil, then the renewal process is disabled and we
|
|
// want to wait for the close call or ctx.Done() so we return a nil
|
|
// chan that will block the select statement for the other cases
|
|
// - if renewAt != nil, we want to execute the renewal at the given interval
|
|
// so we return a channel that will trigger after the given interval
|
|
waitForRenewal := func() <-chan time.Time {
|
|
if renewAt != nil {
|
|
return time.After(time.Until(*renewAt))
|
|
}
|
|
return nil
|
|
}
|
|
select {
|
|
case <-waitForRenewal():
|
|
if hooks.PreRenew != nil {
|
|
hooks.PreRenew(addr)
|
|
}
|
|
|
|
renewReq := providers.RenewEphemeralResourceRequest{
|
|
TypeName: addr.Resource.Resource.Type,
|
|
Private: privateData,
|
|
}
|
|
renewResp := provider.RenewEphemeralResource(ctx, renewReq)
|
|
diags = diags.Append(renewResp.Diagnostics)
|
|
|
|
// TODO consider what happens if renew fails, do we still want to update private?
|
|
renewAt = renewResp.RenewAt
|
|
if hooks.PostRenew != nil {
|
|
hooks.PostRenew(addr, diags)
|
|
}
|
|
privateData = renewResp.Private
|
|
case closeCtx = <-closeCh:
|
|
return
|
|
case <-ctx.Done():
|
|
// Even though the context is "Done" we still want to execute the close operation
|
|
closeCtx = context.WithoutCancel(closeCtx)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
if hooks.PreClose != nil {
|
|
hooks.PreClose(addr)
|
|
}
|
|
|
|
closReq := providers.CloseEphemeralResourceRequest{
|
|
TypeName: addr.Resource.Resource.Type,
|
|
Private: privateData,
|
|
}
|
|
closeResp := provider.CloseEphemeralResource(closeCtx, closReq)
|
|
diags = diags.Append(closeResp.Diagnostics)
|
|
|
|
if hooks.PostClose != nil {
|
|
hooks.PostClose(addr, diags)
|
|
}
|
|
|
|
diagsCh <- diags
|
|
}()
|
|
|
|
closeFunc := func(ctx context.Context) tfdiags.Diagnostics {
|
|
closeCh <- ctx
|
|
close(closeCh)
|
|
defer func() {
|
|
close(diagsCh)
|
|
}()
|
|
|
|
timeout := 10 * time.Second
|
|
select {
|
|
case d := <-diagsCh:
|
|
return d
|
|
case <-time.After(timeout):
|
|
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Closing ephemeral resource timed out",
|
|
Detail: fmt.Sprintf("The ephemeral resource %q timed out on closing after %s", addr.String(), timeout),
|
|
// TODO Subject: n.Config.DeclRange.Ptr(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return newVal, closeFunc, diags
|
|
}
|