Skip to content

Commit f88d095

Browse files
authored
PSS: Add tests showing provider commands being used with PSS (#37959)
* test: Add E2E test for using pluggable state storage with the `providers` command Note: I've excluded the `terraform providers locks` and `terraform providers mirror` commands as they don't interact with backends. * test: Add integration test for using pluggable state storage with the `providers` command * refactor: Change ioutil.ReadDir to os.ReadDir * test: Add integration test for using pluggable state storage with the `providers schema` command * feat: Allow state store schema's to be included when schemas are marshalled into JSON output * test: Assert that state stores are present in provider schemas returned from `providers schema`. * test: Update existing tests to accommodate state stores being in provider schema output * test: Update E2E test for `providers` commands to be better scoped to testing use of a state store to access and use state when generating output. This complements TestProvidersSchema that tests that state stores in a provider are reflected in the JSON representations of the schemas that the command returns. * chore: Replace `io/ioutil` with `io` in `providers schema` tests
1 parent 578766b commit f88d095

File tree

11 files changed

+519
-3
lines changed

11 files changed

+519
-3
lines changed

internal/command/e2etest/pluggable_state_store_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package e2etest
55

66
import (
7+
"bytes"
78
"fmt"
89
"os"
910
"path"
@@ -12,8 +13,12 @@ import (
1213
"testing"
1314

1415
"github.com/google/go-cmp/cmp"
16+
"github.com/hashicorp/go-version"
17+
"github.com/hashicorp/terraform/internal/addrs"
1518
"github.com/hashicorp/terraform/internal/e2e"
1619
"github.com/hashicorp/terraform/internal/getproviders"
20+
"github.com/hashicorp/terraform/internal/states"
21+
"github.com/hashicorp/terraform/internal/states/statefile"
1722
)
1823

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

351356
// TODO(SarahFrench/radeksimko): Show plan file: terraform show <path to plan file>
352357
}
358+
359+
// Tests using the `terraform provider` subcommands in combination with pluggable state storage:
360+
// > `terraform providers`
361+
// > `terraform providers schema`
362+
//
363+
// Commands `terraform providers locks` and `terraform providers mirror` aren't tested as they
364+
// don't interact with the backend.
365+
//
366+
// The test `TestProvidersSchema` has test coverage showing that state store schemas are present
367+
// in the command's outputs. _This_ test is intended to assert that the command is able to read and use
368+
// state via a state store ok, and is able to detect providers required only by the state.
369+
func TestPrimary_stateStore_providerCmds(t *testing.T) {
370+
if !canRunGoBuild {
371+
// We're running in a separate-build-then-run context, so we can't
372+
// currently execute this test which depends on being able to build
373+
// new executable at runtime.
374+
//
375+
// (See the comment on canRunGoBuild's declaration for more information.)
376+
t.Skip("can't run without building a new provider executable")
377+
}
378+
379+
t.Setenv(e2e.TestExperimentFlag, "true")
380+
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
381+
fixturePath := filepath.Join("testdata", "full-workflow-with-state-store-fs")
382+
tf := e2e.NewBinary(t, terraformBin, fixturePath)
383+
workspaceDirName := "states" // See workspace_dir value in the configuration
384+
385+
// Add a state file describing a resource from the simple (v5) provider, so
386+
// we can test that the state is read and used to get all the provider schemas
387+
fakeState := states.BuildState(func(s *states.SyncState) {
388+
s.SetResourceInstanceCurrent(
389+
addrs.Resource{
390+
Mode: addrs.ManagedResourceMode,
391+
Type: "simple_resource",
392+
Name: "foo",
393+
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
394+
&states.ResourceInstanceObjectSrc{
395+
AttrsJSON: []byte(`{"id":"bar"}`),
396+
Status: states.ObjectReady,
397+
},
398+
addrs.AbsProviderConfig{
399+
Provider: addrs.NewDefaultProvider("simple"),
400+
Module: addrs.RootModule,
401+
},
402+
)
403+
})
404+
fakeStateFile := &statefile.File{
405+
Lineage: "boop",
406+
Serial: 4,
407+
TerraformVersion: version.Must(version.NewVersion("1.0.0")),
408+
State: fakeState,
409+
}
410+
var fakeStateBuf bytes.Buffer
411+
err := statefile.WriteForTest(fakeStateFile, &fakeStateBuf)
412+
if err != nil {
413+
t.Error(err)
414+
}
415+
fakeStateBytes := fakeStateBuf.Bytes()
416+
417+
if err := os.MkdirAll(tf.Path(workspaceDirName, "default"), os.ModePerm); err != nil {
418+
t.Fatal(err)
419+
}
420+
if err := os.WriteFile(tf.Path(workspaceDirName, "default", "terraform.tfstate"), fakeStateBytes, 0644); err != nil {
421+
t.Fatal(err)
422+
}
423+
424+
// In order to test integration with PSS we need a provider plugin implementing a state store.
425+
// Here will build the simple6 (built with protocol v6) provider, which will be used for PSS.
426+
// The simple (v5) provider is also built, as that provider will be present in the state and therefore
427+
// needed for creating the schema output.
428+
simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
429+
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
430+
431+
simpleProvider := filepath.Join(tf.WorkDir(), "terraform-provider-simple")
432+
simpleProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", simpleProvider)
433+
434+
// Move the provider binaries into a directory that we will point terraform
435+
// to using the -plugin-dir cli flag.
436+
platform := getproviders.CurrentPlatform.String()
437+
fsMirrorPathV6 := "cache/registry.terraform.io/hashicorp/simple6/0.0.1/"
438+
if err := os.MkdirAll(tf.Path(fsMirrorPathV6, platform), os.ModePerm); err != nil {
439+
t.Fatal(err)
440+
}
441+
if err := os.Rename(simple6ProviderExe, tf.Path(fsMirrorPathV6, platform, "terraform-provider-simple6")); err != nil {
442+
t.Fatal(err)
443+
}
444+
445+
fsMirrorPathV5 := "cache/registry.terraform.io/hashicorp/simple/0.0.1/"
446+
if err := os.MkdirAll(tf.Path(fsMirrorPathV5, platform), os.ModePerm); err != nil {
447+
t.Fatal(err)
448+
}
449+
if err := os.Rename(simpleProviderExe, tf.Path(fsMirrorPathV5, platform, "terraform-provider-simple")); err != nil {
450+
t.Fatal(err)
451+
}
452+
453+
//// Init
454+
_, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color")
455+
if err != nil {
456+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
457+
}
458+
fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate"))
459+
if err != nil {
460+
t.Fatalf("failed to open default workspace's state file: %s", err)
461+
}
462+
if fi.Size() == 0 {
463+
t.Fatal("default workspace's state file should not have size 0 bytes")
464+
}
465+
466+
//// Providers: `terraform providers`
467+
stdout, stderr, err := tf.Run("providers", "-no-color")
468+
if err != nil {
469+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
470+
}
471+
472+
// We expect the command to be able to use the state store to
473+
// detect providers that come from only the state.
474+
expectedMsgs := []string{
475+
"Providers required by configuration:",
476+
"provider[registry.terraform.io/hashicorp/simple6]",
477+
"provider[terraform.io/builtin/terraform]",
478+
"Providers required by state:",
479+
"provider[registry.terraform.io/hashicorp/simple]",
480+
}
481+
for _, msg := range expectedMsgs {
482+
if !strings.Contains(stdout, msg) {
483+
t.Errorf("unexpected output, expected %q, but got:\n%s", msg, stdout)
484+
}
485+
}
486+
487+
//// Provider schemas: `terraform providers schema`
488+
stdout, stderr, err = tf.Run("providers", "schema", "-json", "-no-color")
489+
if err != nil {
490+
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
491+
}
492+
493+
expectedMsgs = []string{
494+
`"registry.terraform.io/hashicorp/simple6"`, // provider used for PSS
495+
`"terraform.io/builtin/terraform"`, // provider used for resources
496+
`"registry.terraform.io/hashicorp/simple"`, // provider present only in the state
497+
}
498+
for _, msg := range expectedMsgs {
499+
if !strings.Contains(stdout, msg) {
500+
t.Errorf("unexpected output, expected %q, but got:\n%s", msg, stdout)
501+
}
502+
}
503+
504+
// More thorough checking of the JSON output is in `TestProvidersSchema`.
505+
// This test just asserts that `terraform providers schema` can read state
506+
// via the state store, and therefore detects all 3 providers needed for the output.
507+
}

internal/command/e2etest/providers_schema_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,33 @@ func TestProvidersSchema(t *testing.T) {
269269
}
270270
}
271271
}
272+
},
273+
"state_store_schemas" : {
274+
"simple6_fs": {
275+
"version":0,
276+
"block": {
277+
"attributes": {
278+
"workspace_dir": {
279+
"type":"string",
280+
"description":"The directory where state files will be created. When unset the value will default to terraform.tfstate.d","description_kind":"plain","optional":true}
281+
},
282+
"description_kind":"plain"
283+
}
284+
},
285+
"simple6_inmem": {
286+
"version": 0,
287+
"block": {
288+
"attributes": {
289+
"lock_id": {
290+
"type": "string",
291+
"description": "initializes the state in a locked configuration",
292+
"description_kind": "plain",
293+
"optional": true
294+
}
295+
},
296+
"description_kind":"plain"
297+
}
298+
}
272299
}
273300
}
274301
}

