Skip to content

Commit

Permalink
resources/ephemeral: A place to track ephemeral resource instances
Browse files Browse the repository at this point in the history
  • Loading branch information
apparentlymart committed May 2, 2024
1 parent 2ab8071 commit 95e1621
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 0 deletions.
31 changes: 31 additions & 0 deletions internal/resources/ephemeral/ephemeral_resource_instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package ephemeral

import (
"context"

"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// ResourceInstance is an interface that must be implemented for each
// active ephemeral resource instance to determine how it should be renewed
// and eventually closed.
type ResourceInstance interface {
// Renew attempts to extend the life of the remote object associated with
// this resource instance, optionally returning a new renewal request to be
// passed to a subsequent call to this method.
//
// If the object's life is not extended successfully then Renew returns
// error diagnostics explaining why not, and future requests that might
// have made use of the object will fail.
Renew(ctx context.Context, req providers.EphemeralRenew) (nextRenew *providers.EphemeralRenew, diags tfdiags.Diagnostics)

// Close proactively ends the life of the remote object associated with
// this resource instance, if possible. For example, if the remote object
// is a temporary lease for a dynamically-generated secret then this
// might end that lease and thus cause the secret to be promptly revoked.
Close(ctx context.Context) tfdiags.Diagnostics
}
105 changes: 105 additions & 0 deletions internal/resources/ephemeral/ephemeral_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package ephemeral

import (
"context"
"fmt"
"sync"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/tfdiags"

"github.com/zclconf/go-cty/cty"
)

// Resources is a tracking structure for active instances of ephemeral
// resources.
//
// The lifecycle of an ephemeral resource instance is quite different than
// other resource modes because it's live for at most the duration of a single
// graph walk, and because it might need periodic "renewing" in order to
// remain live for the necessary duration.
type Resources struct {
active addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *resourceInstanceInternal]]
mu sync.Mutex
}

func NewResources() *Resources {
return &Resources{
active: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *resourceInstanceInternal]](),
}
}

type ResourceInstanceRegistration struct {
Value cty.Value
ConfigBody hcl.Body
Impl ResourceInstance
FirstRenewal *providers.EphemeralRenew
}

func (r *Resources) RegisterInstance(ctx context.Context, addr addrs.AbsResourceInstance, reg ResourceInstanceRegistration) {
if addr.Resource.Resource.Mode != addrs.EphemeralResourceMode {
panic(fmt.Sprintf("can't register %s as an ephemeral resource instance", addr))
}

r.mu.Lock()
defer r.mu.Unlock()

configAddr := addr.ConfigResource()
if !r.active.Has(configAddr) {
r.active.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *resourceInstanceInternal]())
}
r.active.Get(configAddr).Put(addr, &resourceInstanceInternal{
value: reg.Value,
configBody: reg.ConfigBody,
impl: reg.Impl,
nextRenew: reg.FirstRenewal,
})
// TODO: If a renewal was requested, start a cancelable goroutine for it.
}

func (r *Resources) InstanceValue(addr addrs.AbsResourceInstance) (val cty.Value, live bool) {
r.mu.Lock()
defer r.mu.Unlock()

configAddr := addr.ConfigResource()
insts, ok := r.active.GetOk(configAddr)
if !ok {
return cty.DynamicVal, false
}
inst, ok := insts.GetOk(addr)
if !ok {
return cty.DynamicVal, false
}
// If renewal has failed then we can't assume that the object is still
// live, but we can still return the original value regardless.
return inst.value, !inst.renewDiags.HasErrors()
}

func (r *Resources) CloseInstances(ctx context.Context, configAddr addrs.ConfigResource) tfdiags.Diagnostics {
r.mu.Lock()
defer r.mu.Unlock()
// TODO: Can we somehow avoid holding the lock for the entire duration?
// Closing an instance is likely to perform a network request, so this
// could potentially take a while and block other work from starting.

var diags tfdiags.Diagnostics
for _, elem := range r.active.Get(configAddr).Elems {
moreDiags := elem.Value.impl.Close(ctx)
diags = diags.Append(moreDiags.InConfigBody(elem.Value.configBody, elem.Key.String()))
}
r.active.Remove(configAddr)
return diags
}

type resourceInstanceInternal struct {
value cty.Value
configBody hcl.Body
impl ResourceInstance
nextRenew *providers.EphemeralRenew
renewDiags tfdiags.Diagnostics
}

0 comments on commit 95e1621

Please sign in to comment.