terraform/internal/backend/pluggable/pluggable.go
2026-02-17 13:56:34 +00:00

184 lines
6.4 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package pluggable
import (
"errors"
"fmt"
"log"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/backend/pluggable/chunks"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states/remote"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// NewPluggable returns a Pluggable. A Pluggable fulfils the
// backend.Backend interface and allows management of state via
// a state store implemented in the provider that's within the Pluggable.
//
// These are the assumptions about that
// provider:
// * The provider implements at least one state store.
// * The provider has already been fully configured before using NewPluggable.
//
// The state store could also be configured prior to using NewPluggable,
// but preferably it will be configured via the Pluggable,
// using the relevant backend.Backend methods.
//
// By wrapping a configured provider in a Pluggable we allow calling code
// to use the provider's gRPC methods when interacting with state.
func NewPluggable(p providers.Interface, typeName string) (*Pluggable, error) {
if p == nil {
return nil, errors.New("Attempted to initialize pluggable state with a nil provider interface. This is a bug in Terraform and should be reported")
}
if typeName == "" {
return nil, errors.New("Attempted to initialize pluggable state with an empty string identifier for the state store. This is a bug in Terraform and should be reported")
}
return &Pluggable{
provider: p,
typeName: typeName,
}, nil
}
var _ backend.Backend = &Pluggable{}
type Pluggable struct {
provider providers.Interface
typeName string
}
// ConfigSchema returns the schema for the state store implementation
// name provided when the Pluggable was constructed.
//
// ConfigSchema implements backend.Backend
func (p *Pluggable) ConfigSchema() *configschema.Block {
schemaResp := p.provider.GetProviderSchema()
if len(schemaResp.StateStores) == 0 {
// No state stores
return nil
}
val, ok := schemaResp.StateStores[p.typeName]
if !ok {
// Cannot find state store with that type
return nil
}
// State store type exists
return val.Body
}
// ProviderSchema returns the schema for the provider implementing the state store.
//
// This isn't part of the backend.Backend interface but is needed in calling code.
// When it's used the backend.Backend will need to be cast to a Pluggable.
func (p *Pluggable) ProviderSchema() *configschema.Block {
schemaResp := p.provider.GetProviderSchema()
return schemaResp.Provider.Body
}
// PrepareConfig validates configuration for the state store in
// the state storage provider. The configuration sent from Terraform core
// will not include any values from environment variables; it is the
// provider's responsibility to access any environment variables
// to get the complete set of configuration prior to validating it.
//
// PrepareConfig implements backend.Backend
func (p *Pluggable) PrepareConfig(config cty.Value) (cty.Value, tfdiags.Diagnostics) {
req := providers.ValidateStateStoreConfigRequest{
TypeName: p.typeName,
Config: config,
}
resp := p.provider.ValidateStateStoreConfig(req)
return config, resp.Diagnostics
}
// Configure configures the state store in the state storage provider.
// Calling code is expected to have already validated the config using
// the PrepareConfig method.
//
// It is the provider's responsibility to access any environment variables
// set by the user to get the complete set of configuration.
//
// Configure implements backend.Backend
func (p *Pluggable) Configure(config cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
req := providers.ConfigureStateStoreRequest{
TypeName: p.typeName,
Config: config,
Capabilities: providers.StateStoreClientCapabilities{
// The core binary will always request the default chunk size from the provider to start
ChunkSize: chunks.DefaultStateStoreChunkSize,
},
}
resp := p.provider.ConfigureStateStore(req)
diags = diags.Append(resp.Diagnostics)
if diags.HasErrors() {
return diags
}
// Validate the returned value from chunk size negotiation
chunkSize := resp.Capabilities.ChunkSize
if chunkSize == 0 || chunkSize > chunks.MaxStateStoreChunkSize {
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
chunks.MaxStateStoreChunkSize, chunkSize,
))
return diags
}
// Negotiated chunk size is valid, so set it in the provider server
// that will use the value for future RPCs to read/write state.
cs := p.provider.(providers.StateStoreChunkSizeSetter)
cs.SetStateStoreChunkSize(p.typeName, int(chunkSize))
log.Printf("[TRACE] Pluggable.Configure: negotiated a chunk size of %v when configuring state store %s",
chunkSize,
p.typeName,
)
return resp.Diagnostics
}
// Workspaces returns a list of all states/CE workspaces that the backend.Backend
// can find, given how it is configured. For example returning a list of differently
// -named files in a blob storage service.
//
// Workspace implements backend.Backend
func (p *Pluggable) Workspaces() ([]string, tfdiags.Diagnostics) {
req := providers.GetStatesRequest{
TypeName: p.typeName,
}
resp := p.provider.GetStates(req)
return resp.States, resp.Diagnostics
}
// DeleteWorkspace deletes the state file for the named workspace.
// The state storage provider is expected to return error diagnostics
// if the workspace doesn't exist or it is unable to be deleted.
//
// DeleteWorkspace implements backend.Backend
func (p *Pluggable) DeleteWorkspace(workspace string, force bool) tfdiags.Diagnostics {
req := providers.DeleteStateRequest{
TypeName: p.typeName,
StateId: workspace,
}
resp := p.provider.DeleteState(req)
return resp.Diagnostics
}
// StateMgr returns a state manager that uses gRPC to communicate with the
// state storage provider to interact with state.
//
// StateMgr implements backend.Backend
func (p *Pluggable) StateMgr(workspace string) (statemgr.Full, tfdiags.Diagnostics) {
// repackages the provider's methods inside a state manager,
// to be passed to the calling code that expects a statemgr.Full
return remote.NewRemoteGRPC(p.provider, p.typeName, workspace), nil
}