Skip to content
55 changes: 55 additions & 0 deletions internal/command/arguments/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package arguments
import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/hashicorp/terraform/internal/tfdiags"
Expand Down Expand Up @@ -76,6 +77,8 @@ type Init struct {

Args []string

StateStoreProviderLockFile 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
Expand Down Expand Up @@ -113,6 +116,7 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti
cmdFlags.BoolVar(&init.Json, "json", false, "json")
cmdFlags.Var(&init.BackendConfig, "backend-config", "")
cmdFlags.Var(&init.PluginPath, "plugin-dir", "plugin directory")
cmdFlags.StringVar(&init.StateStoreProviderLockFile, "state-provider-lock-file", "", "path to the dependency lock file to use when establishing trust in the provider used for state storage. This is only used when input is disabled. Otherwise, users will be shown interactive prompts instead. Defaults to the working directory's .terraform.lock.hcl file.")

// 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 @@ -138,6 +142,57 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti
"Terraform cannot use the -enable-pluggable-state-storage-experiment flag (or TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable) unless experiments are enabled.",
))
}
if init.StateStoreProviderLockFile != "" {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use -state-provider-lock-file flag without experiments enabled",
"Terraform cannot use the -state-provider-lock-file flag unless experiments are enabled.",
))
}
}

if !init.EnablePssExperiment && init.StateStoreProviderLockFile != "" {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use -state-provider-lock-file flag unless the pluggable state storage experiment is enabled",
"Terraform cannot use the -state-provider-lock-file flag unless the pluggable state storage experiment is enabled. Add the -enable-pluggable-state-storage-experiment flag to your command.",
))
}

if init.StateStoreProviderLockFile != "" {
if init.InputEnabled {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use -state-provider-lock-file flag when input is enabled",
"The -state-provider-lock-file flag is only intended to be used when input is disabled. Either remove the flag or add -input=false to your command.",
))
}
if init.Upgrade {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"The -state-provider-lock-file and -upgrade options are mutually-exclusive",
"The -upgrade flag causes Terraform to ignore prior dependency locks, so it cannot be used in conjunction with the -state-provider-lock-file flag. Remove one flag and try again.",
))
}

// Validate that the path uses the expected file name: .terraform.lock.hcl
srcFilename := filepath.Base(init.StateStoreProviderLockFile)
if srcFilename != lockFileName {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid -state-provider-lock-file value",
fmt.Sprintf("Expected lock file name to be %s, got: %s", lockFileName, srcFilename),
))
}

// Validate that the file exists
if _, err := os.Stat(init.StateStoreProviderLockFile); os.IsNotExist(err) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Lock file supplied by -state-provider-lock-file does not exist",
fmt.Sprintf("Terraform cannot find the dependency lock file at %s. Please ensure the file exists and the path is correct.", init.StateStoreProviderLockFile),
))
}
}

if init.MigrateState && init.Json {
Expand Down
254 changes: 239 additions & 15 deletions internal/command/arguments/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package arguments

import (
"fmt"
"os"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -40,10 +42,12 @@ 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,
EnablePssExperiment: false,
StateStoreProviderLockFile: "",
},
},
"setting multiple options": {
Expand Down Expand Up @@ -74,11 +78,13 @@ 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,
EnablePssExperiment: false,
StateStoreProviderLockFile: "",
},
},
"with cloud option": {
Expand All @@ -103,11 +109,13 @@ 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"},
EnablePssExperiment: false,
StateStoreProviderLockFile: "",
},
},
}
Expand All @@ -129,6 +137,212 @@ func TestParseInit_basicValid(t *testing.T) {
}
}

