Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
10c5e22
test: Add E2E tests for `state list` and `state show` commands
SarahFrench Nov 11, 2025
9aa680d
test: Update `mockPluggableStateStorageProvider` to log a warning dur…
SarahFrench Nov 21, 2025
33635cd
test: Update `mockPluggableStateStorageProvider` helper to include a …
SarahFrench Nov 21, 2025
4d94e87
test: Add command-level test for `state list` showing integration wit…
SarahFrench Nov 21, 2025
b9a1d3f
test: Add command-level test for `state show` showing integration wit…
SarahFrench Nov 21, 2025
6cb5625
test: Add command-level test for `state pull` showing integration wit…
SarahFrench Nov 21, 2025
1bdfba5
test: Add command-level test for `state identities` showing integrati…
SarahFrench Nov 21, 2025
bbfbf63
test: Add command-level test for `state rm` showing integration with …
SarahFrench Nov 24, 2025
0536d82
test: Add command-level test for `state mv` showing integration with …
SarahFrench Nov 24, 2025
4804cc3
test: Add command-level test for `state push` showing integration wit…
SarahFrench Nov 24, 2025
55bfebc
test: Add command-level test for `state replace-provider` showing int…
SarahFrench Nov 24, 2025
723c90d
test: Change shared test fixture to not be named after a specific com…
SarahFrench Nov 24, 2025
c3b7923
test: Update test to use shared test fixture
SarahFrench Nov 24, 2025
c392a6f
test: Remove redundant test fixture
SarahFrench Nov 24, 2025
101f432
fix: Re-add logic for setting chunk size in the context of E2E tests …
SarahFrench Nov 26, 2025
1b0d141
refactor: Let panic happen if there's incompatibility between mock re…
SarahFrench Dec 4, 2025
6ecb90a
test: Refactor to contain paths in reused variable, remove unnecessar…
SarahFrench Dec 4, 2025
c2d9c42
test: Remove unneeded test code
SarahFrench Dec 4, 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
85 changes: 82 additions & 3 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 All @@ -40,11 +42,11 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) {
// Move the provider binaries into a directory that we will point terraform
// to using the -plugin-dir cli flag.
platform := getproviders.CurrentPlatform.String()
hashiDir := "cache/registry.terraform.io/hashicorp/"
if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil {
fsMirrorPath := "cache/registry.terraform.io/hashicorp/simple6/0.0.1/"
if err := os.MkdirAll(tf.Path(fsMirrorPath, platform), os.ModePerm); err != nil {
t.Fatal(err)
}
if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil {
if err := os.Rename(simple6ProviderExe, tf.Path(fsMirrorPath, platform, "terraform-provider-simple6")); err != nil {
t.Fatal(err)
}

Expand Down Expand Up @@ -119,3 +121,80 @@ 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 .terraform/providers/ directory
// that will contain provider binaries in an initialized working directory.
platform := getproviders.CurrentPlatform.String()
providerCachePath := ".terraform/providers/registry.terraform.io/hashicorp/simple6/0.0.1/"
if err := os.MkdirAll(tf.Path(providerCachePath, platform), os.ModePerm); err != nil {
t.Fatal(err)
}
if err := os.Rename(simple6ProviderExe, tf.Path(providerCachePath, 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.

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
}
19 changes: 13 additions & 6 deletions internal/command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3674,7 +3674,7 @@ func TestInit_stateStore_configChanges(t *testing.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 @@ -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,9 +4421,7 @@ 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 {
state = s
}
state = v.([]byte) // If this panics, the mock has been set up with a bad MockStates value
}
return providers.ReadStateBytesResponse{
Bytes: state,
Expand Down
4 changes: 2 additions & 2 deletions internal/command/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ func (c *OutputCommand) Outputs(statePath string, view arguments.ViewType) (map[
}

// Get the state
stateStore, sDiags := b.StateMgr(env)
sMgr, sDiags := b.StateMgr(env)
if sDiags.HasErrors() {
diags = diags.Append(fmt.Errorf("Failed to load state: %s", sDiags.Err()))
return nil, diags
}

output, err := stateStore.GetRootOutputValues(ctx)
output, err := sMgr.GetRootOutputValues(ctx)
if err != nil {
return nil, diags.Append(err)
}
Expand Down
69 changes: 69 additions & 0 deletions internal/command/state_identities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
package command

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/hashicorp/cli"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states/statefile"
)

func TestStateIdentities(t *testing.T) {
Expand Down Expand Up @@ -423,3 +427,68 @@ func TestStateIdentities_modules(t *testing.T) {
})

}

func TestStateIdentities_stateStore(t *testing.T) {
// We need configuration present to force pluggable state storage to be used
td := t.TempDir()
testCopyDir(t, testFixturePath("state-store-unchanged"), td)
t.Chdir(td)

// Get a state file, that contains identity information,as bytes
state := testStateWithIdentity()
var stateBuf bytes.Buffer
if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil {
t.Fatalf("error during test setup: %s", err)
}
stateBytes := stateBuf.Bytes()

// Create a mock that contains a persisted "default" state that uses the bytes from above.
mockProvider := mockPluggableStateStorageProvider()
mockProvider.MockStates = map[string]interface{}{
"default": stateBytes,
}
mockProviderAddress := addrs.NewDefaultProvider("test")

ui := cli.NewMockUi()
c := &StateIdentitiesCommand{
Meta: Meta{
AllowExperimentalFeatures: true,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
mockProviderAddress: providers.FactoryFixed(mockProvider),
},
},
Ui: ui,
},
}

args := []string{"-json"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}

// Test that outputs were displayed
expected := `{
"test_instance.bar": {
"id": "my-bar-id"
},
"test_instance.foo": {
"id": "my-foo-id"
}
}
`
actual := ui.OutputWriter.String()

// Normalize JSON strings
var expectedJSON, actualJSON map[string]interface{}
if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil {
t.Fatalf("Failed to unmarshal expected JSON: %s", err)
}
if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil {
t.Fatalf("Failed to unmarshal actual JSON: %s", err)
}

if !reflect.DeepEqual(expectedJSON, actualJSON) {
t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual)
}
}
Loading