mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
26 KiB
682 lines
26 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package e2etest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-plugin"
|
|
"github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/e2e"
|
|
"github.com/hashicorp/terraform/internal/getproviders"
|
|
"github.com/hashicorp/terraform/internal/grpcwrap"
|
|
tfplugin "github.com/hashicorp/terraform/internal/plugin6"
|
|
simple "github.com/hashicorp/terraform/internal/provider-simple-v6"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
|
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
|
)
|
|
|
|
// Test that users can do the full init-plan-apply workflow with pluggable state storage
|
|
// when the state storage provider is reattached/unmanaged by Terraform.
|
|
// As well as ensuring that the state store can be initialised ok, this tests that
|
|
// the state store's details can be stored in the plan file despite the fact it's reattached.
|
|
func TestPrimary_stateStore_unmanaged_separatePlan(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")
|
|
}
|
|
|
|
fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs")
|
|
|
|
t.Setenv(e2e.TestExperimentFlag, "true")
|
|
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
|
|
tf := e2e.NewBinary(t, terraformBin, fixturePath)
|
|
|
|
reattachCh := make(chan *plugin.ReattachConfig)
|
|
closeCh := make(chan struct{})
|
|
provider := &providerServer{
|
|
ProviderServer: grpcwrap.Provider6(simple.Provider()),
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go plugin.Serve(&plugin.ServeConfig{
|
|
Logger: hclog.New(&hclog.LoggerOptions{
|
|
Name: "plugintest",
|
|
Level: hclog.Trace,
|
|
Output: io.Discard,
|
|
}),
|
|
Test: &plugin.ServeTestConfig{
|
|
Context: ctx,
|
|
ReattachConfigCh: reattachCh,
|
|
CloseCh: closeCh,
|
|
},
|
|
GRPCServer: plugin.DefaultGRPCServer,
|
|
VersionedPlugins: map[int]plugin.PluginSet{
|
|
6: {
|
|
"provider": &tfplugin.GRPCProviderPlugin{
|
|
GRPCProvider: func() proto.ProviderServer {
|
|
return provider
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
config := <-reattachCh
|
|
if config == nil {
|
|
t.Fatalf("no reattach config received")
|
|
}
|
|
reattachStr, err := json.Marshal(map[string]reattachConfig{
|
|
"hashicorp/simple6": {
|
|
Protocol: string(config.Protocol),
|
|
ProtocolVersion: 6,
|
|
Pid: config.Pid,
|
|
Test: true,
|
|
Addr: reattachConfigAddr{
|
|
Network: config.Addr.Network(),
|
|
String: config.Addr.String(),
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
|
|
|
|
// Required for the local state files to be written to the temp directory,
|
|
// instead of the e2e directory in the repo.
|
|
t.Chdir(tf.WorkDir())
|
|
|
|
//// INIT
|
|
t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1")
|
|
stdout, stderr, err := tf.Run("init")
|
|
if err != nil {
|
|
t.Fatalf("unexpected init error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
|
|
}
|
|
if !provider.ReadStateBytesCalled() {
|
|
t.Error("ReadStateBytes not called on un-managed provider")
|
|
}
|
|
provider.ResetReadStateBytesCalled()
|
|
provider.ResetWriteStateBytesCalled()
|
|
|
|
// Make sure we didn't download the binary
|
|
if strings.Contains(stdout, "Installing hashicorp/simple6 v") {
|
|
t.Errorf("test provider download message is present in init output:\n%s", stdout)
|
|
}
|
|
if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "simple6")) {
|
|
t.Errorf("test provider binary found in .terraform dir")
|
|
}
|
|
|
|
//// PLAN
|
|
stdout, stderr, err = tf.Run("plan", "-out=tfplan")
|
|
if err != nil {
|
|
t.Fatalf("unexpected plan error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
|
|
}
|
|
if !provider.ReadStateBytesCalled() {
|
|
t.Error("ReadStateBytes not called on un-managed provider")
|
|
}
|
|
if provider.WriteStateBytesCalled() {
|
|
t.Error("WriteStateBytes should not be called on un-managed provider during plan")
|
|
}
|
|
provider.ResetReadStateBytesCalled()
|
|
provider.ResetWriteStateBytesCalled()
|
|
|
|
//// APPLY
|
|
stdout, stderr, err = tf.Run("apply", "tfplan")
|
|
if err != nil {
|
|
t.Fatalf("unexpected apply error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
|
|
}
|
|
if !provider.ReadStateBytesCalled() {
|
|
t.Error("ReadStateBytes not called on un-managed provider")
|
|
}
|
|
if !provider.WriteStateBytesCalled() {
|
|
t.Error("WriteStateBytes not called on un-managed provider")
|
|
}
|
|
provider.ResetReadStateBytesCalled()
|
|
provider.ResetWriteStateBytesCalled()
|
|
|
|
// Check the apply process has made a state file as expected.
|
|
stateFilePath := filepath.Join("states", "default", "terraform.tfstate")
|
|
if !tf.FileExists(stateFilePath) {
|
|
t.Fatalf("state file not found at expected path: %s", filepath.Join(tf.WorkDir(), stateFilePath))
|
|
}
|
|
|
|
//// DESTROY
|
|
stdout, stderr, err = tf.Run("destroy", "-auto-approve")
|
|
if err != nil {
|
|
t.Fatalf("unexpected destroy error: %s\nstderr:\n%s\nstdout:\n%s", err, stderr, stdout)
|
|
}
|
|
|
|
cancel()
|
|
<-closeCh
|
|
}
|
|
|
|
// 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
|
|
// 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)
|
|
workspaceDirName := "states" // See workspace_dir value in the configuration
|
|
|
|
// 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()
|
|
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(fsMirrorPath, platform, "terraform-provider-simple6")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
//// Init
|
|
_, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
_, err = os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate"))
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal("expected default workspace's state file to not exist, but it exists")
|
|
}
|
|
|
|
//// Create Workspace: terraform workspace new
|
|
newWorkspace := "foobar"
|
|
stdout, stderr, err := tf.Run("workspace", "new", newWorkspace, "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg := fmt.Sprintf("Created and switched to workspace %q!", newWorkspace)
|
|
if !strings.Contains(stdout, expectedMsg) {
|
|
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
|
|
}
|
|
fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, newWorkspace, "terraform.tfstate"))
|
|
if err != nil {
|
|
t.Fatalf("failed to open %s workspace's state file: %s", newWorkspace, err)
|
|
}
|
|
if fi.Size() == 0 {
|
|
t.Fatalf("%s workspace's state file should not have size 0 bytes", newWorkspace)
|
|
}
|
|
|
|
//// List Workspaces: : terraform workspace list
|
|
stdout, stderr, err = tf.Run("workspace", "list", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
if !strings.Contains(stdout, newWorkspace) {
|
|
t.Errorf("unexpected output, expected the new %q workspace to be listed present, but it's missing. Got:\n%s", newWorkspace, stdout)
|
|
}
|
|
|
|
//// Select Workspace: terraform workspace select
|
|
selectedWorkspace := "default"
|
|
stdout, stderr, err = tf.Run("workspace", "select", "-or-create", selectedWorkspace, "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg = fmt.Sprintf("Created and switched to workspace %q!", selectedWorkspace)
|
|
if !strings.Contains(stdout, expectedMsg) {
|
|
t.Errorf("unexpected output, expected %s, but got:\n%s", expectedMsg, stdout)
|
|
}
|
|
|
|
//// Show Workspace: terraform workspace show
|
|
stdout, stderr, err = tf.Run("workspace", "show", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg = fmt.Sprintf("%s\n", selectedWorkspace)
|
|
if stdout != expectedMsg {
|
|
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
|
|
}
|
|
|
|
//// Delete Workspace: terraform workspace delete
|
|
stdout, stderr, err = tf.Run("workspace", "delete", newWorkspace, "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg = fmt.Sprintf("Deleted workspace %q!\n", newWorkspace)
|
|
if stdout != expectedMsg {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Tests using the `terraform output` command in combination with pluggable state storage:
|
|
// > `terraform output`
|
|
// > `terraform output <name>`
|
|
func TestPrimary_stateStore_outputCmd(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()
|
|
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 all outputs: terraform output
|
|
stdout, stderr, err := tf.Run("output", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg := "greeting = \"hello world\"\n" // See the test fixture files
|
|
if stdout != expectedMsg {
|
|
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
|
|
}
|
|
|
|
//// View a specific output: terraform output <name>
|
|
outputName := "greeting"
|
|
stdout, stderr, err = tf.Run("output", outputName, "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg = "\"hello world\"\n" // Only the value is outputted, no name present
|
|
if stdout != expectedMsg {
|
|
t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout)
|
|
}
|
|
}
|
|
|
|
// Tests using the `terraform show` command in combination with pluggable state storage
|
|
// > `terraform show`
|
|
// > `terraform show <path-to-state-file>`
|
|
// > `terraform show <path-to-plan-file>`
|
|
func TestPrimary_stateStore_showCmd(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()
|
|
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")
|
|
}
|
|
|
|
//// Show state: terraform state
|
|
stdout, stderr, err := tf.Run("show", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg := `# terraform_data.my-data:
|
|
resource "terraform_data" "my-data" {
|
|
id = "d71fb368-2ba1-fb4c-5bd9-6a2b7f05d60c"
|
|
input = "hello world"
|
|
output = "hello world"
|
|
}
|
|
|
|
|
|
Outputs:
|
|
|
|
greeting = "hello world"
|
|
` // See the test fixture folder's state file
|
|
|
|
if diff := cmp.Diff(stdout, expectedMsg); diff != "" {
|
|
t.Errorf("wrong result, diff:\n%s", diff)
|
|
}
|
|
|
|
//// Show state: terraform show <path to state file>
|
|
path := fmt.Sprintf("./%s/%s/terraform.tfstate", workspaceDirName, defaultStateId)
|
|
stdout, stderr, err = tf.Run("show", path, "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
if diff := cmp.Diff(stdout, expectedMsg); diff != "" {
|
|
t.Errorf("wrong result, diff:\n%s", diff)
|
|
}
|
|
|
|
//// Show state: terraform show <path to plan file>
|
|
|
|
// 1. Create a plan file via plan command
|
|
newOutput := `output "replacement" {
|
|
value = resource.terraform_data.my-data.output
|
|
}`
|
|
if err := os.WriteFile(filepath.Join(tf.WorkDir(), "outputs.tf"), []byte(newOutput), 0644); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
planFile := "tfplan"
|
|
stdout, stderr, err = tf.Run("plan", fmt.Sprintf("-out=%s", planFile), "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg = "Changes to Outputs"
|
|
if !strings.Contains(stdout, expectedMsg) {
|
|
t.Errorf("wrong result, expected the plan command to create a plan file but that hasn't happened, got:\n%s",
|
|
stdout,
|
|
)
|
|
}
|
|
|
|
// 2. Inspect plan file
|
|
stdout, stderr, err = tf.Run("show", planFile, "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
expectedMsg = `
|
|
Changes to Outputs:
|
|
- greeting = "hello world" -> null
|
|
+ replacement = "hello world"
|
|
|
|
You can apply this plan to save these new output values to the Terraform
|
|
state, without changing any real infrastructure.
|
|
`
|
|
if diff := cmp.Diff(stdout, expectedMsg); diff != "" {
|
|
t.Errorf("wrong result, diff:\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// Tests using the `terraform provider` subcommands in combination with pluggable state storage:
|
|
// > `terraform providers`
|
|
// > `terraform providers schema`
|
|
//
|
|
// Commands `terraform providers locks` and `terraform providers mirror` aren't tested as they
|
|
// don't interact with the backend.
|
|
//
|
|
// The test `TestProvidersSchema` has test coverage showing that state store schemas are present
|
|
// in the command's outputs. _This_ test is intended to assert that the command is able to read and use
|
|
// state via a state store ok, and is able to detect providers required only by the state.
|
|
func TestPrimary_stateStore_providerCmds(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)
|
|
workspaceDirName := "states" // See workspace_dir value in the configuration
|
|
|
|
// Add a state file describing a resource from the simple (v5) provider, so
|
|
// we can test that the state is read and used to get all the provider schemas
|
|
fakeState := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "simple_resource",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"bar"}`),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("simple"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
fakeStateFile := &statefile.File{
|
|
Lineage: "boop",
|
|
Serial: 4,
|
|
TerraformVersion: version.Must(version.NewVersion("1.0.0")),
|
|
State: fakeState,
|
|
}
|
|
var fakeStateBuf bytes.Buffer
|
|
err := statefile.WriteForTest(fakeStateFile, &fakeStateBuf)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
fakeStateBytes := fakeStateBuf.Bytes()
|
|
|
|
if err := os.MkdirAll(tf.Path(workspaceDirName, "default"), os.ModePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(tf.Path(workspaceDirName, "default", "terraform.tfstate"), fakeStateBytes, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// 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 will be used for PSS.
|
|
// The simple (v5) provider is also built, as that provider will be present in the state and therefore
|
|
// needed for creating the schema output.
|
|
simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
|
|
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
|
|
|
|
simpleProvider := filepath.Join(tf.WorkDir(), "terraform-provider-simple")
|
|
simpleProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", simpleProvider)
|
|
|
|
// Move the provider binaries into a directory that we will point terraform
|
|
// to using the -plugin-dir cli flag.
|
|
platform := getproviders.CurrentPlatform.String()
|
|
fsMirrorPathV6 := "cache/registry.terraform.io/hashicorp/simple6/0.0.1/"
|
|
if err := os.MkdirAll(tf.Path(fsMirrorPathV6, platform), os.ModePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Rename(simple6ProviderExe, tf.Path(fsMirrorPathV6, platform, "terraform-provider-simple6")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fsMirrorPathV5 := "cache/registry.terraform.io/hashicorp/simple/0.0.1/"
|
|
if err := os.MkdirAll(tf.Path(fsMirrorPathV5, platform), os.ModePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Rename(simpleProviderExe, tf.Path(fsMirrorPathV5, platform, "terraform-provider-simple")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
//// Init
|
|
_, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
// Note: The default state was already created earlier in the test
|
|
|
|
//// Providers: `terraform providers`
|
|
stdout, stderr, err := tf.Run("providers", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
|
|
// We expect the command to be able to use the state store to
|
|
// detect providers that come from only the state.
|
|
expectedMsgs := []string{
|
|
"Providers required by configuration:",
|
|
"provider[registry.terraform.io/hashicorp/simple6]",
|
|
"provider[terraform.io/builtin/terraform]",
|
|
"Providers required by state:",
|
|
"provider[registry.terraform.io/hashicorp/simple]",
|
|
}
|
|
for _, msg := range expectedMsgs {
|
|
if !strings.Contains(stdout, msg) {
|
|
t.Errorf("unexpected output, expected %q, but got:\n%s", msg, stdout)
|
|
}
|
|
}
|
|
|
|
//// Provider schemas: `terraform providers schema`
|
|
stdout, stderr, err = tf.Run("providers", "schema", "-json", "-no-color")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
|
|
}
|
|
|
|
expectedMsgs = []string{
|
|
`"registry.terraform.io/hashicorp/simple6"`, // provider used for PSS
|
|
`"terraform.io/builtin/terraform"`, // provider used for resources
|
|
`"registry.terraform.io/hashicorp/simple"`, // provider present only in the state
|
|
}
|
|
for _, msg := range expectedMsgs {
|
|
if !strings.Contains(stdout, msg) {
|
|
t.Errorf("unexpected output, expected %q, but got:\n%s", msg, stdout)
|
|
}
|
|
}
|
|
|
|
// More thorough checking of the JSON output is in `TestProvidersSchema`.
|
|
// This test just asserts that `terraform providers schema` can read state
|
|
// via the state store, and therefore detects all 3 providers needed for the output.
|
|
}
|