Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a828718
Minor fixes in diagnostics
SarahFrench Jul 10, 2025
7cc8316
Rename test to make it specific to use of backend block in config
SarahFrench Oct 2, 2025
2e7b18d
Update initBackend to accept whole initArgs collection
SarahFrench Oct 2, 2025
9adf497
Only process --backend-config data, when setting up a `backend`, if t…
SarahFrench Oct 2, 2025
c13e2f1
Simplify how mock provider factories are made in tests
SarahFrench Oct 3, 2025
4f59cb1
Update mock provider's default logic to track and manage existing wor…
SarahFrench Oct 6, 2025
c287a97
Add `ProviderSchema` method to `Pluggable` structs. This allows calli…
SarahFrench Oct 6, 2025
99d6229
Add function for converting a providerreqs.Version to a hashicorp/go-…
SarahFrench Oct 2, 2025
5d42a91
Implement initial version of init new working directories using `stat…
SarahFrench Oct 6, 2025
27a73a5
Update test fixtures to match the hashicorp/test mock provider used i…
SarahFrench Oct 6, 2025
58cae04
Allow tests to obtain locks that include `testingOverrides` providers.
SarahFrench Oct 6, 2025
e27abd4
Add tests showing TF can initialize a working directory for the first…
SarahFrench Oct 6, 2025
43ac7c4
Add -create-default-workspace flag, to be used to disable creating th…
SarahFrench Oct 6, 2025
d6c3b4e
Allow reattached providers to be used during init for PSS
SarahFrench Oct 6, 2025
9e649b9
Rename variable to `backendHash` so relation to `backend` is clearer
SarahFrench Oct 6, 2025
3e5fb58
Allow `(m *Meta) Backend` to return warning diagnostics
SarahFrench Oct 6, 2025
d3d6ff8
Protect against nil testingOverrides in providerFactoriesFromLocks
SarahFrench Oct 7, 2025
7f07422
Add test case seeing what happens if default workspace selected, does…
SarahFrench Oct 7, 2025
2205034
Address code consistency check failure on PR
SarahFrench Oct 7, 2025
14f7468
Refactor use of mock in test that's experiencing EOF error...
SarahFrench Oct 7, 2025
00e6890
Remove test that requires test to supply input for user prompt
SarahFrench Oct 8, 2025
94b3586
Allow -create-default-workspace to be used regardless of whether inpu…
SarahFrench Oct 10, 2025
6d235aa
Add TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable
SarahFrench Oct 10, 2025
93bfc82
Responses to feedback, including making testStdinPipe helper log deta…
SarahFrench Oct 10, 2025
ce8c27c
Use Errorf instead
SarahFrench Oct 10, 2025
4a28cf9
Allow backend state files to not include version data when a builtin …
SarahFrench Oct 13, 2025
e79ea11
Add clarifying comment about re-attached providers when finding the m…
SarahFrench Oct 13, 2025
8d4279e
Report that the default workspace was created to the view
SarahFrench Oct 13, 2025
c32a903
Refactor: use error comparison via `errors.Is` to identify when no wo…
SarahFrench Oct 13, 2025
d89b964
Move handling of TF_ENABLE_PLUGGABLE_STATE_STORAGE into init's ParseI…
SarahFrench Oct 14, 2025
3d38b07
Validate that PSS-related flags can only be used when experiments are…
SarahFrench Oct 14, 2025
8ee5c73
Slight rewording of output message about default workspace
SarahFrench Oct 14, 2025
9c7012b
Update test to assert new output about default workspace
SarahFrench Oct 14, 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
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
67 changes: 67 additions & 0 deletions internal/backend/pluggable/pluggable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,70 @@ func TestPluggable_DeleteWorkspace(t *testing.T) {
t.Fatalf("expected error %q but got: %q", wantError, err)
}
}

func TestPluggable_ProviderSchema(t *testing.T) {
t.Run("Returns the expected provider schema", func(t *testing.T) {
mock := &testing_provider.MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"custom_attr": {Type: cty.String, Optional: true},
},
},
},
},
}
p, err := NewPluggable(mock, "foobar")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

// Calling code will need to case to Pluggable after using NewPluggable,
// so we do something similar in this test
var providerSchema *configschema.Block
if pluggable, ok := p.(*Pluggable); ok {
providerSchema = pluggable.ProviderSchema()
}

if !mock.GetProviderSchemaCalled {
t.Fatal("expected ProviderSchema to call the GetProviderSchema RPC")
}
if providerSchema == nil {
t.Fatal("ProviderSchema returned an unexpected nil schema")
}
if val := providerSchema.Attributes["custom_attr"]; val == nil {
t.Fatalf("expected the returned schema to include an attr called %q, but it was missing. Schema contains attrs: %v",
"custom_attr",
slices.Sorted(maps.Keys(providerSchema.Attributes)))
}
})

t.Run("Returns a nil schema when the provider has an empty schema", func(t *testing.T) {
mock := &testing_provider.MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
// empty schema
},
},
}
p, err := NewPluggable(mock, "foobar")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

// Calling code will need to case to Pluggable after using NewPluggable,
// so we do something similar in this test
var providerSchema *configschema.Block
if pluggable, ok := p.(*Pluggable); ok {
providerSchema = pluggable.ProviderSchema()
}

if !mock.GetProviderSchemaCalled {
t.Fatal("expected ProviderSchema to call the GetProviderSchema RPC")
}
if providerSchema != nil {
t.Fatalf("expected ProviderSchema to return a nil schema but got: %#v", providerSchema)
}
})
}
13 changes: 13 additions & 0 deletions internal/command/arguments/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package arguments

import (
"os"
"time"

"github.com/hashicorp/terraform/internal/tfdiags"
Expand Down Expand Up @@ -78,6 +79,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,6 +116,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 All @@ -123,6 +129,13 @@ func ParseInit(args []string) (*Init, tfdiags.Diagnostics) {
))
}

if v := os.Getenv("TF_SKIP_CREATE_DEFAULT_WORKSPACE"); v != "" {
// If TF_SKIP_CREATE_DEFAULT_WORKSPACE is set it will override
// a -create-default-workspace=true flag that's set explicitly,
// as that's indistinguishable from the default value being used.
init.CreateDefaultWorkspace = false
}

if init.MigrateState && init.Json {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
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
5 changes: 4 additions & 1 deletion internal/command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,10 @@ func testStdinPipe(t *testing.T, src io.Reader) func() {
// Copy the data from the reader to the pipe
go func() {
defer w.Close()
io.Copy(w, src)
_, err := io.Copy(w, src)
if err != nil {
t.Errorf("error when copying data from testStdinPipe reader argument to stdin: %s", err)
}
}()

return func() {
Expand Down
39 changes: 23 additions & 16 deletions internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ 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, configLocks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, initArgs *arguments.Init, configLocks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
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 @@ -195,7 +195,7 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext

// If overrides supplied by -backend-config CLI flag, process them
var configOverride hcl.Body
if !extraConfig.Empty() {
if !initArgs.BackendConfig.Empty() {
// We need to launch an instance of the provider to get the config of the state store for processing any overrides.
provider, err := factory()
defer provider.Close() // Stop the child process once we're done with it here.
Expand Down Expand Up @@ -238,19 +238,21 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext

// Handle any overrides supplied via -backend-config CLI flags
var overrideDiags tfdiags.Diagnostics
configOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, stateStoreSchema.Body)
configOverride, overrideDiags = c.backendConfigOverrideBody(initArgs.BackendConfig, stateStoreSchema.Body)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
}
}

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

case root.Backend != nil:
Expand Down Expand Up @@ -286,17 +288,22 @@ 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
// If overrides supplied by -backend-config CLI flag, process them
var configOverride hcl.Body
if !initArgs.BackendConfig.Empty() {
var overrideDiags tfdiags.Diagnostics
configOverride, overrideDiags = c.backendConfigOverrideBody(initArgs.BackendConfig, backendSchema)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
}
}

opts = &BackendOpts{
BackendConfig: backendConfig,
ConfigOverride: backendConfigOverride,
ConfigOverride: configOverride,
Init: true,
ViewType: viewType,
ViewType: initArgs.ViewType,
}

default:
Expand All @@ -305,7 +312,7 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext
// If the user supplied a -backend-config on the CLI but no backend
// block was found in the configuration, it's likely - but not
// necessarily - a mistake. Return a warning.
if !extraConfig.Empty() {
if !initArgs.BackendConfig.Empty() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Missing backend configuration",
Expand All @@ -328,7 +335,7 @@ the backend configuration is present and valid.

opts = &BackendOpts{
Init: true,
ViewType: viewType,
ViewType: initArgs.ViewType,
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/command/init_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
// initBackend has new parameters that aren't relevant to the original (unpluggable) version of the init command logic here.
// So for this version of the init command, we pass in empty locks intentionally.
emptyLocks := depsfile.NewLocks()
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, emptyLocks, view)
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs, emptyLocks, view)
default:
// load the previously-stored backend config
back, backDiags = c.Meta.backendFromState(ctx)
Expand Down
2 changes: 1 addition & 1 deletion internal/command/init_run_experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ 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:
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, configLocks, view)
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