List implementation for simple provider and e2e tests (#37297)

This commit is contained in:
Samsondeen 2025-07-09 12:19:26 +02:00 committed by GitHub
parent 677e5ea276
commit 870bf99593
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 330 additions and 20 deletions

View 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
})
}
}

View file

@ -0,0 +1,10 @@
terraform {
required_providers {
simple = {
source = "hashicorp/test"
}
}
}
resource "simple_resource" "test" {
}

View file

@ -0,0 +1,7 @@
list "simple_resource" "test" {
provider = simple
include_resource = true
config {
value = "dynamic_value"
}
}

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -23,7 +23,6 @@ import (
)
func TestContext2Plan_queryList(t *testing.T) {
cases := []struct {
name string
mainConfig string