Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 7 additions & 39 deletions internal/command/arguments/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@ type Init struct {

Args []string

// The -enable-pluggable-state-storage-experiment flag is used in control flow logic in the init command.
// 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
Expand Down Expand Up @@ -118,9 +113,6 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti
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")

if err := cmdFlags.Parse(args); err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand All @@ -129,44 +121,20 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti
))
}

if v := os.Getenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE"); v != "" {
init.EnablePssExperiment = true
}

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 !experimentsEnabled {
// If experiments aren't enabled then these flags should not be used.
if init.EnablePssExperiment {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled",
"Terraform cannot use the -enable-pluggable-state-storage-experiment flag (or TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable) unless experiments are enabled.",
))
}
if !init.CreateDefaultWorkspace {
// Can only be set to false by using the flag
// and we cannot identify if -create-default-workspace=true is set explicitly.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use -create-default-workspace flag without experiments enabled",
"Terraform cannot use the -create-default-workspace flag (or TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable) unless experiments are enabled.",
))
}
} else {
// Errors using flags despite experiments being enabled.
if !init.CreateDefaultWorkspace && !init.EnablePssExperiment {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use -create-default-workspace=false flag unless the pluggable state storage experiment is enabled",
"Terraform cannot use the -create-default-workspace=false flag (or TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable) unless you also supply the -enable-pluggable-state-storage-experiment flag (or set the TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable).",
))
}
if !experimentsEnabled && !init.CreateDefaultWorkspace {
// CreateDefaultWorkspace can only be set to false by using the flag
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use -create-default-workspace flag without experiments enabled",
"Terraform cannot use the -create-default-workspace flag (or TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable) unless experiments are enabled.",
))
}

