Skip to content
8 changes: 4 additions & 4 deletions internal/command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,17 +210,17 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args *
))
return nil, diags
}
if plan.Backend == nil {

if plan.Backend == nil && plan.StateStore == nil {
// Should never happen; always indicates a bug in the creation of the plan file
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to read plan from plan file",
"The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file.",
"The given plan file does not have either a valid backend or state store configuration. This is a bug in the Terraform command that generated this plan file.",
))
return nil, diags
}
// TODO: Update BackendForLocalPlan to use state storage, and plan to be able to contain State Store config details
be, beDiags = c.BackendForLocalPlan(*plan.Backend)
be, beDiags = c.BackendForLocalPlan(plan)
} else {

// Load the backend
Expand Down
92 changes: 92 additions & 0 deletions internal/command/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/cli"
version "github.com/hashicorp/go-version"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/internal/addrs"
Expand Down Expand Up @@ -701,6 +703,96 @@ func TestApply_plan(t *testing.T) {
}
}

// Test the ability to apply a plan file with a state store.
//
// The state store's details (provider, config, etc) are supplied by the plan,
// which allows this test to not use any configuration.
func TestApply_plan_stateStore(t *testing.T) {
// Disable test mode so input would be asked
test = false
defer func() { test = true }()

// Set some default reader/writers for the inputs
defaultInputReader = new(bytes.Buffer)
defaultInputWriter = new(bytes.Buffer)

// Create the plan file that includes a state store
ver := version.Must(version.NewVersion("1.2.3"))
providerCfg := cty.ObjectVal(map[string]cty.Value{
"region": cty.StringVal("spain"),
})
providerCfgRaw, err := plans.NewDynamicValue(providerCfg, providerCfg.Type())
if err != nil {
t.Fatal(err)
}
storeCfg := cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("foobar"),
})
storeCfgRaw, err := plans.NewDynamicValue(storeCfg, storeCfg.Type())
if err != nil {
t.Fatal(err)
}

plan := &plans.Plan{
Changes: plans.NewChangesSrc(),

// We'll default to the fake plan being both applyable and complete,
// since that's what most tests expect. Tests can override these
// back to false again afterwards if they need to.
Applyable: true,
Complete: true,

StateStore: &plans.StateStore{
Type: "test_store",
Provider: &plans.Provider{
Version: ver,
Source: &tfaddr.Provider{
Hostname: tfaddr.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
},
Config: providerCfgRaw,
},
Config: storeCfgRaw,
Workspace: "default",
},
}

// Create a plan file on disk
//
// In this process we create a plan file describing the creation of a test_instance.foo resource.
state := testState() // State describes
_, snap := testModuleWithSnapshot(t, "apply")
planPath := testPlanFile(t, snap, state, plan)

// Create a mock, to be used as the pluggable state store described in the planfile
mock := testStateStoreMockWithChunkNegotiation(t, 1000)
view, done := testView(t)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): providers.FactoryFixed(mock),
},
},
View: view,
},
}

args := []string{
planPath,
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}

if !mock.WriteStateBytesCalled {
t.Fatal("expected the test to write new state when applying the plan, but WriteStateBytesCalled is false on the mock provider.")
}
}

func TestApply_plan_backup(t *testing.T) {
statePath := testTempFile(t)
backupPath := testTempFile(t)
Expand Down
82 changes: 82 additions & 0 deletions internal/command/e2etest/primary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,88 @@ func TestPrimary_stateStore(t *testing.T) {
}
}

func TestPrimary_stateStore_planFile(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")
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")

fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs")
tf := e2e.NewBinary(t, terraformBin, fixturePath)

// 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 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 {
t.Fatal(err)
}
if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil {
t.Fatal(err)
}

//// INIT
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Terraform created an empty state file for the default workspace") {
t.Errorf("notice about creating the default workspace is missing from init output:\n%s", stdout)
}

//// PLAN
planFile := "testplan"
_, stderr, err = tf.Run("plan", "-out="+planFile, "-no-color")
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}

//// APPLY
stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color", planFile)
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}

if !strings.Contains(stdout, "Resources: 1 added, 0 changed, 0 destroyed") {
t.Errorf("incorrect apply tally; want 1 added:\n%s", stdout)
}

// Check the statefile saved by the fs state store.
path := "states/default/terraform.tfstate"
f, err := tf.OpenFile(path)
if err != nil {
t.Fatalf("unexpected error opening state file %s: %s\nstderr:\n%s", path, err, stderr)
}
defer f.Close()

stateFile, err := statefile.Read(f)
if err != nil {
t.Fatalf("unexpected error reading statefile %s: %s\nstderr:\n%s", path, err, stderr)
}

r := stateFile.State.RootModule().Resources
if len(r) != 1 {
t.Fatalf("expected state to include one resource, but got %d", len(r))
}
if _, ok := r["terraform_data.my-data"]; !ok {
t.Fatalf("expected state to include terraform_data.my-data but it's missing")
}
}

func TestPrimary_stateStore_inMem(t *testing.T) {
if !canRunGoBuild {
// We're running in a separate-build-then-run context, so we can't
Expand Down
Loading