func TestParseInit_stateProviderLockFile(t *testing.T) {
td := t.TempDir()
t.Chdir(td)

// Create a dummy lock file to test with
lockFileName := ".terraform.lock.hcl"
if err := os.WriteFile(lockFileName, []byte("content doesn't matter!"), 0600); err != nil {
t.Fatalf("unable to create dependency lock file: %v", err)
}

// Create an existing file with an incorrect file name
invalidLockFileName := "foobar.lock.hcl"
if err := os.WriteFile(invalidLockFileName, []byte("content doesn't matter!"), 0600); err != nil {
t.Fatalf("unable to create dependency lock file: %v", err)
}

var flagNameValue []FlagNameValue
testCases := map[string]struct {
args []string
expectErr bool
want *Init
}{
"error when -state-provider-lock-file is used alongside -input=true": {
[]string{"-state-provider-lock-file=foobar.lock.hcl", "-upgrade", "-input=true", "-enable-pluggable-state-storage-experiment"},
true,
&Init{
FromModule: "",
Lockfile: "",
TestsDirectory: "tests",
ViewType: ViewHuman,
Backend: true,
Cloud: true,
Get: true,
ForceInitCopy: false,
StateLock: true,
StateLockTimeout: 0,
Reconfigure: false,
MigrateState: false,
Upgrade: true,
Json: false,
IgnoreRemoteVersion: false,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: true,
Args: []string{},
CompactWarnings: false,
TargetFlags: nil,
EnablePssExperiment: true,
StateStoreProviderLockFile: "foobar.lock.hcl",
},
},
"error when -state-provider-lock-file is used alongside -upgrade": {
[]string{"-state-provider-lock-file=foobar.lock.hcl", "-upgrade", "-input=false", "-enable-pluggable-state-storage-experiment"},
true,
&Init{
FromModule: "",
Lockfile: "",
TestsDirectory: "tests",
ViewType: ViewHuman,
Backend: true,
Cloud: true,
Get: true,
ForceInitCopy: false,
StateLock: true,
StateLockTimeout: 0,
Reconfigure: false,
MigrateState: false,
Upgrade: true,
Json: false,
IgnoreRemoteVersion: false,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: false,
Args: []string{},
CompactWarnings: false,
TargetFlags: nil,
EnablePssExperiment: true,
StateStoreProviderLockFile: "foobar.lock.hcl",
},
},
"error when -state-provider-lock-file references a non-existent file": {
[]string{"-state-provider-lock-file=nonexistent.lock.hcl", "-input=false", "-enable-pluggable-state-storage-experiment"},
true,
&Init{
FromModule: "",
Lockfile: "",
TestsDirectory: "tests",
ViewType: ViewHuman,
Backend: true,
Cloud: true,
Get: true,
ForceInitCopy: false,
StateLock: true,
StateLockTimeout: 0,
Reconfigure: false,
MigrateState: false,
Upgrade: false,
Json: false,
IgnoreRemoteVersion: false,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: false,
Args: []string{},
CompactWarnings: false,
TargetFlags: nil,
EnablePssExperiment: true,
StateStoreProviderLockFile: "nonexistent.lock.hcl",
},
},
"error when -state-provider-lock-file references a file that is not called .terraform.lock.hcl": {
[]string{fmt.Sprintf("-state-provider-lock-file=%s", invalidLockFileName), "-input=false", "-enable-pluggable-state-storage-experiment"},
true,
&Init{
FromModule: "",
Lockfile: "",
TestsDirectory: "tests",
ViewType: ViewHuman,
Backend: true,
Cloud: true,
Get: true,
ForceInitCopy: false,
StateLock: true,
StateLockTimeout: 0,
Reconfigure: false,
MigrateState: false,
Upgrade: false,
Json: false,
IgnoreRemoteVersion: false,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: false,
Args: []string{},
CompactWarnings: false,
TargetFlags: nil,
EnablePssExperiment: true,
StateStoreProviderLockFile: invalidLockFileName,
},
},
"valid when -state-provider-lock-file references a file that exists": {
[]string{fmt.Sprintf("-state-provider-lock-file=%s", lockFileName), "-input=false", "-enable-pluggable-state-storage-experiment"},
false,
&Init{
FromModule: "",
Lockfile: "",
TestsDirectory: "tests",
ViewType: ViewHuman,
Backend: true,
Cloud: true,
Get: true,
ForceInitCopy: false,
StateLock: true,
StateLockTimeout: 0,
Reconfigure: false,
MigrateState: false,
Upgrade: false,
Json: false,
IgnoreRemoteVersion: false,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: false,
Args: []string{},
CompactWarnings: false,
TargetFlags: nil,
EnablePssExperiment: true,
StateStoreProviderLockFile: lockFileName,
},
},
}

cmpOpts := cmpopts.IgnoreUnexported(Vars{})

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
experimentsEnabled := true
got, diags := ParseInit(tc.args, experimentsEnabled)
if len(diags) > 0 {
if !tc.expectErr {
t.Fatalf("unexpected diags: %v", diags)
}
}
if tc.expectErr && len(diags) == 0 {
t.Fatal("expected error diags but got none")
}

if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
t.Errorf("unexpected result\n%s", diff)
}
})
}
}

func TestParseInit_invalid(t *testing.T) {
testCases := map[string]struct {
args []string
Expand Down Expand Up @@ -173,7 +387,7 @@ func TestParseInit_invalid(t *testing.T) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
}
if got.ViewType != tc.wantViewType {
t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman)
t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, tc.wantViewType)
}
})
}
Expand All @@ -198,6 +412,16 @@ func TestParseInit_experimentalFlags(t *testing.T) {
experimentsEnabled: false,
wantErr: "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled",
},
"error: -state-provider-lock-file and experiments are disabled": {
args: []string{"-state-provider-lock-file=.terraform.lock.hcl"},
experimentsEnabled: false,
wantErr: "Cannot use -state-provider-lock-file flag without experiments enabled",
},
"error: -state-provider-lock-file and -enable-pluggable-state-storage-experiment isn't set": {
args: []string{"-state-provider-lock-file=.terraform.lock.hcl"},
experimentsEnabled: true,
wantErr: "Cannot use -state-provider-lock-file flag unless the pluggable state storage experiment is enabled",
},
}

for name, tc := range testCases {
Expand Down
Loading