internal/command/jsonprovider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type Provider struct {
3131
Functions map[string]*jsonfunction.FunctionSignature `json:"functions,omitempty"`
3232
ResourceIdentitySchemas map[string]*IdentitySchema `json:"resource_identity_schemas,omitempty"`
3333
ActionSchemas map[string]*ActionSchema `json:"action_schemas,omitempty"`
34+
StateStoreSchemas map[string]*Schema `json:"state_store_schemas,omitempty"`
3435
}
3536

3637
func newProviders() *Providers {
@@ -69,6 +70,7 @@ func marshalProvider(tps providers.ProviderSchema) *Provider {
6970
Functions: jsonfunction.MarshalProviderFunctions(tps.Functions),
7071
ResourceIdentitySchemas: marshalIdentitySchemas(tps.ResourceTypes),
7172
ActionSchemas: marshalActionSchemas(tps.Actions),
73+
StateStoreSchemas: marshalSchemas(tps.StateStores),
7274
}
7375

7476
// List resource schemas are nested under a "config" block, so we need to

internal/command/jsonprovider/provider_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func TestMarshalProvider(t *testing.T) {
3333
ResourceIdentitySchemas: map[string]*IdentitySchema{},
3434
ListResourceSchemas: map[string]*Schema{},
3535
ActionSchemas: map[string]*ActionSchema{},
36+
StateStoreSchemas: map[string]*Schema{},
3637
},
3738
},
3839
{
@@ -250,6 +251,7 @@ func TestMarshalProvider(t *testing.T) {
250251
},
251252
},
252253
},
254+
StateStoreSchemas: map[string]*Schema{},
253255
},
254256
},
255257
}

0 commit comments

Comments
 (0)