blob: d901d83bd25b04f94076486446150f7b34b5424c [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package rpcapi
import (
"fmt"
"sync"
"github.com/hashicorp/go-slug/sourcebundle"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
"github.com/hashicorp/terraform/internal/states"
)
// handle represents an identifier shared between client and server to identify
// a particular object.
//
// From the server's perspective, these are always issued by and tracked within
// a [handleTable]. The conventional variable name for a handle in this codebase
// is "hnd", or a name with "Hnd" as a suffix such as "planHnd".
type handle[T any] int64
// ForProtobuf returns the handle as a naked int64 for use in a protocol buffers
// message. This erases the information about what type of object this handle
// was for, so should be used only when preparing a protobuf response and not
// for anything internal to this package.
func (hnd handle[T]) ForProtobuf() int64 {
return int64(hnd)
}
// IsNil returns true if the reciever is the "nil handle", which is also the
// zero value of any handle type and represents the absense of a handle.
func (hnd handle[T]) IsNil() bool {
return int64(hnd) == 0
}
// handleTable is our shared table of "handles", which are really just integers
// that clients can use to refer to an object they've previously opened.
//
// In our public API contract each different object has a separate numberspace
// of handles, but as an implementation detail we share a single numberspace
// for everything just because that means that if a client gets their handles
// mixed up then it'll fail with an error rather than potentially doing
// something unexpected to an unrelated object, since these all appear as the
// same type in the protobuf-generated API.
//
// The handleTable API intentionally requires callers to be explicit about
// what kind of handle they are intending to work with. There is no function
// to query which kind of object is associated with a handle, because if
// a particular operation can accept handles of multiple types it should
// separate those into separate request fields and decide how to interpret
// the handles based on which fields are populated by the caller.
type handleTable struct {
handleObjs map[int64]any
nextHandle int64
// handleDeps tracks dependencies between handles to disallow closing
// a handle that has another open handle depending on it. For example,
// each stack configuration handle depends on a source bundle handle
// because closing the source bundle and deleting its underlying directory
// would cause unwanted misbehavior for future operations against that
// stack configuration.
//
// The first level of map is the object being depended on, and the second
// level are the objects doing the depending.
handleDeps map[int64]map[int64]struct{}
// TODO: Consider also tracking when a particular handle is being actively
// used by a running RPC operation, so that we can return an error if
// a caller tries to close a handle concurrently with an active operation.
// That would be a weird thing to do though and always a bug in the caller,
// so for now we're just letting it cause unspecified behavior for
// simplicity's sake.
mu sync.Mutex
}
func newHandleTable() *handleTable {
return &handleTable{
handleObjs: make(map[int64]any),
handleDeps: make(map[int64]map[int64]struct{}),
nextHandle: 1,
}
}
func (t *handleTable) NewSourceBundle(sources *sourcebundle.Bundle) handle[*sourcebundle.Bundle] {
return newHandle(t, sources)
}
func (t *handleTable) SourceBundle(hnd handle[*sourcebundle.Bundle]) *sourcebundle.Bundle {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseSourceBundle(hnd handle[*sourcebundle.Bundle]) error {
return closeHandle(t, hnd)
}
func (t *handleTable) NewStackConfig(cfg *stackconfig.Config, owningSourceBundle handle[*sourcebundle.Bundle]) (handle[*stackconfig.Config], error) {
return newHandleWithDependency(t, cfg, owningSourceBundle)
}
func (t *handleTable) StackConfig(hnd handle[*stackconfig.Config]) *stackconfig.Config {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseStackConfig(hnd handle[*stackconfig.Config]) error {
return closeHandle(t, hnd)
}
func (t *handleTable) NewStackState(state *stackstate.State) handle[*stackstate.State] {
return newHandle(t, state)
}
func (t *handleTable) StackState(hnd handle[*stackstate.State]) *stackstate.State {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseStackState(hnd handle[*stackstate.State]) error {
return closeHandle(t, hnd)
}
func (t *handleTable) NewStackPlan(state *stackplan.Plan) handle[*stackplan.Plan] {
return newHandle(t, state)
}
func (t *handleTable) StackPlan(hnd handle[*stackplan.Plan]) *stackplan.Plan {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseStackPlan(hnd handle[*stackplan.Plan]) error {
return closeHandle(t, hnd)
}
func (t *handleTable) NewTerraformState(state *states.State) handle[*states.State] {
return newHandle(t, state)
}
func (t *handleTable) TerraformState(hnd handle[*states.State]) *states.State {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseTerraformState(hnd handle[*states.State]) error {
return closeHandle(t, hnd)
}
func (t *handleTable) NewDependencyLocks(locks *depsfile.Locks) handle[*depsfile.Locks] {
// NOTE: We intentionally don't track a dependency on a source bundle
// here for two reasons:
// - Not all lock objects necessarily original from lock files. For example,
// we could be creating a new empty locks that will be mutated and then
// saved to disk for the first time afterwards.
// - The locks object in memory is not dependent on the lock file it was
// loaded from once the load is complete. Closing the source bundle and
// deleting its directory would not affect the validity of the locks
// object.
return newHandle(t, locks)
}
func (t *handleTable) DependencyLocks(hnd handle[*depsfile.Locks]) *depsfile.Locks {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseDependencyLocks(hnd handle[*depsfile.Locks]) error {
return closeHandle(t, hnd)
}
func (t *handleTable) NewProviderPluginCache(dir *providercache.Dir) handle[*providercache.Dir] {
return newHandle(t, dir)
}
func (t *handleTable) ProviderPluginCache(hnd handle[*providercache.Dir]) *providercache.Dir {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseProviderPluginCache(hnd handle[*providercache.Dir]) error {
return closeHandle(t, hnd)
}
func (t *handleTable) NewStackInspector(dir *stacksInspector) handle[*stacksInspector] {
return newHandle(t, dir)
}
func (t *handleTable) StackInspector(hnd handle[*stacksInspector]) *stacksInspector {
ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil
return ret
}
func (t *handleTable) CloseStackInspector(hnd handle[*stacksInspector]) error {
return closeHandle(t, hnd)
}
func newHandle[ObjT any](t *handleTable, obj ObjT) handle[ObjT] {
t.mu.Lock()
hnd := t.nextHandle
t.nextHandle++ // NOTE: We're assuming int64 is big enough for overflow to be impractical
t.handleObjs[hnd] = obj
t.mu.Unlock()
return handle[ObjT](hnd)
}
// newHandleWithDependency is a variant of newHandle which also records a
// dependency on some other handle.
//
// Unlike newHandle, this is fallible because creating the new handle might
// race with closing the dependency handle, causing that handle to no longer
// be available by the time this function is running. In that case, the
// returned error will be [newHandleErrorNoParent].
func newHandleWithDependency[ObjT, DepT any](t *handleTable, obj ObjT, dep handle[DepT]) (handle[ObjT], error) {
t.mu.Lock()
if depObjectI, exists := t.handleObjs[int64(dep)]; !exists {
return handle[ObjT](0), newHandleErrorNoParent
} else if depObject, ok := depObjectI.(DepT); !ok {
// It's caller's responsibility to ensure that it's passing in valid handles.
// (This will typically be ensured by our type-safe wrapper methods)
panic(fmt.Sprintf("dependency handle %d is %T, not %T", int64(dep), depObjectI, depObject))
}
hnd := t.nextHandle
t.nextHandle++ // NOTE: We're assuming int64 is big enough for overflow to be impractical
t.handleObjs[hnd] = obj
if _, exists := t.handleDeps[int64(dep)]; !exists {
t.handleDeps[int64(dep)] = make(map[int64]struct{})
}
t.handleDeps[int64(dep)][hnd] = struct{}{}
t.mu.Unlock()
return handle[ObjT](hnd), nil
}
func readHandle[ObjT any](t *handleTable, hnd handle[ObjT]) (ObjT, bool) {
t.mu.Lock()
defer t.mu.Unlock()
var zero ObjT
if existing, exists := t.handleObjs[int64(hnd)]; !exists {
return zero, false
} else if existing, ok := existing.(ObjT); !ok {
return zero, false
} else {
return existing, true
}
}
func closeHandle[ObjT any](t *handleTable, hnd handle[ObjT]) error {
t.mu.Lock()
defer t.mu.Unlock()
if existing, exists := t.handleObjs[int64(hnd)]; !exists {
return closeHandleErrorUnknown
} else if _, ok := existing.(ObjT); !ok {
return closeHandleErrorUnknown
}
if len(t.handleDeps[int64(hnd)]) > 0 {
return closeHandleErrorBlocked
}
delete(t.handleObjs, int64(hnd))
// We'll also revoke this object's dependencies so that they
// can potentially be closed after we return. Our dependency-tracking
// data structure is not optimized for deleting because that's rare
// in comparison to adding and checking, but we expect the handle
// table to typically be small enough for this full scan not to hurt.
for _, m := range t.handleDeps {
delete(m, int64(hnd)) // no-op if not present
}
return nil
}
type newHandleError rune
const (
newHandleErrorNoParent newHandleError = '^'
)
func (err newHandleError) Error() string {
switch err {
case newHandleErrorNoParent:
return "parent handle does not exist"
default:
return "unknown error creating handle"
}
}
type closeHandleError rune
const (
closeHandleErrorUnknown closeHandleError = '?'
closeHandleErrorBlocked closeHandleError = 'B'
)
func (err closeHandleError) Error() string {
switch err {
case closeHandleErrorUnknown:
return "unknown handle"
case closeHandleErrorBlocked:
return "handle is in use by another open handle"
default:
return "unknown error closing handle"
}
}