Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
40da592
Implement ReadStateBytes + WriteStateBytes
radeksimko Aug 13, 2025
c04c107
[WIP] - Testing ReadStateBytes and WriteStateBytes (#37464)
SarahFrench Aug 26, 2025
012ae7a
Update proto file definition of Read/WriteStateBytes RPCs (#37529)
SarahFrench Sep 2, 2025
37d7c38
WIP - avoid crash if chunk is nil
radeksimko Sep 8, 2025
d7a4964
Sarah's updates to radek/pss-read-write (#37642)
SarahFrench Sep 19, 2025
3d3c30a
Store the FQN of the provider used in PSS in representations of the p…
SarahFrench Jul 10, 2025
42f14d1
Add test that hits the code path for adding a state store to a new (o…
SarahFrench Jul 11, 2025
c033bcb
Add test for use of `-reconfigure` flag; show that it hits the code p…
SarahFrench Jul 11, 2025
dec360a
Add test that hits the code path for removing use of a state store, m…
SarahFrench Jul 11, 2025
14dc216
Add test that hits the code path for changing a state store's configu…
SarahFrench Jul 11, 2025
ba2d21e
Update existing test names to be backend-specific
SarahFrench Jul 11, 2025
c4768a6
Add tests that hits the code path for migrating between PSS and backends
SarahFrench Jul 11, 2025
0b4cc96
Consolidate PSS-related tests at end of the file
SarahFrench Jul 11, 2025
1b0f471
Make init command output report if a backend or state store is being …
SarahFrench Jul 14, 2025
9bd9719
Only process -backend-config flags for use with backend if the user h…
SarahFrench Jul 14, 2025
609991e
Implement initialising an uninitialised dir with PSS
SarahFrench Jul 14, 2025
b29f74c
Fix call to `ConfigureProvider` by avoiding use of returned config va…
SarahFrench Aug 7, 2025
0b989ae
Add TF version to `ConfigureProviderRequest`
SarahFrench Aug 7, 2025
a63e31c
Update test to assert that the newly-initialised backend is reflected…
SarahFrench Aug 7, 2025
1bd0c53
Add godoc comment to `GoVersionFromVersion`
SarahFrench Aug 8, 2025
3766f18
command: Prompt user for default workspace creation
radeksimko Aug 4, 2025
2fa43cc
Rename argument to backend and state store init methods to help disti…
SarahFrench Aug 8, 2025
174cc5c
Fix: avoid updating backend state file if an unhandled error has occu…
SarahFrench Aug 8, 2025
b6e6129
Fix missing }
SarahFrench Aug 8, 2025
87d00a4
Add handling of non-default workspace, remove input prompting for now.
SarahFrench Aug 8, 2025
42fdb5e
Rename state manager var, fix bug where PersistState was called on wr…
SarahFrench Aug 8, 2025
e0db4af
Make TODO indicating blocker
SarahFrench Aug 8, 2025
7a4d4d4
Add createDefaultWorkspace method
SarahFrench Aug 12, 2025
ac4aa0b
Add locks to test
SarahFrench Aug 12, 2025
9e31d05
Add `-create-default-workspace` flag to init command, for overriding …
SarahFrench Aug 15, 2025
bac238f
WIP - updating tests, currently blocked
SarahFrench Aug 15, 2025
ccf975e
Random fix from rebasing/remaking branch...
SarahFrench Sep 19, 2025
fe97133
Update tests with new flag
SarahFrench Aug 18, 2025
b119725
Pivot to creating the default workspace without prompts
SarahFrench Aug 18, 2025
ad035bf
Fix: Make sure diags from attempting to create the default workspace …
SarahFrench Aug 26, 2025
b68399a
Add rough implementation of Get and Put methods on remote gRPC state …
SarahFrench Aug 26, 2025
c164cbd
Update tests and mock; this is unblocked now Get and Put are implemented
SarahFrench Aug 26, 2025
1efea5a
Move init command test cases up to the 'command testing' level. This …
SarahFrench Aug 29, 2025
dbb5f30
Fix Get and Put impl. in remote_grpc
SarahFrench Sep 19, 2025
0007471
Pull code for obtaining provider factory into a separate method
SarahFrench Aug 29, 2025
ed701f8
Make Backend method return diagnostics, so warnings aren't swallowed.
SarahFrench Aug 29, 2025
8609e1e
Make test assert that warning about skipping workspace creation is in…
SarahFrench Aug 29, 2025
2cbc866
Update warning detail to include calls to action
SarahFrench Aug 29, 2025
e3b3510
Fix use of hashes when initializing a new workingdir with a state store
SarahFrench Sep 5, 2025
f1414a9
Update test to assert backend state file contents
SarahFrench Sep 5, 2025
72a4334
Replace test for reconfiguring a state_store when the configuration h…
SarahFrench Sep 5, 2025
e6f2839
Fix test, add more assertions about output
SarahFrench Sep 5, 2025
9fecb31
Update comments on tests
SarahFrench Sep 8, 2025
8c55b80
Fix diagnostic using an error unnecessarily
SarahFrench Sep 15, 2025
5567d63
Fix - make sure all errors are handled, and avoid nil error causing a…
SarahFrench Sep 15, 2025
2a411d5
Allow init commands to succeed if using PSS and a reattached provider.
SarahFrench Sep 15, 2025
dc7a0f9
Fix: Pass state bytes into WriteStateBytesRequest
SarahFrench Sep 16, 2025
bca985f
Make error message more specific
SarahFrench Sep 19, 2025
72d2f74
TEMPORARY fix for init error when using provider that isn't builtin o…
SarahFrench Sep 19, 2025
b2091a5
Improvements on previous solution to the problem: How do we obtain pr…
SarahFrench Sep 22, 2025
bd9806d
Use a DeepCopy when getting annotated locks, to avoid using annotated…
SarahFrench Sep 22, 2025
bbf8839
Add test for getStateStoreProviderFactory
SarahFrench Sep 22, 2025
6468225
Implement configurable state chunk size
radeksimko Sep 23, 2025
e5b0991
Implement ReadStateBytes + WriteStateBytes
radeksimko Aug 13, 2025
5cb2a6c
[WIP] - Testing ReadStateBytes and WriteStateBytes (#37464)
SarahFrench Aug 26, 2025
eca6a69
Update proto file definition of Read/WriteStateBytes RPCs (#37529)
SarahFrench Sep 2, 2025
d1930a8
WIP - avoid crash if chunk is nil
radeksimko Sep 8, 2025
d96582c
Sarah's updates to radek/pss-read-write (#37642)
SarahFrench Sep 19, 2025
9d5ab3c
Implement configurable state chunk size
radeksimko Sep 23, 2025
699a3b0
Merge branch 'radek/pss-read-write' into pss/init-of-new-pss-workingd…
SarahFrench Sep 23, 2025
df703ee
Rename methods and update godoc comments
SarahFrench Sep 23, 2025
36768e3
Fix repeat declarations
SarahFrench Sep 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion docs/plugin-protocol/tfplugin6.proto
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ service Provider {
// ConfigureStateStore configures the state store, such as S3 connection in the context of already configured provider
rpc ConfigureStateStore(ConfigureStateStore.Request) returns (ConfigureStateStore.Response);

// ReadStateBytes streams byte chunks of a given state file from a state store
rpc ReadStateBytes(ReadStateBytes.Request) returns (stream ReadStateBytes.Response);
// WriteStateBytes streams byte chunks of a given state file into a state store
rpc WriteStateBytes(stream WriteStateBytes.RequestChunk) returns (WriteStateBytes.Response);

// GetStates returns a list of all states (i.e. CE workspaces) managed by a given state store
rpc GetStates(GetStates.Request) returns (GetStates.Response);
// DeleteState instructs a given state store to delete a specific state (i.e. a CE workspace)
Expand Down Expand Up @@ -896,7 +901,6 @@ message ValidateListResourceConfig {
}
}


message ValidateStateStore {
message Request {
string type_name = 1;
Expand All @@ -911,12 +915,55 @@ message ConfigureStateStore {
message Request {
string type_name = 1;
DynamicValue config = 2;
StateStoreClientCapabilities capabilities = 3;
}
message Response {
repeated Diagnostic diagnostics = 1;
StateStoreServerCapabilities capabilities = 2;
}
}

message StateStoreClientCapabilities {
int64 chunk_size = 1; // suggested chunk size by Core
}

message StateStoreServerCapabilities {
int64 chunk_size = 1; // chosen chunk size by plugin
}

message ReadStateBytes {
message Request {
string type_name = 1;
string state_id = 2;
}
message Response {
bytes bytes = 1;
int64 total_length = 2;
StateRange range = 3;
repeated Diagnostic diagnostics = 4;
}
}

message WriteStateBytes {
message RequestChunk {
// TODO: Can we decouple this outside of the stream?
string type_name = 1;
string state_id = 3;

bytes bytes = 2;
int64 total_length = 4;
StateRange range = 5;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}

message StateRange {
int64 start = 1;
int64 end = 2;
}

message GetStates {
message Request {
string type_name = 1;
Expand Down
9 changes: 9 additions & 0 deletions internal/backend/pluggable/pluggable.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ func (p *Pluggable) ConfigSchema() *configschema.Block {
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
Expand Down
12 changes: 12 additions & 0 deletions internal/builtin/providers/terraform/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,18 @@ func (p *Provider) ConfigureStateStore(req providers.ConfigureStateStoreRequest)
return resp
}

func (p *Provider) ReadStateBytes(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse {
var resp providers.ReadStateBytesResponse
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
return resp
}

func (p *Provider) WriteStateBytes(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse {
var resp providers.WriteStateBytesResponse
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
return resp
}

func (p *Provider) GetStates(req providers.GetStatesRequest) providers.GetStatesResponse {
var resp providers.GetStatesResponse
resp.Diagnostics.Append(fmt.Errorf("unsupported state store type %q", req.TypeName))
Expand Down
14 changes: 13 additions & 1 deletion internal/command/arguments/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ type Init struct {
// TODO(SarahFrench/radeksimko): Remove this once the feature is no longer
// experimental
EnablePssExperiment bool

// CreateDefaultWorkspace indicates whether the default workspace should be created by
// Terraform when initializing a state store for the first time.
CreateDefaultWorkspace bool
}

// ParseInit processes CLI arguments, returning an Init value and errors.
Expand Down Expand Up @@ -111,7 +115,7 @@ func ParseInit(args []string) (*Init, tfdiags.Diagnostics) {
cmdFlags.BoolVar(&init.Json, "json", false, "json")
cmdFlags.Var(&init.BackendConfig, "backend-config", "")
cmdFlags.Var(&init.PluginPath, "plugin-dir", "plugin directory")

cmdFlags.BoolVar(&init.CreateDefaultWorkspace, "create-default-workspace", true, "when -input=false, use this flag to block creation of the default workspace")
// Used for enabling experimental code that's invoked before configuration is parsed.
cmdFlags.BoolVar(&init.EnablePssExperiment, "enable-pluggable-state-storage-experiment", false, "Enable the pluggable state storage experiment")

Expand Down Expand Up @@ -139,6 +143,14 @@ func ParseInit(args []string) (*Init, tfdiags.Diagnostics) {
))
}

if init.InputEnabled && !init.CreateDefaultWorkspace {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Invalid init options",
"The flag -create-default-workspace=false is ignored when Terraform is configured to ask users for input. Instead, add -input=false or remove the -create-default-workspace flag",
))
}

init.Args = cmdFlags.Args()

backendFlagSet := FlagIsSet(cmdFlags, "backend")
Expand Down
31 changes: 17 additions & 14 deletions internal/command/arguments/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ func TestParseInit_basicValid(t *testing.T) {
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: true,
CompactWarnings: false,
TargetFlags: nil,
Vars: &Vars{},
InputEnabled: true,
CompactWarnings: false,
TargetFlags: nil,
CreateDefaultWorkspace: true,
},
},
"setting multiple options": {
Expand Down Expand Up @@ -72,11 +73,12 @@ func TestParseInit_basicValid(t *testing.T) {
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: true,
Args: []string{},
CompactWarnings: true,
TargetFlags: nil,
Vars: &Vars{},
InputEnabled: true,
Args: []string{},
CompactWarnings: true,
TargetFlags: nil,
CreateDefaultWorkspace: true,
},
},
"with cloud option": {
Expand All @@ -101,11 +103,12 @@ func TestParseInit_basicValid(t *testing.T) {
FlagName: "-backend-config",
Items: &[]FlagNameValue{{Name: "-backend-config", Value: "backend.config"}},
},
Vars: &Vars{},
InputEnabled: false,
Args: []string{},
CompactWarnings: false,
TargetFlags: []string{"foo_bar.baz"},
Vars: &Vars{},
InputEnabled: false,
Args: []string{},
CompactWarnings: false,
TargetFlags: []string{"foo_bar.baz"},
CreateDefaultWorkspace: true,
},
},
}
Expand Down
63 changes: 24 additions & 39 deletions internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,11 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra
return back, true, diags
}

func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, initArgs *arguments.Init, locks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
// Temporary vars to account for refactoring the method's parameters
extraConfig := initArgs.BackendConfig
viewType := initArgs.ViewType

ctx, span := tracer.Start(ctx, "initialize backend")
_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
defer span.End()
Expand Down Expand Up @@ -187,34 +191,9 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext
return nil, true, diags
case root.StateStore != nil:
// state_store config present
// Access provider factories
ctxOpts, err := c.contextOpts()
if err != nil {
diags = diags.Append(err)
return nil, true, diags
}

if root.StateStore.ProviderAddr.IsZero() {
// This should not happen; this data is populated when parsing config,
// even for builtin providers
panic(fmt.Sprintf("unknown provider while beginning to initialize state store %q from provider %q",
root.StateStore.Type,
root.StateStore.Provider.Name))
}

var exists bool
factory, exists := ctxOpts.Providers[root.StateStore.ProviderAddr]
if !exists {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider unavailable",
Detail: fmt.Sprintf("The provider %s (%q) is required to initialize the %q state store, but the matching provider factory is missing. This is a bug in Terraform and should be reported.",
root.StateStore.Provider.Name,
root.StateStore.ProviderAddr,
root.StateStore.Type,
),
Subject: &root.StateStore.TypeRange,
})
factory, fDiags := c.Meta.getStateStoreProviderFactory(root.StateStore, locks)
diags = diags.Append(fDiags)
if fDiags.HasErrors() {
return nil, true, diags
}

Expand Down Expand Up @@ -271,11 +250,13 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext
}

opts = &BackendOpts{
StateStoreConfig: root.StateStore,
ProviderFactory: factory,
ConfigOverride: configOverride,
Init: true,
ViewType: viewType,
StateStoreConfig: root.StateStore,
Locks: locks,
ProviderFactory: factory,
CreateDefaultWorkspace: initArgs.CreateDefaultWorkspace,
ConfigOverride: configOverride,
Init: true,
ViewType: viewType,
}

case root.Backend != nil:
Expand Down Expand Up @@ -311,15 +292,19 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext
backendSchema := b.ConfigSchema()
backendConfig := root.Backend

backendConfigOverride, overrideDiags := c.backendConfigOverrideBody(extraConfig, backendSchema)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
var configOverride hcl.Body
if len(*extraConfig.Items) > 0 {
var overrideDiags tfdiags.Diagnostics
configOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, backendSchema)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
}
}

opts = &BackendOpts{
BackendConfig: backendConfig,
ConfigOverride: backendConfigOverride,
ConfigOverride: configOverride,
Init: true,
ViewType: viewType,
}
Expand Down
4 changes: 3 additions & 1 deletion internal/command/init_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
Expand Down Expand Up @@ -170,7 +171,8 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
case initArgs.Cloud && rootModEarly.CloudConfig != nil:
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
case initArgs.Backend:
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
var locks *depsfile.Locks // Empty locks- this value is unused when a `backend` is used (vs. a `state_store`)
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs, locks, view)
default:
// load the previously-stored backend config
back, backDiags = c.Meta.backendFromState(ctx)
Expand Down
7 changes: 4 additions & 3 deletions internal/command/init_run_experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,10 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int
case initArgs.Cloud && rootModEarly.CloudConfig != nil:
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
case initArgs.Backend:
// TODO(SarahFrench/radeksimko) - pass information about config locks (`configLocks`) into initBackend to
// enable PSS
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
// This handles case when config contains either backend or state_store blocks.
// This is valid as either can be implementations of backend.Backend, which is what we
// obtain here.
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs, configLocks, view)
default:
// load the previously-stored backend config
back, backDiags = c.Meta.backendFromState(ctx)
Expand Down
Loading