Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0548a42
test: Add E2E tests for `state list` and `state show` commands
SarahFrench Nov 11, 2025
4a3ed2b
test: Update `mockPluggableStateStorageProvider` to log a warning dur…
SarahFrench Nov 21, 2025
1670449
test: Update `mockPluggableStateStorageProvider` helper to include a …
SarahFrench Nov 21, 2025
2874ba3
test: Add command-level test for `state list` showing integration wit…
SarahFrench Nov 21, 2025
fb446c0
test: Add command-level test for `state show` showing integration wit…
SarahFrench Nov 21, 2025
db2b662
test: Add command-level test for `state pull` showing integration wit…
SarahFrench Nov 21, 2025
ac088bf
test: Add command-level test for `state identities` showing integrati…
SarahFrench Nov 21, 2025
85b4a83
test: Add command-level test for `state rm` showing integration with …
SarahFrench Nov 24, 2025
cebf4a5
test: Add command-level test for `state mv` showing integration with …
SarahFrench Nov 24, 2025
38abe77
test: Add command-level test for `state push` showing integration wit…
SarahFrench Nov 24, 2025
6567d88
test: Add command-level test for `state replace-provider` showing int…
SarahFrench Nov 24, 2025
deda57e
test: Change shared test fixture to not be named after a specific com…
SarahFrench Nov 24, 2025
a6b6c1a
test: Update test to use shared test fixture
SarahFrench Nov 24, 2025
ea418bc
test: Remove redundant test fixture
SarahFrench Nov 24, 2025
8248ba0
Merge branch 'main' into pss/pss-with-state-commands
SarahFrench Nov 26, 2025
bd6f62e
fix: Re-add logic for setting chunk size in the context of E2E tests …
SarahFrench Nov 26, 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
4 changes: 2 additions & 2 deletions internal/command/autocomplete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestMetaCompletePredictWorkspaceName(t *testing.T) {
t.Chdir(td)

// Set up pluggable state store provider mock
mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
// Mock the existence of workspaces
mockProvider.MockStates = map[string]interface{}{
"default": true,
Expand Down Expand Up @@ -89,7 +89,7 @@ func TestMetaCompletePredictWorkspaceName(t *testing.T) {
t.Chdir(td)

// Set up pluggable state store provider mock
mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
// No workspaces exist in the mock
mockProvider.MockStates = map[string]interface{}{}
mockProviderAddress := addrs.NewDefaultProvider("test")
Expand Down
78 changes: 78 additions & 0 deletions internal/command/e2etest/pluggable_state_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/e2e"
"github.com/hashicorp/terraform/internal/getproviders"
)

// Tests using `terraform workspace` commands in combination with pluggable state storage.
func TestPrimary_stateStore_workspaceCmd(t *testing.T) {
if !canRunGoBuild {
// We're running in a separate-build-then-run context, so we can't
Expand Down Expand Up @@ -119,3 +121,79 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
}
}

// Tests using `terraform state` subcommands in combination with pluggable state storage:
// > `terraform state show`
// > `terraform state list`
func TestPrimary_stateStore_stateCmds(t *testing.T) {

if !canRunGoBuild {
// We're running in a separate-build-then-run context, so we can't
// currently execute this test which depends on being able to build
// new executable at runtime.
//
// (See the comment on canRunGoBuild's declaration for more information.)
t.Skip("can't run without building a new provider executable")
}

t.Setenv(e2e.TestExperimentFlag, "true")
tfBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")

fixturePath := filepath.Join("testdata", "initialized-directory-with-state-store-fs")
tf := e2e.NewBinary(t, tfBin, fixturePath)

workspaceDirName := "states" // see test fixture value for workspace_dir

// In order to test integration with PSS we need a provider plugin implementing a state store.
// Here will build the simple6 (built with protocol v6) provider, which implements PSS.
simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)

// Move the provider binaries into the correct directory .terraform/providers/ directory
// that will contain provider binaries in an initialized working directory.
platform := getproviders.CurrentPlatform.String()
if err := os.MkdirAll(tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform), os.ModePerm); err != nil {
t.Fatal(err)
}
if err := os.Rename(simple6ProviderExe, tf.Path(".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil {
t.Fatal(err)
}

// Assert that the test starts with the default state present from test fixtures
defaultStateId := "default"
fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, defaultStateId, "terraform.tfstate"))
if err != nil {
t.Fatalf("failed to open default workspace's state file: %s", err)
}
if fi.Size() == 0 {
t.Fatal("default workspace's state file should not have size 0 bytes")
}