if init.MigrateState && init.Json {
Expand Down
24 changes: 0 additions & 24 deletions internal/command/arguments/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,6 @@ func TestParseInit_experimentalFlags(t *testing.T) {
wantErr string
experimentsEnabled bool
}{
"error: -enable-pluggable-state-storage-experiment and experiments are disabled": {
args: []string{"-enable-pluggable-state-storage-experiment"},
experimentsEnabled: false,
wantErr: "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled",
},
"error: TF_ENABLE_PLUGGABLE_STATE_STORAGE is set and experiments are disabled": {
envs: map[string]string{
"TF_ENABLE_PLUGGABLE_STATE_STORAGE": "1",
},
experimentsEnabled: false,
wantErr: "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled",
},
"error: -create-default-workspace=false and experiments are disabled": {
args: []string{"-create-default-workspace=false"},
experimentsEnabled: false,
Expand All @@ -206,18 +194,6 @@ func TestParseInit_experimentalFlags(t *testing.T) {
experimentsEnabled: false,
wantErr: "Cannot use -create-default-workspace flag without experiments enabled",
},
"error: -create-default-workspace=false used without -enable-pluggable-state-storage-experiment, while experiments are enabled": {
args: []string{"-create-default-workspace=false"},
experimentsEnabled: true,
wantErr: "Cannot use -create-default-workspace=false flag unless the pluggable state storage experiment is enabled",
},
"error: TF_SKIP_CREATE_DEFAULT_WORKSPACE used without -enable-pluggable-state-storage-experiment, while experiments are enabled": {
envs: map[string]string{
"TF_SKIP_CREATE_DEFAULT_WORKSPACE": "1",
},
experimentsEnabled: true,
wantErr: "Cannot use -create-default-workspace=false flag unless the pluggable state storage experiment is enabled",
},
}

for name, tc := range testCases {
Expand Down
2 changes: 1 addition & 1 deletion internal/command/e2etest/pluggable_state_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) {
}

//// Init
_, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
_, stderr, err := tf.Run("init", "-plugin-dir=cache", "-no-color")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/command/e2etest/primary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func TestPrimary_stateStore(t *testing.T) {
}

//// INIT
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
stdout, stderr, err := tf.Run("init", "-plugin-dir=cache", "-no-color")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}
Expand Down Expand Up @@ -350,7 +350,7 @@ func TestPrimary_stateStore_inMem(t *testing.T) {
//
// Note - the inmem PSS implementation means that the default workspace state created during init
// is lost as soon as the command completes.
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
stdout, stderr, err := tf.Run("init", "-plugin-dir=cache", "-no-color")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
terraform {
experiments = [pluggable_state_stores]
required_providers {
simple6 = {
source = "registry.terraform.io/hashicorp/simple6"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
terraform {
experiments = [pluggable_state_stores]
required_providers {
simple6 = {
source = "registry.terraform.io/hashicorp/simple6"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
terraform {
experiments = [pluggable_state_stores]

required_providers {
simple6 = {
source = "registry.terraform.io/hashicorp/simple6"
Expand Down
43 changes: 39 additions & 4 deletions internal/command/experimental_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,43 @@ import (

func TestInit_stateStoreBlockIsExperimental(t *testing.T) {

t.Run("init command", func(t *testing.T) {
// Create a temporary working directory with state_store in use
// When experiments are enabled, users are prompted to add experiments = [pluggable_state_stores] to their config.
t.Run("init command without `pluggable_state_stores` experiment in config", func(t *testing.T) {
// Create a temporary working directory with state_store in use but experiment not declared
td := t.TempDir()
testCopyDir(t, testFixturePath("init-with-state-store-no-experiment"), td)
t.Chdir(td)

ui := new(cli.MockUi)
view, done := testView(t)
c := &InitCommand{
Meta: Meta{
Ui: ui,
View: view,
AllowExperimentalFeatures: true,
},
}

args := []string{}
code := c.Run(args)
testOutput := done(t)
if code != 1 {
t.Fatalf("unexpected output: \n%s", testOutput.All())
}

// Check output
output := cleanString(testOutput.Stderr())
if !strings.Contains(output, `Error: Pluggable state store experiment not supported`) {
t.Fatalf("doesn't look like experiment is blocking access': %s", output)
}
if !strings.Contains(output, "opt into the \"pluggable_state_stores\" experiment using the `terraform` block's `experiments` attribute") {
t.Fatalf("expected the error to explain the need for a config change': %s", output)
}
})

// When experiments aren't enabled, the state_store block is reported as being unexpected
t.Run("init command without experiments enabled", func(t *testing.T) {
// Create a temporary working directory with state_store in use but experiment not declared
td := t.TempDir()
testCopyDir(t, testFixturePath("init-with-state-store"), td)
t.Chdir(td)
Expand Down Expand Up @@ -42,7 +77,7 @@ func TestInit_stateStoreBlockIsExperimental(t *testing.T) {
}
})

t.Run("non-init command: plan", func(t *testing.T) {
t.Run("non-init command: `plan` without experiments enabled", func(t *testing.T) {
// Create a temporary working directory with state_store in use
td := t.TempDir()
testCopyDir(t, testFixturePath("init-with-state-store"), td)
Expand Down Expand Up @@ -72,7 +107,7 @@ func TestInit_stateStoreBlockIsExperimental(t *testing.T) {
}
})

t.Run("non-init command: state list", func(t *testing.T) {
t.Run("non-init command: `state list` without experiments enabled", func(t *testing.T) {
// Create a temporary working directory with state_store in use
td := t.TempDir()
testCopyDir(t, testFixturePath("init-with-state-store"), td)
Expand Down
37 changes: 23 additions & 14 deletions internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/experiments"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/providercache"
Expand Down Expand Up @@ -55,16 +56,27 @@ func (c *InitCommand) Run(args []string) int {
return 1
}

path, err := ModulePath(initArgs.Args)
if err != nil {
diags = diags.Append(err)
view.Diagnostics(diags)
return 1
}
rootMod, rootModDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)
// We purposefully don't exit early if there are error diagnostics returned here; there are errors related to the Terraform version
// that have precedence and are detected downstream.
// We pass the configuration and diagnostic values from here into downstream code, replacing where the files are parsed there.
// This prevents the diagnostics being lost, as re-parsing the same config results in lost diagnostics.

// The else condition below invokes the original logic of the init command.
// An experimental version of the init code will be used if:
// > The user uses an experimental version of TF (alpha or built from source)
// > Either the flag -enable-pluggable-state-storage-experiment is passed to the init command.
// > Or, the environment variable TF_ENABLE_PLUGGABLE_STATE_STORAGE is set to any value.
if c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment {
// > The user uses an experimental version of TF (alpha or built from source).
// > The terraform block in the configuration lists the `pluggable_state_stores` experiment.
if c.Meta.AllowExperimentalFeatures && rootMod.ActiveExperiments.Has(experiments.PluggableStateStores) {
// TODO(SarahFrench/radeksimko): Remove forked init logic once feature is no longer experimental
return c.runPssInit(initArgs, view)
return c.runPssInit(initArgs, view, rootMod, rootModDiags)
} else {
return c.run(initArgs, view)
return c.run(initArgs, view, rootMod, rootModDiags)
}
}

Expand Down Expand Up @@ -1476,15 +1488,12 @@ Options:

-test-directory=path Set the Terraform test directory, defaults to "tests".

-enable-pluggable-state-storage-experiment [EXPERIMENTAL]
A flag to enable an alternative init command that allows use of
pluggable state storage. Only usable with experiments enabled.

-create-default-workspace [EXPERIMENTAL]
This flag must be used alongside the -enable-pluggable-state-storage-
experiment flag with experiments enabled. This flag's value defaults
to true, which allows the default workspace to be created if it does
not exist. Use -create-default-workspace=false to disable this behavior.
This flag must be used alongside naming the pluggable_state_stores
experiment in your configuration and using an experimental build of
Terraform. This flag's value defaults to true, which allows the default
workspace to be created if it does not exist.
Use -create-default-workspace=false to disable this behavior.

`
return strings.TrimSpace(helpText)
Expand Down
21 changes: 15 additions & 6 deletions 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/experiments"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
Expand All @@ -22,7 +23,7 @@ import (
"go.opentelemetry.io/otel/trace"
)

func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
func (c *InitCommand) run(initArgs *arguments.Init, view views.Init, rootModEarly *configs.Module, earlyConfDiags tfdiags.Diagnostics) int {
var diags tfdiags.Diagnostics

c.forceInitCopy = initArgs.ForceInitCopy
Expand Down Expand Up @@ -129,8 +130,14 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
return 0
}

// Load just the root module to begin backend and module initialization
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)
// If the passed root module is nil, attempt to parse it.
// At this point we load just the root module to begin backend and module initialization
//
// TODO(SarahFrench/radeksimko): Once PSS's experiment is over, remove use of arguments and
// restore parsing of config to always happen here in this code instead of calling code.
if rootModEarly == nil {
rootModEarly, earlyConfDiags = c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)
}

// There may be parsing errors in config loading but these will be shown later _after_
// checking for core version requirement errors. Not meeting the version requirement should
Expand All @@ -142,7 +149,7 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {

return 1
}
if !(c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment) && rootModEarly.StateStore != nil {
if !(c.Meta.AllowExperimentalFeatures && rootModEarly.ActiveExperiments.Has(experiments.PluggableStateStores)) && rootModEarly.StateStore != nil {
// TODO(SarahFrench/radeksimko) - remove when this feature isn't experimental.
// This approach for making the feature experimental is required
// to let us assert the feature is gated behind an experiment in tests.
Expand All @@ -152,11 +159,13 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
if !c.Meta.AllowExperimentalFeatures {
detail += " an experimental build of terraform"
}
if !initArgs.EnablePssExperiment {
if !rootModEarly.ActiveExperiments.Has(experiments.PluggableStateStores) {
if !c.Meta.AllowExperimentalFeatures {
detail += " and"
}
detail += " -enable-pluggable-state-storage-experiment flag"
detail += fmt.Sprintf(" the configuration to opt into the %q experiment using the `terraform` block's `experiments` attribute",
experiments.PluggableStateStores.Keyword(),
)
}

diags = diags.Append(earlyConfDiags)
Expand Down
12 changes: 9 additions & 3 deletions internal/command/init_run_experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
// `runPssInit` is an altered version of the logic in `run` that contains changes
// related to the PSS project. This is used by the (InitCommand.Run method only if Terraform has
// experimental features enabled.
func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int {
func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init, rootModEarly *configs.Module, earlyConfDiags tfdiags.Diagnostics) int {
var diags tfdiags.Diagnostics

c.forceInitCopy = initArgs.ForceInitCopy
Expand Down Expand Up @@ -138,8 +138,14 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int
return 0
}

// Load just the root module to begin backend and module initialization
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)
// If the passed root module is nil, attempt to parse it.
// At this point we load just the root module to begin backend and module initialization
//
// TODO(SarahFrench/radeksimko): Once PSS's experiment is over, remove use of arguments and
// restore parsing of config to always happen here in this code instead of calling code.
if rootModEarly == nil {
rootModEarly, earlyConfDiags = c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)
}
Comment on lines +144 to +148
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I don't expect a nil rootModEarly to be passed in, unless there's a future refactor that alters the calling code. Mainly I changed the code here in this way to allow the new method arguments to be used but still leave the appropriate position in the method body marked with that TODO comment.


// There may be parsing errors in config loading but these will be shown later _after_
// checking for core version requirement errors. Not meeting the version requirement should
Expand Down
Loading
Loading