mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-03 20:50:59 -05:00
List implementation for simple provider and e2e tests (#37297)
This commit is contained in:
parent
677e5ea276
commit
870bf99593
10 changed files with 330 additions and 20 deletions
164
internal/command/e2etest/terraform_query_test.go
Normal file
164
internal/command/e2etest/terraform_query_test.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package e2etest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/hashicorp/terraform/internal/e2e"
|
||||
"github.com/hashicorp/terraform/internal/grpcwrap"
|
||||
tfplugin5 "github.com/hashicorp/terraform/internal/plugin"
|
||||
tfplugin "github.com/hashicorp/terraform/internal/plugin6"
|
||||
simple "github.com/hashicorp/terraform/internal/provider-simple-v6"
|
||||
proto5 "github.com/hashicorp/terraform/internal/tfplugin5"
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
)
|
||||
|
||||
func TestUnmanagedQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
protocolVersion int
|
||||
}{
|
||||
{
|
||||
name: "proto6",
|
||||
protocolVersion: 6,
|
||||
},
|
||||
{
|
||||
name: "proto5",
|
||||
protocolVersion: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
os.Setenv(e2e.TestExperimentFlag, "true")
|
||||
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
|
||||
|
||||
fixturePath := filepath.Join("testdata", "query-provider")
|
||||
tf := e2e.NewBinary(t, terraformBin, fixturePath)
|
||||
|
||||
reattachCh := make(chan *plugin.ReattachConfig)
|
||||
closeCh := make(chan struct{})
|
||||
|
||||
var provider interface {
|
||||
ListResourceCalled() bool
|
||||
}
|
||||
var versionedPlugins map[int]plugin.PluginSet
|
||||
|
||||
// Configure provider and plugins based on protocol version
|
||||
if tc.protocolVersion == 6 {
|
||||
provider6 := &providerServer{
|
||||
ProviderServer: grpcwrap.Provider6(simple.Provider()),
|
||||
}
|
||||
provider = provider6
|
||||
versionedPlugins = map[int]plugin.PluginSet{
|
||||
6: {
|
||||
"provider": &tfplugin.GRPCProviderPlugin{
|
||||
GRPCProvider: func() proto.ProviderServer {
|
||||
return provider6
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
provider5 := &providerServer5{
|
||||
ProviderServer: grpcwrap.Provider(simple.Provider()),
|
||||
}
|
||||
provider = provider5
|
||||
versionedPlugins = map[int]plugin.PluginSet{
|
||||
5: {
|
||||
"provider": &tfplugin5.GRPCProviderPlugin{
|
||||
GRPCProvider: func() proto5.ProviderServer {
|
||||
return provider5
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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: versionedPlugins,
|
||||
})
|
||||
config := <-reattachCh
|
||||
if config == nil {
|
||||
t.Fatalf("no reattach config received")
|
||||
}
|
||||
reattachStr, err := json.Marshal(map[string]reattachConfig{
|
||||
"hashicorp/test": {
|
||||
Protocol: string(config.Protocol),
|
||||
ProtocolVersion: tc.protocolVersion,
|
||||
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))
|
||||
|
||||
//// INIT
|
||||
stdout, stderr, err := tf.Run("init")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
|
||||
}
|
||||
|
||||
// Make sure we didn't download the binary
|
||||
if strings.Contains(stdout, "Installing hashicorp/test 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", "test")) {
|
||||
t.Errorf("test provider binary found in .terraform dir")
|
||||
}
|
||||
|
||||
//// QUERY
|
||||
stdout, stderr, err = tf.Run("query")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected query error: %s\nstderr:\n%s", err, stderr)
|
||||
}
|
||||
|
||||
if !provider.ListResourceCalled() {
|
||||
t.Error("ListResource not called on un-managed provider")
|
||||
}
|
||||
|
||||
// The output should contain the expected resource data. (using regex so that the number of whitespace characters doesn't matter)
|
||||
regex := regexp.MustCompile(`(?m)^list\.simple_resource\.test\s+id=static_id\s+static_display_name$`)
|
||||
if !regex.MatchString(stdout) {
|
||||
t.Errorf("expected resource data not found in output:\n%s", stdout)
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-closeCh
|
||||
})
|
||||
}
|
||||
}
|
||||
10
internal/command/e2etest/testdata/query-provider/main.tf
vendored
Normal file
10
internal/command/e2etest/testdata/query-provider/main.tf
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
simple = {
|
||||
source = "hashicorp/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "simple_resource" "test" {
|
||||
}
|
||||
7
internal/command/e2etest/testdata/query-provider/main.tfquery.hcl
vendored
Normal file
7
internal/command/e2etest/testdata/query-provider/main.tfquery.hcl
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
list "simple_resource" "test" {
|
||||
provider = simple
|
||||
include_resource = true
|
||||
config {
|
||||
value = "dynamic_value"
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ type providerServer struct {
|
|||
proto.ProviderServer
|
||||
planResourceChangeCalled bool
|
||||
applyResourceChangeCalled bool
|
||||
listResourceCalled bool
|
||||
}
|
||||
|
||||
func (p *providerServer) PlanResourceChange(ctx context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) {
|
||||
|
|
@ -70,6 +71,14 @@ func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.App
|
|||
return p.ProviderServer.ApplyResourceChange(ctx, req)
|
||||
}
|
||||
|
||||
func (p *providerServer) ListResource(req *proto.ListResource_Request, res proto.Provider_ListResourceServer) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
p.listResourceCalled = true
|
||||
return p.ProviderServer.ListResource(req, res)
|
||||
}
|
||||
|
||||
func (p *providerServer) PlanResourceChangeCalled() bool {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
|
@ -96,11 +105,19 @@ func (p *providerServer) ResetApplyResourceChangeCalled() {
|
|||
p.applyResourceChangeCalled = false
|
||||
}
|
||||
|
||||
func (p *providerServer) ListResourceCalled() bool {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
return p.listResourceCalled
|
||||
}
|
||||
|
||||
type providerServer5 struct {
|
||||
sync.Mutex
|
||||
proto5.ProviderServer
|
||||
planResourceChangeCalled bool
|
||||
applyResourceChangeCalled bool
|
||||
listResourceCalled bool
|
||||
}
|
||||
|
||||
func (p *providerServer5) PlanResourceChange(ctx context.Context, req *proto5.PlanResourceChange_Request) (*proto5.PlanResourceChange_Response, error) {
|
||||
|
|
@ -120,6 +137,14 @@ func (p *providerServer5) ApplyResourceChange(ctx context.Context, req *proto5.A
|
|||
return p.ProviderServer.ApplyResourceChange(ctx, req)
|
||||
}
|
||||
|
||||
func (p *providerServer5) ListResource(req *proto5.ListResource_Request, res proto5.Provider_ListResourceServer) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
p.listResourceCalled = true
|
||||
return p.ProviderServer.ListResource(req, res)
|
||||
}
|
||||
|
||||
func (p *providerServer5) PlanResourceChangeCalled() bool {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
|
@ -146,6 +171,13 @@ func (p *providerServer5) ResetApplyResourceChangeCalled() {
|
|||
p.applyResourceChangeCalled = false
|
||||
}
|
||||
|
||||
func (p *providerServer5) ListResourceCalled() bool {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
return p.listResourceCalled
|
||||
}
|
||||
|
||||
func TestUnmanagedSeparatePlan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
)
|
||||
|
||||
var (
|
||||
// TestExperimentFlag is the name of the environment variable that
|
||||
// can be set to built a terraform binary with experimental features enabled.
|
||||
// Any value besides "false" or an empty string will enable the feature.
|
||||
TestExperimentFlag = "TF_TEST_EXPERIMENTS"
|
||||
)
|
||||
|
||||
// Type binary represents the combination of a compiled binary
|
||||
// and a temporary working directory to run it in.
|
||||
type binary struct {
|
||||
|
|
@ -249,11 +256,12 @@ func GoBuild(pkgPath, tmpPrefix string) string {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
"go", "build",
|
||||
"-o", tmpFilename,
|
||||
pkgPath,
|
||||
)
|
||||
args := []string{"build", "-o", tmpFilename}
|
||||
if exp := os.Getenv(TestExperimentFlag); exp != "" && exp != "false" {
|
||||
args = append(args, "-ldflags", "-X 'main.experimentsAllowed=yes'")
|
||||
}
|
||||
args = append(args, pkgPath)
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
|
|
|
|||
|
|
@ -794,10 +794,10 @@ func (p *provider) ListResource(req *tfplugin5.ListResource_Request, res tfplugi
|
|||
|
||||
for iter := data.ElementIterator(); iter.Next(); {
|
||||
_, item := iter.Element()
|
||||
state := item.GetAttr("state")
|
||||
var stateVal *tfplugin5.DynamicValue
|
||||
var err error
|
||||
if !state.IsNull() {
|
||||
if item.Type().HasAttribute("state") {
|
||||
state := item.GetAttr("state")
|
||||
var err error
|
||||
stateVal, err = encodeDynamicValue(state, resourceSchema.Body.ImpliedType())
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "failed to encode list resource item state: %v", err)
|
||||
|
|
|
|||
|
|
@ -849,10 +849,10 @@ func (p *provider6) ListResource(req *tfplugin6.ListResource_Request, res tfplug
|
|||
|
||||
for iter := data.ElementIterator(); iter.Next(); {
|
||||
_, item := iter.Element()
|
||||
state := item.GetAttr("state")
|
||||
var stateVal *tfplugin6.DynamicValue
|
||||
var err error
|
||||
if !state.IsNull() {
|
||||
if item.Type().HasAttribute("state") {
|
||||
state := item.GetAttr("state")
|
||||
var err error
|
||||
stateVal, err = encodeDynamicValue6(state, resourceSchema.Body.ImpliedType())
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "failed to encode list resource item state: %v", err)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@ func Provider() providers.Interface {
|
|||
},
|
||||
},
|
||||
},
|
||||
Identity: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
}
|
||||
|
||||
return simple{
|
||||
|
|
@ -51,6 +60,18 @@ func Provider() providers.Interface {
|
|||
EphemeralResourceTypes: map[string]providers.Schema{
|
||||
"simple_resource": simpleResource,
|
||||
},
|
||||
ListResourceTypes: map[string]providers.Schema{
|
||||
"simple_resource": {
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Optional: true,
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServerCapabilities: providers.ServerCapabilities{
|
||||
PlanDestroy: true,
|
||||
GetProviderSchemaOptional: true,
|
||||
|
|
@ -251,10 +272,34 @@ func (s simple) CallFunction(req providers.CallFunctionRequest) (resp providers.
|
|||
return resp
|
||||
}
|
||||
|
||||
func (s simple) ListResource(req providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
// Our schema doesn't include any list resource types, so it should be
|
||||
// impossible to get in here.
|
||||
panic("ListResource on provider that didn't declare any list resource types")
|
||||
func (s simple) ListResource(req providers.ListResourceRequest) (resp providers.ListResourceResponse) {
|
||||
vals := make([]cty.Value, 0)
|
||||
|
||||
staticVal := cty.StringVal("static_value")
|
||||
m := req.Config.AsValueMap()
|
||||
if val, ok := m["value"]; ok && val != cty.NilVal {
|
||||
staticVal = val
|
||||
}
|
||||
|
||||
obj := map[string]cty.Value{
|
||||
"display_name": cty.StringVal("static_display_name"),
|
||||
"identity": cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("static_id"),
|
||||
}),
|
||||
}
|
||||
if req.IncludeResourceObject {
|
||||
obj["state"] = cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("static_id"),
|
||||
"value": staticVal,
|
||||
})
|
||||
}
|
||||
vals = append(vals, cty.ObjectVal(obj))
|
||||
|
||||
resp.Result = cty.ObjectVal(map[string]cty.Value{
|
||||
"data": cty.TupleVal(vals),
|
||||
"config": req.Config,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s simple) ValidateStateStoreConfig(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@ func Provider() providers.Interface {
|
|||
},
|
||||
},
|
||||
},
|
||||
Identity: &configschema.Object{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
}
|
||||
|
||||
return simple{
|
||||
|
|
@ -49,6 +58,18 @@ func Provider() providers.Interface {
|
|||
EphemeralResourceTypes: map[string]providers.Schema{
|
||||
"simple_resource": simpleResource,
|
||||
},
|
||||
ListResourceTypes: map[string]providers.Schema{
|
||||
"simple_resource": {
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"value": {
|
||||
Optional: true,
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServerCapabilities: providers.ServerCapabilities{
|
||||
PlanDestroy: true,
|
||||
},
|
||||
|
|
@ -211,10 +232,34 @@ func (s simple) CallFunction(req providers.CallFunctionRequest) (resp providers.
|
|||
panic("CallFunction on provider that didn't declare any functions")
|
||||
}
|
||||
|
||||
func (s simple) ListResource(req providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
// Our schema doesn't include any list resource types, so it should be
|
||||
// impossible to get in here.
|
||||
panic("ListResource on provider that didn't declare any list resource types")
|
||||
func (s simple) ListResource(req providers.ListResourceRequest) (resp providers.ListResourceResponse) {
|
||||
vals := make([]cty.Value, 0)
|
||||
|
||||
staticVal := cty.StringVal("static_value")
|
||||
m := req.Config.AsValueMap()
|
||||
if val, ok := m["value"]; ok && val != cty.NilVal {
|
||||
staticVal = val
|
||||
}
|
||||
|
||||
obj := map[string]cty.Value{
|
||||
"display_name": cty.StringVal("static_display_name"),
|
||||
"identity": cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("static_id"),
|
||||
}),
|
||||
}
|
||||
if req.IncludeResourceObject {
|
||||
obj["state"] = cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("static_id"),
|
||||
"value": staticVal,
|
||||
})
|
||||
}
|
||||
vals = append(vals, cty.ObjectVal(obj))
|
||||
|
||||
resp.Result = cty.ObjectVal(map[string]cty.Value{
|
||||
"data": cty.TupleVal(vals),
|
||||
"config": req.Config,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s simple) ValidateStateStoreConfig(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import (
|
|||
)
|
||||
|
||||
func TestContext2Plan_queryList(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
mainConfig string
|
||||
|
|
|
|||
Loading…
Reference in a new issue