//// List State: terraform state list
expectedResourceAddr := "terraform_data.my-data"
stdout, stderr, err := tf.Run("state", "list", "-no-color")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}
expectedMsg := expectedResourceAddr + "\n" // This is the only resource instance in the test fixture state
if stdout != expectedMsg {
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
}

//// Show State: terraform state show
stdout, stderr, err = tf.Run("state", "show", expectedResourceAddr, "-no-color")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}
// show displays the state for the specified resource
expectedMsg = `# terraform_data.my-data:
resource "terraform_data" "my-data" {
id = "d71fb368-2ba1-fb4c-5bd9-6a2b7f05d60c"
input = "hello world"
output = "hello world"
}
`
if diff := cmp.Diff(stdout, expectedMsg); diff != "" {
t.Errorf("wrong result, diff:\n%s", diff)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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: the test will still need to build the appropriate binary and put it in a folder here that's named appropriately for the platform that is running the test.

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": 3,
"terraform_version": "1.15.0",
"state_store": {
"type": "simple6_fs",
"provider": {
"version": "0.0.1",
"source": "registry.terraform.io/hashicorp/simple6",
"config": {}
},
"config": {
"workspace_dir": "states"
},
"hash": 3942813381
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
terraform {
required_providers {
simple6 = {
source = "registry.terraform.io/hashicorp/simple6"
}
}

state_store "simple6_fs" {
provider "simple6" {}

workspace_dir = "states"
}
}

variable "name" {
default = "world"
}

resource "terraform_data" "my-data" {
input = "hello ${var.name}"
}

output "greeting" {
value = resource.terraform_data.my-data.output
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"version": 4,
"terraform_version": "1.15.0",
"serial": 1,
"lineage": "9e13d881-e480-7a63-d47a-b4f5224e6743",
"outputs": {
"greeting": {
"value": "hello world",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "terraform_data",
"name": "my-data",
"provider": "provider[\"terraform.io/builtin/terraform\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "d71fb368-2ba1-fb4c-5bd9-6a2b7f05d60c",
"input": {
"value": "hello world",
"type": "string"
},
"output": {
"value": "hello world",
"type": "string"
},
"triggers_replace": null
},
"sensitive_attributes": [],
"identity_schema_version": 0
}
]
}
],
"check_results": null
}
55 changes: 35 additions & 20 deletions internal/command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3238,7 +3238,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
testCopyDir(t, testFixturePath("init-with-state-store"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
// The test fixture config has no version constraints, so the latest version will
Expand Down Expand Up @@ -3325,7 +3325,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
testCopyDir(t, testFixturePath("init-with-state-store"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.0.0"},
Expand Down Expand Up @@ -3374,7 +3374,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
testCopyDir(t, testFixturePath("init-with-state-store"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.0.0"},
Expand Down Expand Up @@ -3429,7 +3429,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
customWorkspace := "my-custom-workspace"
t.Setenv(WorkspaceNameEnvVar, customWorkspace)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.0.0"},
Expand Down Expand Up @@ -3492,7 +3492,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) {
testCopyDir(t, testFixturePath("init-with-state-store"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProvider.GetStatesResponse = &providers.GetStatesResponse{
States: []string{
"foobar1",
Expand Down Expand Up @@ -3583,7 +3583,7 @@ func TestInit_stateStore_configUnchanged(t *testing.T) {
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
// If the working directory was previously initialized successfully then at least
// one workspace is guaranteed to exist when a user is re-running init with no config
// changes since last init. So this test says `default` exists.
Expand Down Expand Up @@ -3670,11 +3670,11 @@ func TestInit_stateStore_configChanges(t *testing.T) {
testCopyDir(t, testFixturePath("state-store-changed/store-config"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)

// The previous init implied by this test scenario would have created this.
mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}}
mockProvider.MockStates = map[string]interface{}{"default": true}
mockProvider.MockStates = map[string]interface{}{"default": []byte(`{"version": 4,"terraform_version":"1.15.0","serial": 1,"lineage": "","outputs": {},"resources": [],"checks":[]}`)}

mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
Expand Down Expand Up @@ -3758,7 +3758,7 @@ func TestInit_stateStore_configChanges(t *testing.T) {
testCopyDir(t, testFixturePath("state-store-changed/store-config"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace.
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
Expand Down Expand Up @@ -3808,7 +3808,7 @@ func TestInit_stateStore_configChanges(t *testing.T) {
testCopyDir(t, testFixturePath("state-store-changed/provider-config"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace.
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
Expand Down Expand Up @@ -3857,7 +3857,7 @@ func TestInit_stateStore_configChanges(t *testing.T) {
testCopyDir(t, testFixturePath("state-store-changed/state-store-type"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
storeName := "test_store"
otherStoreName := "test_otherstore"
// Make the provider report that it contains a 2nd storage implementation with the above name
Expand Down Expand Up @@ -3909,11 +3909,11 @@ func TestInit_stateStore_configChanges(t *testing.T) {
testCopyDir(t, testFixturePath("state-store-changed/provider-used"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace.

// Make a mock that implies its name is test2 based on returned schemas
mockProvider2 := mockPluggableStateStorageProvider()
mockProvider2 := mockPluggableStateStorageProvider(t)
mockProvider2.GetProviderSchemaResponse.StateStores["test2_store"] = mockProvider.GetProviderSchemaResponse.StateStores["test_store"]
delete(mockProvider2.GetProviderSchemaResponse.StateStores, "test_store")

Expand Down Expand Up @@ -3974,7 +3974,7 @@ func TestInit_stateStore_providerUpgrade(t *testing.T) {
testCopyDir(t, testFixturePath("state-store-changed/provider-upgraded"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
mockProviderAddress := addrs.NewDefaultProvider("test")
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3", "9.9.9"}, // 1.2.3 is the version used in the backend state file, 9.9.9 is the version being upgraded to
Expand Down Expand Up @@ -4023,7 +4023,7 @@ func TestInit_stateStore_unset(t *testing.T) {
testCopyDir(t, testFixturePath("init-state-store"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
storeName := "test_store"
otherStoreName := "test_otherstore"
// Make the provider report that it contains a 2nd storage implementation with the above name
Expand Down Expand Up @@ -4121,7 +4121,7 @@ func TestInit_stateStore_unset_withoutProviderRequirements(t *testing.T) {
testCopyDir(t, testFixturePath("init-state-store"), td)
t.Chdir(td)

mockProvider := mockPluggableStateStorageProvider()
mockProvider := mockPluggableStateStorageProvider(t)
storeName := "test_store"
otherStoreName := "test_otherstore"
// Make the provider report that it contains a 2nd storage implementation with the above name
Expand Down Expand Up @@ -4355,7 +4355,7 @@ func expectedPackageInstallPath(name, version string, exe bool) string {
))
}

func mockPluggableStateStorageProvider() *testing_provider.MockProvider {
func mockPluggableStateStorageProvider(t *testing.T) *testing_provider.MockProvider {
// Create a mock provider to use for PSS
// Get mock provider factory to be used during init
//
Expand All @@ -4371,8 +4371,17 @@ func mockPluggableStateStorageProvider() *testing_provider.MockProvider {
},
},
},
DataSources: map[string]providers.Schema{},
ResourceTypes: map[string]providers.Schema{},
DataSources: map[string]providers.Schema{},
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"input": {Type: cty.String, Optional: true},
"id": {Type: cty.String, Computed: true},
},
},
},
},
ListResourceTypes: map[string]providers.Schema{},
StateStores: map[string]providers.Schema{
pssName: {
Expand Down Expand Up @@ -4412,8 +4421,14 @@ func mockPluggableStateStorageProvider() *testing_provider.MockProvider {
mock.ReadStateBytesFn = func(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse {
state := []byte{}
if v, exist := mock.MockStates[req.StateId]; exist {
if s, ok := v.([]byte); ok {
s, ok := v.([]byte)
if ok {
state = s
} else {
// Test setup is incorrect if this happens
t.Logf("Warning: The mock provider is set up to return state bytes from the MockStates map, but the mock encountered a value that wasn't a slice of bytes: %#v",
mock.MockStates,
)
}
}
return providers.ReadStateBytesResponse{
Expand Down
Loading