Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
155 changes: 155 additions & 0 deletions internal/command/e2etest/pluggable_state_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package e2etest

import (
"bytes"
"fmt"
"os"
"path"
Expand All @@ -12,8 +13,12 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"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/states"
"github.com/hashicorp/terraform/internal/states/statefile"
)

// Tests using `terraform workspace` commands in combination with pluggable state storage.
Expand Down Expand Up @@ -350,3 +355,153 @@ greeting = "hello world"

// TODO(SarahFrench/radeksimko): Show plan file: terraform show <path to plan file>
}

// 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)
}
fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "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")
}

//// 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.
}
27 changes: 27 additions & 0 deletions internal/command/e2etest/providers_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,33 @@ func TestProvidersSchema(t *testing.T) {
}
}
}
},
"state_store_schemas" : {
"simple6_fs": {
"version":0,
"block": {
"attributes": {
"workspace_dir": {
"type":"string",
"description":"The directory where state files will be created. When unset the value will default to terraform.tfstate.d","description_kind":"plain","optional":true}
},
"description_kind":"plain"
}
},
"simple6_inmem": {
"version": 0,
"block": {
"attributes": {
"lock_id": {
"type": "string",
"description": "initializes the state in a locked configuration",
"description_kind": "plain",
"optional": true
}
},
"description_kind":"plain"
}
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions internal/command/jsonprovider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Provider struct {
Functions map[string]*jsonfunction.FunctionSignature `json:"functions,omitempty"`
ResourceIdentitySchemas map[string]*IdentitySchema `json:"resource_identity_schemas,omitempty"`
ActionSchemas map[string]*ActionSchema `json:"action_schemas,omitempty"`
StateStoreSchemas map[string]*Schema `json:"state_store_schemas,omitempty"`
}

func newProviders() *Providers {
Expand Down Expand Up @@ -69,6 +70,7 @@ func marshalProvider(tps providers.ProviderSchema) *Provider {
Functions: jsonfunction.MarshalProviderFunctions(tps.Functions),
ResourceIdentitySchemas: marshalIdentitySchemas(tps.ResourceTypes),
ActionSchemas: marshalActionSchemas(tps.Actions),
StateStoreSchemas: marshalSchemas(tps.StateStores),
}

// List resource schemas are nested under a "config" block, so we need to
Expand Down
2 changes: 2 additions & 0 deletions internal/command/jsonprovider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestMarshalProvider(t *testing.T) {
ResourceIdentitySchemas: map[string]*IdentitySchema{},
ListResourceSchemas: map[string]*Schema{},
ActionSchemas: map[string]*ActionSchema{},
StateStoreSchemas: map[string]*Schema{},
},
},
{
Expand Down Expand Up @@ -250,6 +251,7 @@ func TestMarshalProvider(t *testing.T) {
},
},
},
StateStoreSchemas: map[string]*Schema{},
},
},
}
Expand Down
Loading