mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-08 16:35:25 -04:00
Schema representation of list block config and results (#37209)
This commit is contained in:
parent
b3d7dae793
commit
685ff9f192
27 changed files with 1539 additions and 282 deletions
|
|
@ -177,6 +177,9 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, allowPartial bo
|
|||
case "ephemeral":
|
||||
mode = EphemeralResourceMode
|
||||
remain = remain[1:]
|
||||
case "list":
|
||||
mode = ListResourceMode
|
||||
remain = remain[1:]
|
||||
case "resource":
|
||||
// Starting a resource address with "resource" is optional, so we'll
|
||||
// just ignore it.
|
||||
|
|
|
|||
|
|
@ -175,6 +175,10 @@ func (a *Attribute) internalValidate(name, prefix string) error {
|
|||
func (o *Object) InternalValidate() error {
|
||||
var err error
|
||||
|
||||
if o.Nesting == nestingModeInvalid {
|
||||
return fmt.Errorf("object schema nesting mode is invalid")
|
||||
}
|
||||
|
||||
for name, attrS := range o.Attributes {
|
||||
if attrS == nil {
|
||||
err = errors.Join(err, fmt.Errorf("%s: attribute schema is nil", name))
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ func TestObjectInternalValidate(t *testing.T) {
|
|||
Errs []string
|
||||
}{
|
||||
"empty": {
|
||||
&Object{},
|
||||
&Object{Nesting: NestingSingle},
|
||||
[]string{},
|
||||
},
|
||||
"valid": {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ func (c *Changes) Encode(schemas *schemarepo.Schemas) (*ChangesSrc, error) {
|
|||
schema = p.ResourceTypes[rc.Addr.Resource.Resource.Type]
|
||||
case addrs.DataResourceMode:
|
||||
schema = p.DataSources[rc.Addr.Resource.Resource.Type]
|
||||
case addrs.ListResourceMode:
|
||||
schema = p.ListResourceTypes[rc.Addr.Resource.Resource.Type]
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected resource mode %s", rc.Addr.Resource.Resource.Mode))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ func (c *ChangesSrc) Decode(schemas *schemarepo.Schemas) (*Changes, error) {
|
|||
schema = p.ResourceTypes[rcs.Addr.Resource.Resource.Type]
|
||||
case addrs.DataResourceMode:
|
||||
schema = p.DataSources[rcs.Addr.Resource.Resource.Type]
|
||||
case addrs.ListResourceMode:
|
||||
schema = p.ListResourceTypes[rcs.Addr.Resource.Resource.Type]
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected resource mode %s", rcs.Addr.Resource.Resource.Mode))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/logging"
|
||||
"github.com/hashicorp/terraform/internal/plugin/convert"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
|
|
@ -170,7 +171,24 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
|
|||
}
|
||||
|
||||
for name, list := range protoResp.ListResourceSchemas {
|
||||
resp.ListResourceTypes[name] = convert.ProtoToProviderSchema(list, nil)
|
||||
ret := convert.ProtoToProviderSchema(list, nil)
|
||||
resp.ListResourceTypes[name] = providers.Schema{
|
||||
Version: ret.Version,
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"data": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"config": {
|
||||
Block: *ret.Body,
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if decls, err := convert.FunctionDeclsFromProto(protoResp.Functions); err == nil {
|
||||
|
|
@ -357,7 +375,8 @@ func (p *GRPCProvider) ValidateListResourceConfig(r providers.ValidateListResour
|
|||
return resp
|
||||
}
|
||||
|
||||
mp, err := msgpack.Marshal(r.Config, listResourceSchema.Body.ImpliedType())
|
||||
configSchema := listResourceSchema.Body.BlockTypes["config"]
|
||||
mp, err := msgpack.Marshal(r.Config, configSchema.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
|
|
@ -1274,7 +1293,8 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
|
|||
return resp
|
||||
}
|
||||
|
||||
mp, err := msgpack.Marshal(r.Config, listResourceSchema.Body.ImpliedType())
|
||||
configSchema := listResourceSchema.Body.BlockTypes["config"]
|
||||
mp, err := msgpack.Marshal(r.Config, configSchema.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
|
|
@ -1298,43 +1318,42 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
|
|||
return resp
|
||||
}
|
||||
|
||||
var results []providers.ListResourceEvent
|
||||
resp.Result = cty.DynamicVal
|
||||
results := make([]cty.Value, 0)
|
||||
// Process the stream
|
||||
for {
|
||||
if int64(len(results)) >= r.Limit {
|
||||
// If we have reached the limit, we stop receiving events
|
||||
resp.Results = results
|
||||
return resp
|
||||
break
|
||||
}
|
||||
|
||||
event, err := client.Recv()
|
||||
if err == io.EOF {
|
||||
// End of stream, we're done
|
||||
resp.Results = results
|
||||
return resp
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.Results = results
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
break
|
||||
}
|
||||
|
||||
// Process the event
|
||||
resourceEvent := providers.ListResourceEvent{
|
||||
DisplayName: event.DisplayName,
|
||||
Diagnostics: convert.ProtoToDiagnostics(event.Diagnostic),
|
||||
obj := map[string]cty.Value{
|
||||
"display_name": cty.StringVal(event.DisplayName),
|
||||
"state": cty.NullVal(resourceSchema.Body.ImpliedType()),
|
||||
"identity": cty.NullVal(resourceSchema.Identity.ImpliedType()),
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(event.Diagnostic))
|
||||
|
||||
// Handle identity data - it must be present
|
||||
if event.Identity == nil || event.Identity.IdentityData == nil {
|
||||
resourceEvent.Diagnostics = resourceEvent.Diagnostics.Append(fmt.Errorf("missing identity data in ListResource event for %s", r.TypeName))
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing identity data in ListResource event for %s", r.TypeName))
|
||||
} else {
|
||||
identityVal, err := decodeDynamicValue(event.Identity.IdentityData, resourceSchema.Identity.ImpliedType())
|
||||
if err != nil {
|
||||
resourceEvent.Diagnostics = resourceEvent.Diagnostics.Append(err)
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
} else {
|
||||
resourceEvent.Identity = identityVal
|
||||
obj["identity"] = identityVal
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1343,14 +1362,26 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
|
|||
// Use the ResourceTypes schema for the resource object
|
||||
resourceObj, err := decodeDynamicValue(event.ResourceObject, resourceSchema.Body.ImpliedType())
|
||||
if err != nil {
|
||||
resourceEvent.Diagnostics = resourceEvent.Diagnostics.Append(err)
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
} else {
|
||||
resourceEvent.ResourceObject = resourceObj
|
||||
obj["state"] = resourceObj
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, resourceEvent)
|
||||
if resp.Diagnostics.HasErrors() {
|
||||
break
|
||||
}
|
||||
|
||||
results = append(results, cty.ObjectVal(obj))
|
||||
|
||||
}
|
||||
|
||||
// The provider result of a list resource is always a list, but
|
||||
// we will wrap that list in an object with a single attribute "data",
|
||||
// so that we can differentiate between a list resource instance (list.aws_instance.test[index])
|
||||
// and the elements of the result of a list resource instance (list.aws_instance.test.data[index])
|
||||
resp.Result = cty.ObjectVal(map[string]cty.Value{"data": cty.TupleVal(results)})
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ValidateStateStoreConfig(r providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/configs/hcl2shim"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/schemarepo"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
|
@ -22,6 +25,7 @@ import (
|
|||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plugin/convert"
|
||||
mockproto "github.com/hashicorp/terraform/internal/plugin/mock_proto"
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin5"
|
||||
)
|
||||
|
|
@ -1356,6 +1360,120 @@ func TestGRPCProvider_closeEphemeralResource(t *testing.T) {
|
|||
checkDiags(t, resp.Diagnostics)
|
||||
}
|
||||
|
||||
func TestGRPCProvider_GetSchema_ListResourceTypes(t *testing.T) {
|
||||
p := &GRPCProvider{
|
||||
client: mockProviderClient(t),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
resp := p.GetProviderSchema()
|
||||
listResourceSchema := resp.ListResourceTypes
|
||||
expected := map[string]providers.Schema{
|
||||
"list": {
|
||||
Version: 1,
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"data": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"config": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"filter_attr": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
actualBody := convert.ConfigSchemaToProto(listResourceSchema["list"].Body).String()
|
||||
expectedBody := convert.ConfigSchemaToProto(expected["list"].Body).String()
|
||||
if actualBody != expectedBody {
|
||||
t.Fatalf("expected %v, got %v", expectedBody, actualBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGRPCProvider_Encode(t *testing.T) {
|
||||
// TODO: This is the only test in this package that imports plans. If that
|
||||
// ever leads to a circular import, we should consider moving this test to
|
||||
// a different package or refactoring the test to not use plans.
|
||||
p := &GRPCProvider{
|
||||
client: mockProviderClient(t),
|
||||
ctx: context.Background(),
|
||||
Addr: addrs.ImpliedProviderForUnqualifiedType("testencode"),
|
||||
}
|
||||
resp := p.GetProviderSchema()
|
||||
|
||||
src := plans.NewChanges()
|
||||
src.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChange{
|
||||
Addr: addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ListResourceMode,
|
||||
Type: "list",
|
||||
Name: "test",
|
||||
},
|
||||
Key: addrs.NoKey,
|
||||
},
|
||||
},
|
||||
ProviderAddr: addrs.AbsProviderConfig{
|
||||
Provider: p.Addr,
|
||||
},
|
||||
Change: plans.Change{
|
||||
Before: cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"config": cty.Object(map[string]cty.Type{
|
||||
"filter_attr": cty.String,
|
||||
}),
|
||||
"data": cty.List(cty.Object(map[string]cty.Type{
|
||||
"state": cty.Object(map[string]cty.Type{
|
||||
"resource_attr": cty.String,
|
||||
}),
|
||||
"identity": cty.Object(map[string]cty.Type{
|
||||
"id_attr": cty.String,
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"config": cty.ObjectVal(map[string]cty.Value{
|
||||
"filter_attr": cty.StringVal("value"),
|
||||
}),
|
||||
"data": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"state": cty.ObjectVal(map[string]cty.Value{
|
||||
"resource_attr": cty.StringVal("value"),
|
||||
}),
|
||||
"identity": cty.ObjectVal(map[string]cty.Value{
|
||||
"id_attr": cty.StringVal("value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
_, err := src.Encode(&schemarepo.Schemas{
|
||||
Providers: map[addrs.Provider]providers.ProviderSchema{
|
||||
p.Addr: {
|
||||
ResourceTypes: resp.ResourceTypes,
|
||||
ListResourceTypes: resp.ListResourceTypes,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error encoding changes: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementation of the ListResource stream client
|
||||
type mockListResourceStreamClient struct {
|
||||
events []*proto.ListResource_Event
|
||||
|
|
@ -1424,47 +1542,57 @@ func TestGRPCProvider_ListResource(t *testing.T) {
|
|||
resp := p.ListResource(request)
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
results := resp.Results
|
||||
|
||||
// Verify that we received both events
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("Expected 2 events, got %d", len(results))
|
||||
data := resp.Result.AsValueMap()
|
||||
if _, ok := data["data"]; !ok {
|
||||
t.Fatal("Expected 'data' key in result")
|
||||
}
|
||||
// Verify that we received both events
|
||||
if len(data["data"].AsValueSlice()) != 2 {
|
||||
t.Fatalf("Expected 2 resources, got %d", len(data["data"].AsValueSlice()))
|
||||
}
|
||||
results := data["data"].AsValueSlice()
|
||||
|
||||
// Verify first event
|
||||
if results[0].DisplayName != "Test Resource 1" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 1', got '%s'", results[0].DisplayName)
|
||||
displayName := results[0].GetAttr("display_name")
|
||||
if displayName.AsString() != "Test Resource 1" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 1', got '%s'", displayName.AsString())
|
||||
}
|
||||
|
||||
expectedId1 := cty.ObjectVal(map[string]cty.Value{
|
||||
"id_attr": cty.StringVal("id-1"),
|
||||
})
|
||||
if !results[0].Identity.RawEquals(expectedId1) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId1, results[0].Identity)
|
||||
|
||||
identity := results[0].GetAttr("identity")
|
||||
if !identity.RawEquals(expectedId1) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId1, identity)
|
||||
}
|
||||
|
||||
// ResourceObject should be null for the first event as it wasn't provided
|
||||
if !results[0].ResourceObject.IsNull() {
|
||||
t.Errorf("Expected ResourceObject to be null, got %#v", results[0].ResourceObject)
|
||||
resourceObject := results[0].GetAttr("state")
|
||||
if !resourceObject.IsNull() {
|
||||
t.Errorf("Expected ResourceObject to be null, got %#v", resourceObject)
|
||||
}
|
||||
|
||||
// Verify second event
|
||||
if results[1].DisplayName != "Test Resource 2" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 2', got '%s'", results[1].DisplayName)
|
||||
displayName = results[1].GetAttr("display_name")
|
||||
if displayName.AsString() != "Test Resource 2" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 2', got '%s'", displayName.AsString())
|
||||
}
|
||||
|
||||
expectedId2 := cty.ObjectVal(map[string]cty.Value{
|
||||
"id_attr": cty.StringVal("id-2"),
|
||||
})
|
||||
if !results[1].Identity.RawEquals(expectedId2) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId2, results[1].Identity)
|
||||
identity = results[1].GetAttr("identity")
|
||||
if !identity.RawEquals(expectedId2) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId2, identity)
|
||||
}
|
||||
|
||||
expectedResource := cty.ObjectVal(map[string]cty.Value{
|
||||
"resource_attr": cty.StringVal("value"),
|
||||
})
|
||||
if !results[1].ResourceObject.RawEquals(expectedResource) {
|
||||
t.Errorf("Expected ResourceObject %#v, got %#v", expectedResource, results[1].ResourceObject)
|
||||
resourceObject = results[1].GetAttr("state")
|
||||
if !resourceObject.RawEquals(expectedResource) {
|
||||
t.Errorf("Expected ResourceObject %#v, got %#v", expectedResource, resourceObject)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1539,12 +1667,12 @@ func TestGRPCProvider_ListResource_Diagnostics(t *testing.T) {
|
|||
resp := p.ListResource(request)
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
// Verify that we received one event with diagnostics
|
||||
if len(resp.Results) != 1 {
|
||||
t.Fatalf("Expected 1 event, got %d", len(resp.Results))
|
||||
data := resp.Result.AsValueMap()
|
||||
if _, ok := data["data"]; !ok {
|
||||
t.Fatal("Expected 'data' key in result")
|
||||
}
|
||||
|
||||
if !resp.Results[0].Diagnostics.HasWarnings() {
|
||||
if !resp.Diagnostics.HasWarnings() {
|
||||
t.Fatal("Expected warning diagnostics, but got none")
|
||||
}
|
||||
}
|
||||
|
|
@ -1591,6 +1719,7 @@ func TestGRPCProvider_ListResource_Limit(t *testing.T) {
|
|||
gomock.Any(),
|
||||
).Return(mockStream, nil)
|
||||
|
||||
// Create the request
|
||||
configVal := cty.ObjectVal(map[string]cty.Value{
|
||||
"filter_attr": cty.StringVal("filter-value"),
|
||||
})
|
||||
|
|
@ -1603,8 +1732,17 @@ func TestGRPCProvider_ListResource_Limit(t *testing.T) {
|
|||
resp := p.ListResource(request)
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
results := resp.Results
|
||||
data := resp.Result.AsValueMap()
|
||||
if _, ok := data["data"]; !ok {
|
||||
t.Fatal("Expected 'data' key in result")
|
||||
}
|
||||
// Verify that we received both events
|
||||
if len(data["data"].AsValueSlice()) != 2 {
|
||||
t.Fatalf("Expected 2 resources, got %d", len(data["data"].AsValueSlice()))
|
||||
}
|
||||
results := data["data"].AsValueSlice()
|
||||
|
||||
// Verify that we received both events
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("Expected 2 events, got %d", len(results))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/logging"
|
||||
"github.com/hashicorp/terraform/internal/plugin6/convert"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
|
|
@ -171,7 +172,24 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
|
|||
}
|
||||
|
||||
for name, list := range protoResp.ListResourceSchemas {
|
||||
resp.ListResourceTypes[name] = convert.ProtoToProviderSchema(list, nil)
|
||||
ret := convert.ProtoToProviderSchema(list, nil)
|
||||
resp.ListResourceTypes[name] = providers.Schema{
|
||||
Version: ret.Version,
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"data": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"config": {
|
||||
Block: *ret.Body,
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for name, store := range protoResp.StateStoreSchemas {
|
||||
|
|
@ -354,8 +372,8 @@ func (p *GRPCProvider) ValidateListResourceConfig(r providers.ValidateListResour
|
|||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown list resource type %q", r.TypeName))
|
||||
return resp
|
||||
}
|
||||
|
||||
mp, err := msgpack.Marshal(r.Config, listResourceSchema.Body.ImpliedType())
|
||||
configSchema := listResourceSchema.Body.BlockTypes["config"]
|
||||
mp, err := msgpack.Marshal(r.Config, configSchema.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
|
|
@ -1270,7 +1288,8 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
|
|||
return resp
|
||||
}
|
||||
|
||||
mp, err := msgpack.Marshal(r.Config, listResourceSchema.Body.ImpliedType())
|
||||
configSchema := listResourceSchema.Body.BlockTypes["config"]
|
||||
mp, err := msgpack.Marshal(r.Config, configSchema.ImpliedType())
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
|
|
@ -1283,50 +1302,53 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
|
|||
Limit: r.Limit,
|
||||
}
|
||||
|
||||
// Start the streaming RPC
|
||||
client, err := p.client.ListResource(p.ctx, protoReq)
|
||||
// Start the streaming RPC with a context. The context will be cancelled
|
||||
// when this function returns, which will stop the stream if it is still
|
||||
// running.
|
||||
ctx, cancel := context.WithCancel(p.ctx)
|
||||
defer cancel()
|
||||
client, err := p.client.ListResource(ctx, protoReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
}
|
||||
|
||||
var results []providers.ListResourceEvent
|
||||
resp.Result = cty.DynamicVal
|
||||
results := make([]cty.Value, 0)
|
||||
// Process the stream
|
||||
for {
|
||||
if int64(len(results)) >= r.Limit {
|
||||
// If we have reached the limit, we stop receiving events
|
||||
resp.Results = results
|
||||
return resp
|
||||
break
|
||||
}
|
||||
|
||||
event, err := client.Recv()
|
||||
if err == io.EOF {
|
||||
// End of stream, we're done
|
||||
resp.Results = results
|
||||
return resp
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.Results = results
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
return resp
|
||||
break
|
||||
}
|
||||
|
||||
// Process the event
|
||||
resourceEvent := providers.ListResourceEvent{
|
||||
DisplayName: event.DisplayName,
|
||||
Diagnostics: convert.ProtoToDiagnostics(event.Diagnostic),
|
||||
obj := map[string]cty.Value{
|
||||
"display_name": cty.StringVal(event.DisplayName),
|
||||
"state": cty.NullVal(resourceSchema.Body.ImpliedType()),
|
||||
"identity": cty.NullVal(resourceSchema.Identity.ImpliedType()),
|
||||
}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(event.Diagnostic))
|
||||
|
||||
// Handle identity data - it must be present
|
||||
if event.Identity == nil || event.Identity.IdentityData == nil {
|
||||
resourceEvent.Diagnostics = resourceEvent.Diagnostics.Append(fmt.Errorf("missing identity data in ListResource event for %s", r.TypeName))
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing identity data in ListResource event for %s", r.TypeName))
|
||||
} else {
|
||||
identityVal, err := decodeDynamicValue(event.Identity.IdentityData, resourceSchema.Identity.ImpliedType())
|
||||
if err != nil {
|
||||
resourceEvent.Diagnostics = resourceEvent.Diagnostics.Append(err)
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
} else {
|
||||
resourceEvent.Identity = identityVal
|
||||
obj["identity"] = identityVal
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1335,14 +1357,26 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
|
|||
// Use the ResourceTypes schema for the resource object
|
||||
resourceObj, err := decodeDynamicValue(event.ResourceObject, resourceSchema.Body.ImpliedType())
|
||||
if err != nil {
|
||||
resourceEvent.Diagnostics = resourceEvent.Diagnostics.Append(err)
|
||||
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||
} else {
|
||||
resourceEvent.ResourceObject = resourceObj
|
||||
obj["state"] = resourceObj
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, resourceEvent)
|
||||
if resp.Diagnostics.HasErrors() {
|
||||
break
|
||||
}
|
||||
|
||||
results = append(results, cty.ObjectVal(obj))
|
||||
|
||||
}
|
||||
|
||||
// The provider result of a list resource is always a list, but
|
||||
// we will wrap that list in an object with a single attribute "data",
|
||||
// so that we can differentiate between a list resource instance (list.aws_instance.test[index])
|
||||
// and the elements of the result of a list resource instance (list.aws_instance.test.data[index])
|
||||
resp.Result = cty.ObjectVal(map[string]cty.Value{"data": cty.TupleVal(results)})
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *GRPCProvider) ValidateStateStoreConfig(r providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,11 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/configs/hcl2shim"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/schemarepo"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
|
@ -23,6 +26,7 @@ import (
|
|||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plugin6/convert"
|
||||
mockproto "github.com/hashicorp/terraform/internal/plugin6/mock_proto"
|
||||
proto "github.com/hashicorp/terraform/internal/tfplugin6"
|
||||
)
|
||||
|
|
@ -1362,6 +1366,119 @@ func TestGRPCProvider_closeEphemeralResource(t *testing.T) {
|
|||
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
}
|
||||
func TestGRPCProvider_GetSchema_ListResourceTypes(t *testing.T) {
|
||||
p := &GRPCProvider{
|
||||
client: mockProviderClient(t),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
resp := p.GetProviderSchema()
|
||||
listResourceSchema := resp.ListResourceTypes
|
||||
expected := map[string]providers.Schema{
|
||||
"list": {
|
||||
Version: 1,
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"data": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"config": {
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"filter_attr": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
actualBody := convert.ConfigSchemaToProto(listResourceSchema["list"].Body).String()
|
||||
expectedBody := convert.ConfigSchemaToProto(expected["list"].Body).String()
|
||||
if diff := cmp.Diff(expectedBody, actualBody); diff != "" {
|
||||
t.Fatalf("unexpected body (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGRPCProvider_Encode(t *testing.T) {
|
||||
// TODO: This is the only test in this package that imports plans. If that
|
||||
// ever leads to a circular import, we should consider moving this test to
|
||||
// a different package or refactoring the test to not use plans.
|
||||
p := &GRPCProvider{
|
||||
client: mockProviderClient(t),
|
||||
ctx: context.Background(),
|
||||
Addr: addrs.ImpliedProviderForUnqualifiedType("testencode"),
|
||||
}
|
||||
resp := p.GetProviderSchema()
|
||||
|
||||
src := plans.NewChanges()
|
||||
src.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChange{
|
||||
Addr: addrs.AbsResourceInstance{
|
||||
Module: addrs.RootModuleInstance,
|
||||
Resource: addrs.ResourceInstance{
|
||||
Resource: addrs.Resource{
|
||||
Mode: addrs.ListResourceMode,
|
||||
Type: "list",
|
||||
Name: "test",
|
||||
},
|
||||
Key: addrs.NoKey,
|
||||
},
|
||||
},
|
||||
ProviderAddr: addrs.AbsProviderConfig{
|
||||
Provider: p.Addr,
|
||||
},
|
||||
Change: plans.Change{
|
||||
Before: cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"config": cty.Object(map[string]cty.Type{
|
||||
"filter_attr": cty.String,
|
||||
}),
|
||||
"data": cty.List(cty.Object(map[string]cty.Type{
|
||||
"state": cty.Object(map[string]cty.Type{
|
||||
"resource_attr": cty.String,
|
||||
}),
|
||||
"identity": cty.Object(map[string]cty.Type{
|
||||
"id_attr": cty.String,
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
After: cty.ObjectVal(map[string]cty.Value{
|
||||
"config": cty.ObjectVal(map[string]cty.Value{
|
||||
"filter_attr": cty.StringVal("value"),
|
||||
}),
|
||||
"data": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"state": cty.ObjectVal(map[string]cty.Value{
|
||||
"resource_attr": cty.StringVal("value"),
|
||||
}),
|
||||
"identity": cty.ObjectVal(map[string]cty.Value{
|
||||
"id_attr": cty.StringVal("value"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
_, err := src.Encode(&schemarepo.Schemas{
|
||||
Providers: map[addrs.Provider]providers.ProviderSchema{
|
||||
p.Addr: {
|
||||
ResourceTypes: resp.ResourceTypes,
|
||||
ListResourceTypes: resp.ListResourceTypes,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error encoding changes: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementation of the ListResource stream client
|
||||
type mockListResourceStreamClient struct {
|
||||
|
|
@ -1431,47 +1548,57 @@ func TestGRPCProvider_ListResource(t *testing.T) {
|
|||
resp := p.ListResource(request)
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
results := resp.Results
|
||||
|
||||
// Verify that we received both events
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("Expected 2 events, got %d", len(results))
|
||||
data := resp.Result.AsValueMap()
|
||||
if _, ok := data["data"]; !ok {
|
||||
t.Fatal("Expected 'data' key in result")
|
||||
}
|
||||
// Verify that we received both events
|
||||
if len(data["data"].AsValueSlice()) != 2 {
|
||||
t.Fatalf("Expected 2 resources, got %d", len(data["data"].AsValueSlice()))
|
||||
}
|
||||
results := data["data"].AsValueSlice()
|
||||
|
||||
// Verify first event
|
||||
if results[0].DisplayName != "Test Resource 1" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 1', got '%s'", results[0].DisplayName)
|
||||
displayName := results[0].GetAttr("display_name")
|
||||
if displayName.AsString() != "Test Resource 1" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 1', got '%s'", displayName.AsString())
|
||||
}
|
||||
|
||||
expectedId1 := cty.ObjectVal(map[string]cty.Value{
|
||||
"id_attr": cty.StringVal("id-1"),
|
||||
})
|
||||
if !results[0].Identity.RawEquals(expectedId1) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId1, results[0].Identity)
|
||||
|
||||
identity := results[0].GetAttr("identity")
|
||||
if !identity.RawEquals(expectedId1) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId1, identity)
|
||||
}
|
||||
|
||||
// ResourceObject should be null for the first event as it wasn't provided
|
||||
if !results[0].ResourceObject.IsNull() {
|
||||
t.Errorf("Expected ResourceObject to be null, got %#v", results[0].ResourceObject)
|
||||
resourceObject := results[0].GetAttr("state")
|
||||
if !resourceObject.IsNull() {
|
||||
t.Errorf("Expected ResourceObject to be null, got %#v", resourceObject)
|
||||
}
|
||||
|
||||
// Verify second event
|
||||
if results[1].DisplayName != "Test Resource 2" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 2', got '%s'", results[1].DisplayName)
|
||||
displayName = results[1].GetAttr("display_name")
|
||||
if displayName.AsString() != "Test Resource 2" {
|
||||
t.Errorf("Expected DisplayName 'Test Resource 2', got '%s'", displayName.AsString())
|
||||
}
|
||||
|
||||
expectedId2 := cty.ObjectVal(map[string]cty.Value{
|
||||
"id_attr": cty.StringVal("id-2"),
|
||||
})
|
||||
if !results[1].Identity.RawEquals(expectedId2) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId2, results[1].Identity)
|
||||
identity = results[1].GetAttr("identity")
|
||||
if !identity.RawEquals(expectedId2) {
|
||||
t.Errorf("Expected Identity %#v, got %#v", expectedId2, identity)
|
||||
}
|
||||
|
||||
expectedResource := cty.ObjectVal(map[string]cty.Value{
|
||||
"resource_attr": cty.StringVal("value"),
|
||||
})
|
||||
if !results[1].ResourceObject.RawEquals(expectedResource) {
|
||||
t.Errorf("Expected ResourceObject %#v, got %#v", expectedResource, results[1].ResourceObject)
|
||||
resourceObject = results[1].GetAttr("state")
|
||||
if !resourceObject.RawEquals(expectedResource) {
|
||||
t.Errorf("Expected ResourceObject %#v, got %#v", expectedResource, resourceObject)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1479,6 +1606,7 @@ func TestGRPCProvider_ListResource_Error(t *testing.T) {
|
|||
client := mockProviderClient(t)
|
||||
p := &GRPCProvider{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
// Test case where the provider returns an error
|
||||
|
|
@ -1503,6 +1631,7 @@ func TestGRPCProvider_ListResource_Diagnostics(t *testing.T) {
|
|||
client := mockProviderClient(t)
|
||||
p := &GRPCProvider{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
// Create a mock stream client that will return a resource event with diagnostics
|
||||
|
|
@ -1544,12 +1673,12 @@ func TestGRPCProvider_ListResource_Diagnostics(t *testing.T) {
|
|||
resp := p.ListResource(request)
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
// Verify that we received one event with diagnostics
|
||||
if len(resp.Results) != 1 {
|
||||
t.Fatalf("Expected 1 event, got %d", len(resp.Results))
|
||||
data := resp.Result.AsValueMap()
|
||||
if _, ok := data["data"]; !ok {
|
||||
t.Fatal("Expected 'data' key in result")
|
||||
}
|
||||
|
||||
if !resp.Results[0].Diagnostics.HasWarnings() {
|
||||
if !resp.Diagnostics.HasWarnings() {
|
||||
t.Fatal("Expected warning diagnostics, but got none")
|
||||
}
|
||||
}
|
||||
|
|
@ -1558,6 +1687,7 @@ func TestGRPCProvider_ListResource_Limit(t *testing.T) {
|
|||
client := mockProviderClient(t)
|
||||
p := &GRPCProvider{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
// Create a mock stream client that will return resource events
|
||||
|
|
@ -1608,7 +1738,15 @@ func TestGRPCProvider_ListResource_Limit(t *testing.T) {
|
|||
resp := p.ListResource(request)
|
||||
checkDiags(t, resp.Diagnostics)
|
||||
|
||||
results := resp.Results
|
||||
data := resp.Result.AsValueMap()
|
||||
if _, ok := data["data"]; !ok {
|
||||
t.Fatal("Expected 'data' key in result")
|
||||
}
|
||||
// Verify that we received both events
|
||||
if len(data["data"].AsValueSlice()) != 2 {
|
||||
t.Fatalf("Expected 2 resources, got %d", len(data["data"].AsValueSlice()))
|
||||
}
|
||||
results := data["data"].AsValueSlice()
|
||||
|
||||
// Verify that we received both events
|
||||
if len(results) != 2 {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ type GetProviderSchemaResponse struct {
|
|||
// to its schema.
|
||||
EphemeralResourceTypes map[string]Schema
|
||||
|
||||
// ListResourceTypes maps the name of an ephemeral resource type to its
|
||||
// ListResourceTypes maps the name of a list resource type to its
|
||||
// schema.
|
||||
ListResourceTypes map[string]Schema
|
||||
|
||||
|
|
@ -725,23 +725,8 @@ type CallFunctionResponse struct {
|
|||
Err error
|
||||
}
|
||||
|
||||
// ListResourceEvent represents a single resource from the list operation
|
||||
type ListResourceEvent struct {
|
||||
// Identity contains the resource identity data
|
||||
Identity cty.Value
|
||||
|
||||
// DisplayName is a human-readable name for the resource
|
||||
DisplayName string
|
||||
|
||||
// ResourceObject contains the full resource object if requested
|
||||
ResourceObject cty.Value
|
||||
|
||||
// Diagnostics contains any warnings or errors specific to this event
|
||||
Diagnostics tfdiags.Diagnostics
|
||||
}
|
||||
|
||||
type ListResourceResponse struct {
|
||||
Results []ListResourceEvent
|
||||
Result cty.Value
|
||||
|
||||
Diagnostics tfdiags.Diagnostics
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,22 +14,13 @@ type Module struct {
|
|||
// Resources contains the state for each resource. The keys in this map are
|
||||
// an implementation detail and must not be used by outside callers.
|
||||
Resources map[string]*Resource
|
||||
|
||||
// ListResources contains the state of the result of each list resource.
|
||||
// The keys in this map are the config address of the list resource
|
||||
// itself. The values are maps of the list resource instance addresses to the
|
||||
// remote resource instance objects that are the result of the list
|
||||
// resource.
|
||||
// e.g "list.aws_instance.test" -> map["list.aws_instance.test[0]"] = ["resource.aws_instance.test[0]", "resource.aws_instance.test[1]"]
|
||||
ListResources map[string]addrs.Map[addrs.AbsResourceInstance, *ResourceInstanceObject]
|
||||
}
|
||||
|
||||
// NewModule constructs an empty module state for the given module address.
|
||||
func NewModule(addr addrs.ModuleInstance) *Module {
|
||||
return &Module{
|
||||
Addr: addr,
|
||||
Resources: map[string]*Resource{},
|
||||
ListResources: map[string]addrs.Map[addrs.AbsResourceInstance, *ResourceInstanceObject]{},
|
||||
Addr: addr,
|
||||
Resources: map[string]*Resource{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -644,32 +644,3 @@ func (s *State) MoveModule(src, dst addrs.AbsModuleCall) {
|
|||
s.MoveModuleInstance(ms.Addr, newInst)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SetListResourceInstance(addr addrs.AbsResourceInstance, val *ResourceInstanceObject) {
|
||||
ms := s.Module(addr.Module)
|
||||
if ms == nil {
|
||||
panic(fmt.Sprintf("no state for module %s", addr.Module.String()))
|
||||
}
|
||||
key := addr.Resource.Resource.String()
|
||||
if _, ok := ms.ListResources[key]; !ok {
|
||||
ms.ListResources[key] = addrs.MakeMap[addrs.AbsResourceInstance, *ResourceInstanceObject]()
|
||||
}
|
||||
ms.ListResources[key].Put(addr, val)
|
||||
}
|
||||
|
||||
func (s *State) GetListResource(addr addrs.AbsResource) addrs.Map[addrs.AbsResourceInstance, *ResourceInstanceObject] {
|
||||
ms := s.Module(addr.Module)
|
||||
if ms == nil {
|
||||
panic(fmt.Sprintf("no state for module %s", addr.Module.String()))
|
||||
}
|
||||
key := addr.Resource.String()
|
||||
if _, ok := ms.ListResources[key]; !ok {
|
||||
ms.ListResources[key] = addrs.MakeMap[addrs.AbsResourceInstance, *ResourceInstanceObject]()
|
||||
}
|
||||
return ms.ListResources[key]
|
||||
}
|
||||
|
||||
func (s *State) AllListResourceInstances() map[string]addrs.Map[addrs.AbsResourceInstance, *ResourceInstanceObject] {
|
||||
// only root modules have list resources
|
||||
return s.Modules[addrs.RootModuleInstance.String()].ListResources
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,15 +62,9 @@ func (ms *Module) DeepCopy() *Module {
|
|||
resources[k] = r.DeepCopy()
|
||||
}
|
||||
|
||||
listResources := make(map[string]addrs.Map[addrs.AbsResourceInstance, *ResourceInstanceObject], len(ms.ListResources))
|
||||
for k, r := range ms.ListResources {
|
||||
listResources[k] = r
|
||||
}
|
||||
|
||||
return &Module{
|
||||
Addr: ms.Addr, // technically mutable, but immutable by convention
|
||||
Resources: resources,
|
||||
ListResources: listResources,
|
||||
Addr: ms.Addr, // technically mutable, but immutable by convention
|
||||
Resources: resources,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -441,21 +441,6 @@ func (s *SyncState) RecordCheckResults(checkState *checks.State) {
|
|||
s.state.CheckResults = newResults
|
||||
}
|
||||
|
||||
func (s *SyncState) SetListResourceInstance(addr addrs.AbsResourceInstance, val *ResourceInstanceObject) {
|
||||
defer s.beginWrite()()
|
||||
s.state.SetListResourceInstance(addr, val)
|
||||
}
|
||||
|
||||
func (s *SyncState) GetListResource(addr addrs.AbsResource) addrs.Map[addrs.AbsResourceInstance, *ResourceInstanceObject] {
|
||||
defer s.beginWrite()()
|
||||
return s.state.GetListResource(addr)
|
||||
}
|
||||
|
||||
func (s *SyncState) AllListResourceInstances() map[string]addrs.Map[addrs.AbsResourceInstance, *ResourceInstanceObject] {
|
||||
defer s.beginWrite()()
|
||||
return s.state.AllListResourceInstances()
|
||||
}
|
||||
|
||||
// Lock acquires an explicit lock on the state, allowing direct read and write
|
||||
// access to the returned state object. The caller must call Unlock once
|
||||
// access is no longer needed, and then immediately discard the state pointer
|
||||
|
|
|
|||
|
|
@ -135,6 +135,10 @@ type PlanOpts struct {
|
|||
// Forget if set to true will cause the plan to forget all resources. This is
|
||||
// only allowd in the context of a destroy plan.
|
||||
Forget bool
|
||||
|
||||
// Query is a boolean that indicates whether the plan is being
|
||||
// generated for a query operation.
|
||||
Query bool
|
||||
}
|
||||
|
||||
// Plan generates an execution plan by comparing the given configuration
|
||||
|
|
@ -885,8 +889,14 @@ func (c *Context) deferredResources(config *configs.Config, deferrals []*plans.D
|
|||
|
||||
func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*Graph, walkOperation, tfdiags.Diagnostics) {
|
||||
var externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface
|
||||
var modeFilter addrs.ResourceMode
|
||||
if opts != nil {
|
||||
externalProviderConfigs = opts.ExternalProviders
|
||||
// During a query operation, we only want to add the list-type resources
|
||||
// to the graph.
|
||||
if opts.Query {
|
||||
modeFilter = addrs.ListResourceMode
|
||||
}
|
||||
}
|
||||
|
||||
switch mode := opts.Mode; mode {
|
||||
|
|
@ -915,6 +925,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
|
|||
forgetModules: forgetModules,
|
||||
GenerateConfigPath: opts.GenerateConfigPath,
|
||||
SkipGraphValidation: c.graphOpts.SkipGraphValidation,
|
||||
targetResourceMode: modeFilter,
|
||||
}).Build(addrs.RootModuleInstance)
|
||||
return graph, walkPlan, diags
|
||||
case plans.RefreshOnlyMode:
|
||||
|
|
|
|||
|
|
@ -6848,67 +6848,134 @@ data "test_data_source" "foo" {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContext2Plan_sensitiveOutput(t *testing.T) {
|
||||
m := testModuleInline(t, map[string]string{
|
||||
"main.tf": `
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
output "is_secret" {
|
||||
// not only must the plan store the output as sensitive, it must also be
|
||||
// evaluated as such
|
||||
value = issensitive(module.child.secret)
|
||||
}
|
||||
`,
|
||||
"./child/main.tf": `
|
||||
output "secret" {
|
||||
sensitive = true
|
||||
value = "test"
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{})
|
||||
|
||||
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
|
||||
tfdiags.AssertNoErrors(t, diags)
|
||||
|
||||
expectedChanges := &plans.Changes{
|
||||
Outputs: []*plans.OutputChange{
|
||||
{
|
||||
Addr: mustAbsOutputValue("module.child.output.secret"),
|
||||
Change: plans.Change{
|
||||
Action: plans.Create,
|
||||
BeforeIdentity: cty.NullVal(cty.DynamicPseudoType),
|
||||
AfterIdentity: cty.NullVal(cty.DynamicPseudoType),
|
||||
Before: cty.NullVal(cty.DynamicPseudoType),
|
||||
After: cty.StringVal("test"),
|
||||
// Testing that managed resources of type list are handled correctly
|
||||
// - They must be referenced using the fully qualified name
|
||||
// - We provide a hint to the user if they try to reference a resource of type list
|
||||
// without using the fully qualified name
|
||||
func TestContext2Plan_resourceNamedList(t *testing.T) {
|
||||
schemaResp := getProviderSchemaResponseFromProviderSchema(&providerSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"list": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"attr": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
Sensitive: true,
|
||||
},
|
||||
{
|
||||
Addr: mustAbsOutputValue("output.is_secret"),
|
||||
Change: plans.Change{
|
||||
Action: plans.Create,
|
||||
BeforeIdentity: cty.NullVal(cty.DynamicPseudoType),
|
||||
AfterIdentity: cty.NullVal(cty.DynamicPseudoType),
|
||||
Before: cty.NullVal(cty.DynamicPseudoType),
|
||||
After: cty.True,
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"instance_type": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"test_child_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"instance_type": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
changes, err := plan.Changes.Decode(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sort.SliceStable(changes.Outputs, func(i, j int) bool {
|
||||
return changes.Outputs[i].Addr.String() < changes.Outputs[j].Addr.String()
|
||||
})
|
||||
|
||||
if diff := cmp.Diff(expectedChanges, changes, ctydebug.CmpOptions); diff != "" {
|
||||
t.Fatalf("unexpected changes: %s", diff)
|
||||
cases := []struct {
|
||||
name string
|
||||
mainConfig string
|
||||
diagCount int
|
||||
expectedErrMsg []string
|
||||
}{
|
||||
{
|
||||
// We tried to reference a resource of type list without using the fully-qualified name.
|
||||
// The error contains a hint to help the user.
|
||||
name: "reference list block from resource",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "list" "test_resource1" {
|
||||
provider = test
|
||||
}
|
||||
|
||||
resource "list" "test_resource2" {
|
||||
provider = test
|
||||
attr = list.test_resource1.attr
|
||||
}
|
||||
`,
|
||||
diagCount: 1,
|
||||
expectedErrMsg: []string{
|
||||
"Reference to undeclared resource",
|
||||
"A list resource \"test_resource1\" \"attr\" has not been declared in the root module.",
|
||||
"Did you mean the managed resource list.test_resource1? If so, please use the fully qualified name of the resource, e.g. resource.list.test_resource1",
|
||||
},
|
||||
},
|
||||
{
|
||||
// We are referencing a managed resource
|
||||
// of type list using the resource.<block>.<name> syntax. This should be allowed.
|
||||
name: "reference managed resource of type list using resource.<block>.<name>",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "list" "test_resource" {
|
||||
provider = test
|
||||
attr = "bar"
|
||||
}
|
||||
|
||||
resource "list" "normal_resource" {
|
||||
provider = test
|
||||
attr = resource.list.test_resource.attr
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
configs := map[string]string{"main.tf": tc.mainConfig}
|
||||
|
||||
m := testModuleInline(t, configs)
|
||||
|
||||
providerAddr := addrs.NewDefaultProvider("test")
|
||||
provider := testProvider("test")
|
||||
provider.ConfigureProvider(providers.ConfigureProviderRequest{})
|
||||
provider.GetProviderSchemaResponse = schemaResp
|
||||
|
||||
ctx, diags := NewContext(&ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
providerAddr: testProviderFuncFixed(provider),
|
||||
},
|
||||
})
|
||||
tfdiags.AssertNoDiagnostics(t, diags)
|
||||
|
||||
_, diags = ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, nil))
|
||||
if len(diags) != tc.diagCount {
|
||||
t.Fatalf("expected %d diagnostics, got %d \n -diags: %s", tc.diagCount, len(diags), diags)
|
||||
}
|
||||
|
||||
if tc.diagCount > 0 {
|
||||
for _, err := range tc.expectedErrMsg {
|
||||
if !strings.Contains(diags.Err().Error(), err) {
|
||||
t.Fatalf("expected error message %q, but got %q", err, diags.Err().Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
759
internal/terraform/context_plan_query_test.go
Normal file
759
internal/terraform/context_plan_query_test.go
Normal file
|
|
@ -0,0 +1,759 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestContext2Plan_queryList(t *testing.T) {
|
||||
schemaResp := getProviderSchemaResponseFromProviderSchema(&providerSchema{
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"list": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"attr": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"instance_type": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"test_child_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"instance_type": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ListResourceTypes: map[string]*configschema.Block{
|
||||
"test_resource": getQueryTestSchema(),
|
||||
"test_child_resource": getQueryTestSchema(),
|
||||
},
|
||||
IdentityTypes: map[string]*configschema.Object{
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
"test_child_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
Nesting: configschema.NestingSingle,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
mainConfig string
|
||||
queryConfig string
|
||||
diagCount int
|
||||
expectedErrMsg []string
|
||||
assertState func(*states.State)
|
||||
assertChanges func(providers.ProviderSchema, *plans.ChangesSrc)
|
||||
listResourceFn func(request providers.ListResourceRequest) providers.ListResourceResponse
|
||||
}{
|
||||
{
|
||||
name: "valid list reference",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
variable "input" {
|
||||
type = string
|
||||
default = "foo"
|
||||
}
|
||||
|
||||
list "test_resource" "test" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = var.input
|
||||
}
|
||||
}
|
||||
|
||||
list "test_resource" "test2" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test.data[0].state.instance_type
|
||||
}
|
||||
}
|
||||
`,
|
||||
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
madeUp := []cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-654321")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-789012")}),
|
||||
}
|
||||
ids := []cty.Value{}
|
||||
for i := range madeUp {
|
||||
ids = append(ids, cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal(fmt.Sprintf("i-v%d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
resp := []cty.Value{}
|
||||
for i, v := range madeUp {
|
||||
resp = append(resp, cty.ObjectVal(map[string]cty.Value{
|
||||
"state": v,
|
||||
"identity": ids[i],
|
||||
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
ret := map[string]cty.Value{
|
||||
"data": cty.TupleVal(resp),
|
||||
}
|
||||
for k, v := range request.Config.AsValueMap() {
|
||||
if k != "data" {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
|
||||
},
|
||||
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
|
||||
expectedResources := []string{"list.test_resource.test", "list.test_resource.test2"}
|
||||
actualResources := make([]string, 0)
|
||||
for _, change := range changes.Resources {
|
||||
actualResources = append(actualResources, change.Addr.String())
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
expectedTypes := []string{"ami-123456", "ami-654321", "ami-789012"}
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
obj.ForEachElement(func(key cty.Value, val cty.Value) bool {
|
||||
val = val.GetAttr("state")
|
||||
if val.IsNull() {
|
||||
t.Fatalf("Expected 'state' attribute to be present, but it is null")
|
||||
}
|
||||
if val.GetAttr("instance_type").IsNull() {
|
||||
t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing")
|
||||
}
|
||||
actualTypes = append(actualTypes, val.GetAttr("instance_type").AsString())
|
||||
return false
|
||||
})
|
||||
sort.Strings(actualTypes)
|
||||
sort.Strings(expectedTypes)
|
||||
if diff := cmp.Diff(expectedTypes, actualTypes); diff != "" {
|
||||
t.Fatalf("Expected instance types to match, but they differ: %s", diff)
|
||||
}
|
||||
}
|
||||
sort.Strings(actualResources)
|
||||
sort.Strings(expectedResources)
|
||||
if diff := cmp.Diff(expectedResources, actualResources); diff != "" {
|
||||
t.Fatalf("Expected resources to match, but they differ: %s", diff)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid list instance reference",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
variable "input" {
|
||||
type = string
|
||||
default = "foo"
|
||||
}
|
||||
|
||||
list "test_resource" "test" {
|
||||
count = 1
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = var.input
|
||||
}
|
||||
}
|
||||
|
||||
list "test_resource" "test2" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test[0].data[0].state.instance_type
|
||||
}
|
||||
}
|
||||
`,
|
||||
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
madeUp := []cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-654321")}),
|
||||
}
|
||||
ids := []cty.Value{}
|
||||
for i := range madeUp {
|
||||
ids = append(ids, cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal(fmt.Sprintf("i-v%d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
resp := []cty.Value{}
|
||||
for i, v := range madeUp {
|
||||
resp = append(resp, cty.ObjectVal(map[string]cty.Value{
|
||||
"state": v,
|
||||
"identity": ids[i],
|
||||
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
ret := map[string]cty.Value{
|
||||
"data": cty.TupleVal(resp),
|
||||
}
|
||||
for k, v := range request.Config.AsValueMap() {
|
||||
if k != "data" {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
|
||||
},
|
||||
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
|
||||
expectedResources := []string{"list.test_resource.test[0]", "list.test_resource.test2"}
|
||||
actualResources := make([]string, 0)
|
||||
for _, change := range changes.Resources {
|
||||
actualResources = append(actualResources, change.Addr.String())
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
expectedTypes := []string{"ami-123456", "ami-654321"}
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
obj.ForEachElement(func(key cty.Value, val cty.Value) bool {
|
||||
val = val.GetAttr("state")
|
||||
if val.IsNull() {
|
||||
t.Fatalf("Expected 'state' attribute to be present, but it is null")
|
||||
}
|
||||
if val.GetAttr("instance_type").IsNull() {
|
||||
t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing")
|
||||
}
|
||||
actualTypes = append(actualTypes, val.GetAttr("instance_type").AsString())
|
||||
return false
|
||||
})
|
||||
sort.Strings(actualTypes)
|
||||
sort.Strings(expectedTypes)
|
||||
if diff := cmp.Diff(expectedTypes, actualTypes); diff != "" {
|
||||
t.Fatalf("Expected instance types to match, but they differ: %s", diff)
|
||||
}
|
||||
}
|
||||
sort.Strings(actualResources)
|
||||
sort.Strings(expectedResources)
|
||||
if diff := cmp.Diff(expectedResources, actualResources); diff != "" {
|
||||
t.Fatalf("Expected resources to match, but they differ: %s", diff)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid list result's attribute reference",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
variable "input" {
|
||||
type = string
|
||||
default = "foo"
|
||||
}
|
||||
|
||||
list "test_resource" "test" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = var.input
|
||||
}
|
||||
}
|
||||
|
||||
list "test_resource" "test2" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test.state.instance_type
|
||||
}
|
||||
}
|
||||
`,
|
||||
diagCount: 1,
|
||||
expectedErrMsg: []string{
|
||||
"Invalid list resource traversal",
|
||||
"The first step in the traversal for a list resource must be an attribute \"data\"",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test referencing a non-existent list resource
|
||||
name: "reference non-existent list resource",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
list "test_resource" "test" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.non_existent.attr
|
||||
}
|
||||
}
|
||||
`,
|
||||
diagCount: 1,
|
||||
expectedErrMsg: []string{
|
||||
"A list resource \"non_existent\" \"attr\" has not been declared in the root module.",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test referencing a list resource with invalid attribute
|
||||
name: "reference list resource with invalid attribute",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
list "test_resource" "test" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = "valid"
|
||||
}
|
||||
}
|
||||
|
||||
list "test_resource" "another" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test.data[0].state.invalid_attr
|
||||
}
|
||||
}
|
||||
`,
|
||||
diagCount: 1,
|
||||
expectedErrMsg: []string{
|
||||
"Unsupported attribute: This object has no argument, nested block, or exported attribute named \"invalid_attr\".",
|
||||
},
|
||||
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
madeUp := []cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
|
||||
}
|
||||
ids := []cty.Value{}
|
||||
for i := range madeUp {
|
||||
ids = append(ids, cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal(fmt.Sprintf("i-v%d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
resp := []cty.Value{}
|
||||
for i, v := range madeUp {
|
||||
resp = append(resp, cty.ObjectVal(map[string]cty.Value{
|
||||
"state": v,
|
||||
"identity": ids[i],
|
||||
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
ret := map[string]cty.Value{
|
||||
"data": cty.TupleVal(resp),
|
||||
}
|
||||
for k, v := range request.Config.AsValueMap() {
|
||||
if k != "data" {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "circular reference between list resources",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
list "test_resource" "test1" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test2.data[0].state.id
|
||||
}
|
||||
}
|
||||
|
||||
list "test_resource" "test2" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test1.data[0].state.id
|
||||
}
|
||||
}
|
||||
`,
|
||||
diagCount: 1,
|
||||
expectedErrMsg: []string{
|
||||
"Cycle: list.test_resource",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test complex expression with list reference
|
||||
name: "complex expression with list reference",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
variable "test_var" {
|
||||
type = string
|
||||
default = "default"
|
||||
}
|
||||
|
||||
list "test_resource" "test1" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = var.test_var
|
||||
}
|
||||
}
|
||||
|
||||
list "test_resource" "test2" {
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = length(list.test_resource.test1.data) > 0 ? list.test_resource.test1.data[0].state.instance_type : var.test_var
|
||||
}
|
||||
}
|
||||
`,
|
||||
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
madeUp := []cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
|
||||
}
|
||||
ids := []cty.Value{}
|
||||
for i := range madeUp {
|
||||
ids = append(ids, cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal(fmt.Sprintf("i-v%d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
resp := []cty.Value{}
|
||||
for i, v := range madeUp {
|
||||
resp = append(resp, cty.ObjectVal(map[string]cty.Value{
|
||||
"state": v,
|
||||
"identity": ids[i],
|
||||
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
ret := map[string]cty.Value{
|
||||
"data": cty.TupleVal(resp),
|
||||
}
|
||||
for k, v := range request.Config.AsValueMap() {
|
||||
if k != "data" {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
|
||||
},
|
||||
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
|
||||
expectedResources := []string{"list.test_resource.test1", "list.test_resource.test2"}
|
||||
actualResources := make([]string, 0)
|
||||
for _, change := range changes.Resources {
|
||||
actualResources = append(actualResources, change.Addr.String())
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
expectedTypes := []string{"ami-123456"}
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
obj.ForEachElement(func(key cty.Value, val cty.Value) bool {
|
||||
val = val.GetAttr("state")
|
||||
if val.IsNull() {
|
||||
t.Fatalf("Expected 'state' attribute to be present, but it is null")
|
||||
}
|
||||
if val.GetAttr("instance_type").IsNull() {
|
||||
t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing")
|
||||
}
|
||||
actualTypes = append(actualTypes, val.GetAttr("instance_type").AsString())
|
||||
return false
|
||||
})
|
||||
sort.Strings(actualTypes)
|
||||
sort.Strings(expectedTypes)
|
||||
if diff := cmp.Diff(expectedTypes, actualTypes); diff != "" {
|
||||
t.Fatalf("Expected instance types to match, but they differ: %s", diff)
|
||||
}
|
||||
}
|
||||
sort.Strings(actualResources)
|
||||
sort.Strings(expectedResources)
|
||||
if diff := cmp.Diff(expectedResources, actualResources); diff != "" {
|
||||
t.Fatalf("Expected resources to match, but they differ: %s", diff)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test list reference with index but without data field
|
||||
name: "list reference with index but without data field",
|
||||
mainConfig: `
|
||||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
version = "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
queryConfig: `
|
||||
list "test_resource" "test1" {
|
||||
for_each = toset(["foo", "bar"])
|
||||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = each.value
|
||||
}
|
||||
}
|
||||
|
||||
list "test_resource" "test2" {
|
||||
provider = test
|
||||
for_each = list.test_resource.test1
|
||||
|
||||
filter = {
|
||||
attr = each.value.data[0].state.instance_type
|
||||
}
|
||||
}
|
||||
`,
|
||||
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
madeUp := []cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
|
||||
}
|
||||
ids := []cty.Value{}
|
||||
for i := range madeUp {
|
||||
ids = append(ids, cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal(fmt.Sprintf("i-v%d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
resp := []cty.Value{}
|
||||
for i, v := range madeUp {
|
||||
resp = append(resp, cty.ObjectVal(map[string]cty.Value{
|
||||
"state": v,
|
||||
"identity": ids[i],
|
||||
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
|
||||
}))
|
||||
}
|
||||
|
||||
ret := map[string]cty.Value{
|
||||
"data": cty.TupleVal(resp),
|
||||
}
|
||||
for k, v := range request.Config.AsValueMap() {
|
||||
if k != "data" {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
|
||||
},
|
||||
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
|
||||
expectedResources := []string{"list.test_resource.test1[\"foo\"]", "list.test_resource.test1[\"bar\"]", "list.test_resource.test2[\"foo\"]", "list.test_resource.test2[\"bar\"]"}
|
||||
actualResources := make([]string, 0)
|
||||
for _, change := range changes.Resources {
|
||||
actualResources = append(actualResources, change.Addr.String())
|
||||
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
|
||||
cs, err := change.Decode(schema)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode change: %s", err)
|
||||
}
|
||||
|
||||
if cs.Change.Action != plans.Read {
|
||||
t.Fatalf("expected action to be Read, got %s", cs.Change.Action)
|
||||
}
|
||||
|
||||
// Verify instance types
|
||||
expectedTypes := []string{"ami-123456"}
|
||||
actualTypes := make([]string, 0)
|
||||
obj := cs.After.GetAttr("data")
|
||||
if obj.IsNull() {
|
||||
t.Fatalf("Expected 'data' attribute to be present, but it is null")
|
||||
}
|
||||
obj.ForEachElement(func(key cty.Value, val cty.Value) bool {
|
||||
val = val.GetAttr("state")
|
||||
if val.IsNull() {
|
||||
t.Fatalf("Expected 'state' attribute to be present, but it is null")
|
||||
}
|
||||
if val.GetAttr("instance_type").IsNull() {
|
||||
t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing")
|
||||
}
|
||||
actualTypes = append(actualTypes, val.GetAttr("instance_type").AsString())
|
||||
return false
|
||||
})
|
||||
sort.Strings(actualTypes)
|
||||
sort.Strings(expectedTypes)
|
||||
if diff := cmp.Diff(expectedTypes, actualTypes); diff != "" {
|
||||
t.Fatalf("Expected instance types to match, but they differ: %s", diff)
|
||||
}
|
||||
}
|
||||
sort.Strings(actualResources)
|
||||
sort.Strings(expectedResources)
|
||||
if diff := cmp.Diff(expectedResources, actualResources); diff != "" {
|
||||
t.Fatalf("Expected resources to match, but they differ: %s", diff)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
configs := map[string]string{"main.tf": tc.mainConfig}
|
||||
if tc.queryConfig != "" {
|
||||
configs["main.tfquery.hcl"] = tc.queryConfig
|
||||
}
|
||||
|
||||
mod := testModuleInline(t, configs)
|
||||
|
||||
providerAddr := addrs.NewDefaultProvider("test")
|
||||
provider := testProvider("test")
|
||||
provider.ConfigureProvider(providers.ConfigureProviderRequest{})
|
||||
provider.GetProviderSchemaResponse = schemaResp
|
||||
var requestConfigs = make(map[string]cty.Value)
|
||||
provider.ListResourceFn = func(request providers.ListResourceRequest) providers.ListResourceResponse {
|
||||
requestConfigs[request.TypeName] = request.Config
|
||||
fn := tc.listResourceFn
|
||||
if fn == nil {
|
||||
return provider.ListResourceResponse
|
||||
}
|
||||
return fn(request)
|
||||
}
|
||||
|
||||
ctx, diags := NewContext(&ContextOpts{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
providerAddr: testProviderFuncFixed(provider),
|
||||
},
|
||||
})
|
||||
tfdiags.AssertNoDiagnostics(t, diags)
|
||||
|
||||
plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{
|
||||
Mode: plans.NormalMode,
|
||||
SetVariables: testInputValuesUnset(mod.Module.Variables),
|
||||
Query: true,
|
||||
})
|
||||
if len(diags) != tc.diagCount {
|
||||
t.Fatalf("expected %d diagnostics, got %d \n -diags: %s", tc.diagCount, len(diags), diags)
|
||||
}
|
||||
|
||||
if tc.assertChanges != nil {
|
||||
sch, err := ctx.Schemas(mod, states.NewState())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get schemas: %s", err)
|
||||
}
|
||||
tc.assertChanges(sch.Providers[providerAddr], plan.Changes)
|
||||
}
|
||||
|
||||
if tc.diagCount > 0 {
|
||||
for _, err := range tc.expectedErrMsg {
|
||||
if !strings.Contains(diags.Err().Error(), err) {
|
||||
t.Fatalf("expected error message %q, but got %q", err, diags.Err().Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -3135,7 +3135,7 @@ func TestContext2Validate_queryList(t *testing.T) {
|
|||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test.data[0].instance_type
|
||||
attr = list.test_resource.test.data[0].state.instance_type
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -3171,7 +3171,7 @@ func TestContext2Validate_queryList(t *testing.T) {
|
|||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test[0].data[0].instance_type
|
||||
attr = list.test_resource.test[0].data[0].state.instance_type
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -3206,7 +3206,7 @@ func TestContext2Validate_queryList(t *testing.T) {
|
|||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test.instance_type
|
||||
attr = list.test_resource.test.state.instance_type
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -3347,7 +3347,7 @@ func TestContext2Validate_queryList(t *testing.T) {
|
|||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test.data[0].invalid_attr
|
||||
attr = list.test_resource.test.data[0].state.invalid_attr
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -3373,7 +3373,7 @@ func TestContext2Validate_queryList(t *testing.T) {
|
|||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test2.data[0].id
|
||||
attr = list.test_resource.test2.data[0].state.id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3381,7 +3381,7 @@ func TestContext2Validate_queryList(t *testing.T) {
|
|||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = list.test_resource.test1.data[0].id
|
||||
attr = list.test_resource.test1.data[0].state.id
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -3421,7 +3421,7 @@ func TestContext2Validate_queryList(t *testing.T) {
|
|||
provider = test
|
||||
|
||||
filter = {
|
||||
attr = length(list.test_resource.test1.data) > 0 ? list.test_resource.test1.data[0].instance_type : var.test_var
|
||||
attr = length(list.test_resource.test1.data) > 0 ? list.test_resource.test1.data[0].state.instance_type : var.test_var
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
@ -3554,6 +3554,10 @@ func getQueryTestSchema() *configschema.Block {
|
|||
},
|
||||
},
|
||||
},
|
||||
"data": {
|
||||
Computed: true,
|
||||
Type: cty.DynamicPseudoType,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,24 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/instances"
|
||||
"github.com/hashicorp/terraform/internal/namedvals"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
|
||||
"github.com/hashicorp/terraform/internal/resources/ephemeral"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
)
|
||||
|
||||
func TestBuiltinEvalContextProviderInput(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
cache := make(map[string]map[string]cty.Value)
|
||||
|
||||
ctx1 := testBuiltinEvalContext(t)
|
||||
ctx1 := defaultTestCtx(t)
|
||||
ctx1 = ctx1.withScope(evalContextModuleInstance{Addr: addrs.RootModuleInstance}).(*BuiltinEvalContext)
|
||||
ctx1.ProviderInputConfig = cache
|
||||
ctx1.ProviderLock = &lock
|
||||
|
||||
ctx2 := testBuiltinEvalContext(t)
|
||||
ctx2 := defaultTestCtx(t)
|
||||
ctx2 = ctx2.withScope(evalContextModuleInstance{Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey)}).(*BuiltinEvalContext)
|
||||
ctx2.ProviderInputConfig = cache
|
||||
ctx2.ProviderLock = &lock
|
||||
|
|
@ -61,7 +65,7 @@ func TestBuildingEvalContextInitProvider(t *testing.T) {
|
|||
|
||||
testP := &testing_provider.MockProvider{}
|
||||
|
||||
ctx := testBuiltinEvalContext(t)
|
||||
ctx := defaultTestCtx(t)
|
||||
ctx = ctx.withScope(evalContextModuleInstance{Addr: addrs.RootModuleInstance}).(*BuiltinEvalContext)
|
||||
ctx.ProviderLock = &lock
|
||||
ctx.ProviderCache = make(map[string]providers.Interface)
|
||||
|
|
@ -101,6 +105,41 @@ func TestBuildingEvalContextInitProvider(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func testBuiltinEvalContext(t *testing.T) *BuiltinEvalContext {
|
||||
return &BuiltinEvalContext{}
|
||||
var defaultTestCtx = func(t *testing.T) *BuiltinEvalContext {
|
||||
return testBuiltinEvalContext(t, walkPlan, nil, nil, nil)
|
||||
}
|
||||
|
||||
func testBuiltinEvalContext(t *testing.T, op walkOperation, cfg *configs.Config, state *states.State, valState *namedvals.State) *BuiltinEvalContext {
|
||||
t.Helper()
|
||||
if state == nil {
|
||||
state = states.NewState()
|
||||
}
|
||||
if cfg == nil {
|
||||
cfg = configs.NewEmptyConfig()
|
||||
}
|
||||
if valState == nil {
|
||||
valState = namedvals.NewState()
|
||||
}
|
||||
ex := instances.NewExpander(nil)
|
||||
eph := ephemeral.NewResources()
|
||||
ev := &Evaluator{
|
||||
Config: cfg,
|
||||
State: state.SyncWrapper(),
|
||||
Operation: op,
|
||||
NamedValues: valState,
|
||||
Instances: ex,
|
||||
EphemeralResources: eph,
|
||||
}
|
||||
return &BuiltinEvalContext{
|
||||
Evaluator: ev,
|
||||
StateValue: state.SyncWrapper(),
|
||||
PrevRunStateValue: state.DeepCopy().SyncWrapper(),
|
||||
RefreshStateValue: state.DeepCopy().SyncWrapper(),
|
||||
NamedValuesValue: valState,
|
||||
ProviderLock: &sync.Mutex{},
|
||||
ProviderCache: make(map[string]providers.Interface),
|
||||
ProviderFuncCache: make(map[string]providers.Interface),
|
||||
InstanceExpanderValue: ex,
|
||||
EphemeralResourcesValue: eph,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -816,7 +816,7 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
switch d.Operation {
|
||||
case walkValidate:
|
||||
return cty.DynamicVal, diags
|
||||
case walkQuery:
|
||||
case walkPlan:
|
||||
// continue
|
||||
default:
|
||||
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
|
||||
|
|
@ -827,16 +827,6 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
})
|
||||
}
|
||||
|
||||
// The provider result of a list resource is always a tuple, but
|
||||
// we will wrap that tuple in an object with a single attribute "data",
|
||||
// so that we can differentiate between a list resource instance (list.aws_instance.test[index])
|
||||
// and the elements of the result of a list resource instance (list.aws_instance.test.data[index])
|
||||
wrappedVal := func(v *states.ResourceInstanceObject) cty.Value {
|
||||
return cty.ObjectVal(map[string]cty.Value{
|
||||
"data": v.Value,
|
||||
"identity": v.Identity,
|
||||
})
|
||||
}
|
||||
lAddr := config.Addr()
|
||||
mAddr := addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
|
|
@ -856,9 +846,9 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
return cty.DynamicVal, diags
|
||||
}
|
||||
resourceType := resourceSchema.Body.ImpliedType()
|
||||
instances := d.Evaluator.State.GetListResource(lAddr.Absolute(d.ModulePath))
|
||||
changes := d.Evaluator.Changes.GetChangesForAbsResource(lAddr.Absolute(d.ModulePath))
|
||||
|
||||
if len(instances.Values()) == 0 {
|
||||
if len(changes) == 0 {
|
||||
// Since we know there are no instances, return an empty container of the expected type.
|
||||
switch {
|
||||
case config.Count != nil:
|
||||
|
|
@ -875,18 +865,18 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
case config.Count != nil:
|
||||
// figure out what the last index we have is
|
||||
length := -1
|
||||
for _, inst := range instances.Elems {
|
||||
if intKey, ok := inst.Key.Resource.Key.(addrs.IntKey); ok {
|
||||
for _, inst := range changes {
|
||||
if intKey, ok := inst.Addr.Resource.Key.(addrs.IntKey); ok {
|
||||
length = max(int(intKey)+1, length)
|
||||
}
|
||||
}
|
||||
|
||||
if length > 0 {
|
||||
vals := make([]cty.Value, length)
|
||||
for _, inst := range instances.Elems {
|
||||
key := inst.Key.Resource.Key
|
||||
for _, inst := range changes {
|
||||
key := inst.Addr.Resource.Key
|
||||
if intKey, ok := key.(addrs.IntKey); ok {
|
||||
vals[int(intKey)] = wrappedVal(inst.Value)
|
||||
vals[int(intKey)] = inst.After
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -902,10 +892,10 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
}
|
||||
case config.ForEach != nil:
|
||||
vals := make(map[string]cty.Value)
|
||||
for _, inst := range instances.Elems {
|
||||
key := inst.Key.Resource.Key
|
||||
for _, inst := range changes {
|
||||
key := inst.Addr.Resource.Key
|
||||
if strKey, ok := key.(addrs.StringKey); ok {
|
||||
vals[string(strKey)] = wrappedVal(inst.Value)
|
||||
vals[string(strKey)] = inst.After
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -918,18 +908,15 @@ func (d *evaluationStateData) getListResource(config *configs.Resource, rng tfdi
|
|||
} else {
|
||||
ret = cty.EmptyObjectVal
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
default:
|
||||
inst, ok := instances.GetOk(lAddr.Absolute(d.ModulePath).Instance(addrs.NoKey))
|
||||
if !ok {
|
||||
// if the instance is missing, insert an unknown value
|
||||
inst = &states.ResourceInstanceObject{
|
||||
Value: cty.UnknownVal(resourceType),
|
||||
}
|
||||
if len(changes) <= 0 {
|
||||
// if the instance is missing, insert an empty tuple
|
||||
ret = cty.ObjectVal(map[string]cty.Value{
|
||||
"data": cty.EmptyTupleVal,
|
||||
})
|
||||
} else {
|
||||
ret = changes[0].After
|
||||
}
|
||||
|
||||
ret = wrappedVal(inst)
|
||||
}
|
||||
|
||||
return ret, diags
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource
|
|||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid list resource traversal`,
|
||||
Detail: fmt.Sprintf(`The first step in the traversal for a %s resource must be an attribute "data", but got %T instead.`, modeAdjective, remain[0]),
|
||||
Detail: fmt.Sprintf(`The first step in the traversal for a %s resource must be an attribute "data", but got %q instead.`, modeAdjective, remain[0]),
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return diags
|
||||
|
|
@ -315,7 +315,32 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource
|
|||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid list resource traversal`,
|
||||
Detail: fmt.Sprintf(`The second step in the traversal for a %s resource must be an index, but got %T instead.`, modeAdjective, remain[0]),
|
||||
Detail: fmt.Sprintf(`The second step in the traversal for a %s resource must be an index, but got %q instead.`, modeAdjective, remain[0]),
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
// remove the index, and now we have the rest of the traversal,
|
||||
// which we can validate against the schema
|
||||
remain = remain[1:]
|
||||
}
|
||||
|
||||
if len(remain) > 0 { // i.e list.aws_instance.foo.data[count.index].state
|
||||
stateOrIdent, ok := remain[0].(hcl.TraverseAttr)
|
||||
if !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid list resource traversal`,
|
||||
Detail: fmt.Sprintf(`The third step in the traversal for a %s resource must be an attribute "state" or "identity", but got %q instead.`, modeAdjective, remain[0]),
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return diags
|
||||
}
|
||||
if stateOrIdent.Name != "state" && stateOrIdent.Name != "identity" {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: `Invalid list resource traversal`,
|
||||
Detail: fmt.Sprintf(`The third step in the traversal for a %s resource must be an attribute "state" or "identity", but got %q instead.`, modeAdjective, stateOrIdent.Name),
|
||||
Subject: rng.ToHCL().Ptr(),
|
||||
})
|
||||
return diags
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ package terraform
|
|||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
|
|
@ -111,6 +110,11 @@ type PlanGraphBuilder struct {
|
|||
// SkipGraphValidation indicates whether the graph builder should skip
|
||||
// validation of the graph.
|
||||
SkipGraphValidation bool
|
||||
|
||||
// targetResourceMode is the resource mode to select for the graph.
|
||||
// If set, this is used to filter out resources that are not of the given mode.
|
||||
// Otherwise, all resources are included.
|
||||
targetResourceMode addrs.ResourceMode
|
||||
}
|
||||
|
||||
// See GraphBuilder
|
||||
|
|
@ -146,10 +150,9 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
|
|||
destroy: b.Operation == walkDestroy || b.Operation == walkPlanDestroy,
|
||||
|
||||
importTargets: b.ImportTargets,
|
||||
ModeFilter: b.Operation == walkPlan && b.targetResourceMode != addrs.InvalidResourceMode,
|
||||
Mode: b.targetResourceMode,
|
||||
|
||||
// the validate walk also needs to include query-related nodes.
|
||||
includeQuery: slices.Contains([]walkOperation{walkValidate, walkQuery}, b.Operation),
|
||||
// We only want to generate config during a plan operation.
|
||||
generateConfigPathForImportTargets: b.GenerateConfigPath,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -17,5 +17,4 @@ const (
|
|||
walkDestroy
|
||||
walkImport
|
||||
walkEval // used just to prepare EvalContext for expression evaluation, with no other actions
|
||||
walkQuery
|
||||
)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ func (n *NodePlannableResourceInstance) Execute(ctx EvalContext, op walkOperatio
|
|||
return n.dataResourceExecute(ctx)
|
||||
case addrs.EphemeralResourceMode:
|
||||
return n.ephemeralResourceExecute(ctx)
|
||||
case addrs.ListResourceMode:
|
||||
return n.listResourceExecute(ctx)
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode))
|
||||
}
|
||||
|
|
|
|||
90
internal/terraform/node_resource_plan_instance_query.go
Normal file
90
internal/terraform/node_resource_plan_instance_query.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) {
|
||||
log.Printf("[TRACE] NodePlannableResourceInstance: listing resources for %s", n.Addr)
|
||||
config := n.Config
|
||||
addr := n.ResourceInstanceAddr()
|
||||
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
|
||||
diags = diags.Append(err)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// validate self ref
|
||||
diags = diags.Append(validateSelfRef(addr.Resource, config.Config, providerSchema))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
keyData := EvalDataForInstanceKey(addr.Resource.Key, nil)
|
||||
if config.ForEach != nil {
|
||||
forEach, _, _ := evaluateForEachExpression(config.ForEach, ctx, false)
|
||||
keyData = EvalDataForInstanceKey(addr.Resource.Key, forEach)
|
||||
}
|
||||
|
||||
// evaluate the list config block
|
||||
var configDiags tfdiags.Diagnostics
|
||||
configVal, _, configDiags := ctx.EvaluateBlock(config.Config, n.Schema.Body, nil, keyData)
|
||||
diags = diags.Append(configDiags)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// Unmark before sending to provider
|
||||
unmarkedConfigVal, _ := configVal.UnmarkDeepWithPaths()
|
||||
configKnown := configVal.IsWhollyKnown()
|
||||
if !configKnown {
|
||||
diags = diags.Append(fmt.Errorf("config is not known"))
|
||||
return diags
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] NodePlannableResourceInstance: Re-validating config for %s", n.Addr)
|
||||
validateResp := provider.ValidateListResourceConfig(
|
||||
providers.ValidateListResourceConfigRequest{
|
||||
TypeName: n.Config.Type,
|
||||
Config: unmarkedConfigVal,
|
||||
},
|
||||
)
|
||||
diags = diags.Append(validateResp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// If we get down here then our configuration is complete and we're ready
|
||||
// to actually call the provider to list the data.
|
||||
resp := provider.ListResource(providers.ListResourceRequest{
|
||||
TypeName: n.Config.Type,
|
||||
Config: unmarkedConfigVal,
|
||||
})
|
||||
if resp.Diagnostics != nil {
|
||||
return diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
|
||||
}
|
||||
|
||||
change := &plans.ResourceInstanceChange{
|
||||
Addr: n.Addr,
|
||||
ProviderAddr: n.ResolvedProvider,
|
||||
Change: plans.Change{
|
||||
Action: plans.Read,
|
||||
Before: cty.DynamicVal,
|
||||
After: resp.Result,
|
||||
},
|
||||
DeposedKey: states.NotDeposed,
|
||||
}
|
||||
|
||||
ctx.Changes().AppendResourceInstanceChange(change)
|
||||
return diags
|
||||
}
|
||||
|
|
@ -38,9 +38,6 @@ type ConfigTransformer struct {
|
|||
// some actions are skipped during the destroy process
|
||||
destroy bool
|
||||
|
||||
// includeQuery is true if the graph should include query nodes.
|
||||
includeQuery bool
|
||||
|
||||
// importTargets specifies a slice of addresses that will have state
|
||||
// imported for them.
|
||||
importTargets []*ImportTarget
|
||||
|
|
@ -103,9 +100,6 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er
|
|||
for _, r := range module.DataResources {
|
||||
allResources = append(allResources, r)
|
||||
}
|
||||
}
|
||||
|
||||
if t.includeQuery {
|
||||
for _, r := range module.ListResources {
|
||||
allResources = append(allResources, r)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@ func _() {
|
|||
_ = x[walkDestroy-5]
|
||||
_ = x[walkImport-6]
|
||||
_ = x[walkEval-7]
|
||||
_ = x[walkQuery-8]
|
||||
}
|
||||
|
||||
const _walkOperation_name = "walkInvalidwalkApplywalkPlanwalkPlanDestroywalkValidatewalkDestroywalkImportwalkEvalwalkQuery"
|
||||
const _walkOperation_name = "walkInvalidwalkApplywalkPlanwalkPlanDestroywalkValidatewalkDestroywalkImportwalkEval"
|
||||
|
||||
var _walkOperation_index = [...]uint8{0, 11, 20, 28, 43, 55, 66, 76, 84, 93}
|
||||
var _walkOperation_index = [...]uint8{0, 11, 20, 28, 43, 55, 66, 76, 84}
|
||||
|
||||
func (i walkOperation) String() string {
|
||||
if i >= walkOperation(len(_walkOperation_index)-1) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue