diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go index 6b2cb008d0..a340ac5b7c 100644 --- a/internal/command/arguments/init.go +++ b/internal/command/arguments/init.go @@ -83,6 +83,13 @@ type Init struct { // CreateDefaultWorkspace indicates whether the default workspace should be created by // Terraform when initializing a state store for the first time. CreateDefaultWorkspace bool + + // SafeInitWithPluggableStateStore indicates whether the user has opted into the process of downloading and approving + // a new provider binary to use for pluggable state storage. + // When false and `init` detects that a provider for PSS needs to be downloaded, `init` will return early and prompt the user to re-run with `-safe init`. + // When true and `init` detects that a provider for PSS needs to be downloaded then the user will experience a new UX. + // Details of the new UX depending on whether Terraform is being run in automation or not. + SafeInitWithPluggableStateStore bool } // ParseInit processes CLI arguments, returning an Init value and errors. @@ -117,6 +124,7 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti 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") + cmdFlags.BoolVar(&init.SafeInitWithPluggableStateStore, "safe-init", false, `Enable the "safe init" workflow when downloading a provider binary for use with pluggable state storage.`) // 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") @@ -158,6 +166,15 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti "Terraform cannot use the -create-default-workspace flag (or TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable) unless experiments are enabled.", )) } + if init.SafeInitWithPluggableStateStore { + // 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 -safe-init flag without experiments enabled", + "Terraform cannot use the -safe-init flag unless experiments are enabled.", + )) + } } else { // Errors using flags despite experiments being enabled. if !init.CreateDefaultWorkspace && !init.EnablePssExperiment { @@ -167,6 +184,38 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti "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 init.SafeInitWithPluggableStateStore && !init.EnablePssExperiment { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot use -safe-init flag unless the pluggable state storage experiment is enabled", + "Terraform cannot use the -safe-init flag unless you also supply the -enable-pluggable-state-storage-experiment flag (or set the TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable).", + )) + } + } + + // Manage all flag interactions with -safe-init + if init.SafeInitWithPluggableStateStore { + if !init.Backend { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "The -safe-init and -backend=false options are mutually-exclusive", + "Terraform cannot use the -safe-init flag to influence backend initialization if backend initialization is due to be skipped.", + )) + } + if len(init.PluginPath) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "The -safe-init and -plugin-dir options are mutually-exclusive", + "Providers sourced through -plugin-dir have already been vetted by the user, so -safe-init is unnecessary. Please re-run the command without the -safe-init flag.", + )) + } + if init.Lockfile == "readonly" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + `The -safe-init and -lockfile=readonly options are mutually-exclusive`, + "The -safe-init flag is intended to help when first downloading or upgrading a provider to use for state storage, and in those scenarios the lockfile cannot be treated as read-only.", + )) + } } if init.MigrateState && init.Json { diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go index 53205da52a..9e2aef375d 100644 --- a/internal/command/arguments/init_test.go +++ b/internal/command/arguments/init_test.go @@ -40,19 +40,22 @@ func TestParseInit_basicValid(t *testing.T) { FlagName: "-backend-config", Items: &flagNameValue, }, - Vars: &Vars{}, - InputEnabled: true, - CompactWarnings: false, - TargetFlags: nil, - CreateDefaultWorkspace: true, + Vars: &Vars{}, + InputEnabled: true, + CompactWarnings: false, + TargetFlags: nil, + CreateDefaultWorkspace: true, + SafeInitWithPluggableStateStore: false, }, }, "setting multiple options": { - []string{"-backend=false", "-force-copy=true", + []string{ + "-backend=false", "-force-copy=true", "-from-module=./main-dir", "-json", "-get=false", "-lock=false", "-lock-timeout=10s", "-reconfigure=true", "-upgrade=true", "-lockfile=readonly", "-compact-warnings=true", - "-ignore-remote-version=true", "-test-directory=./test-dir"}, + "-ignore-remote-version=true", "-test-directory=./test-dir", + }, &Init{ FromModule: "./main-dir", Lockfile: "readonly", @@ -156,6 +159,21 @@ func TestParseInit_invalid(t *testing.T) { wantErr: "The -migrate-state and -reconfigure options are mutually-exclusive.", wantViewType: ViewHuman, }, + "with both -safe-init and -backend=false options set": { + args: []string{"-safe-init", "-backend=false"}, + wantErr: "The -safe-init and -backend=false options are mutually-exclusive", + wantViewType: ViewHuman, + }, + "with both -safe-init and -plugin-dir options set": { + args: []string{"-safe-init", "-plugin-dir=./my/path/to/dir"}, + wantErr: "The -safe-init and -plugin-dir options are mutually-exclusive", + wantViewType: ViewHuman, + }, + "with both -safe-init and -lockfile=readonly options set": { + args: []string{"-safe-init", `-lockfile=readonly`}, + wantErr: "The -safe-init and -lockfile=readonly options are mutually-exclusive", + wantViewType: ViewHuman, + }, } for name, tc := range testCases { @@ -218,6 +236,16 @@ func TestParseInit_experimentalFlags(t *testing.T) { experimentsEnabled: true, wantErr: "Cannot use -create-default-workspace=false flag unless the pluggable state storage experiment is enabled", }, + "error: -safe-init and experiments are disabled": { + args: []string{"-safe-init"}, + experimentsEnabled: false, + wantErr: "Cannot use -safe-init flag without experiments enabled: Terraform cannot use the -safe-init flag unless experiments are enabled.", + }, + "error: -safe-init used without -enable-pluggable-state-storage-experiment": { + args: []string{"-safe-init"}, + experimentsEnabled: true, + wantErr: "Cannot use -safe-init flag unless the pluggable state storage experiment is enabled", + }, } for name, tc := range testCases {