@ -12,6 +12,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
@ -24,15 +25,16 @@ import (
func TestContext2Plan_queryList ( t * testing . T ) {
cases := [ ] struct {
name string
mainConfig string
queryConfig string
generatedPath string
diagCount int
expectedErrMsg [ ] string
assertState func ( * states . State )
assertChanges func ( providers . ProviderSchema , * plans . ChangesSrc )
listResourceFn func ( request providers . ListResourceRequest ) providers . ListResourceResponse
name string
mainConfig string
queryConfig string
generatedPath string
expectedErrMsg [ ] string
transformSchema func ( * providers . GetProviderSchemaResponse )
assertState func ( * states . State )
assertValidateDiags func ( t * testing . T , diags tfdiags . Diagnostics )
assertChanges func ( providers . ProviderSchema , * plans . ChangesSrc )
listResourceFn func ( request providers . ListResourceRequest ) providers . ListResourceResponse
} {
{
name : "valid list reference - generates config" ,
@ -261,6 +263,186 @@ func TestContext2Plan_queryList(t *testing.T) {
}
} ,
} ,
{
name : "with empty config when it is required" ,
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
}
` ,
transformSchema : func ( schema * providers . GetProviderSchemaResponse ) {
schema . ListResourceTypes [ "test_resource" ] . Body . BlockTypes = map [ string ] * configschema . NestedBlock {
"config" : {
Block : configschema . Block {
Attributes : map [ string ] * configschema . Attribute {
"filter" : {
Required : true ,
NestedType : & configschema . Object {
Nesting : configschema . NestingSingle ,
Attributes : map [ string ] * configschema . Attribute {
"attr" : {
Type : cty . String ,
Optional : true ,
} ,
} ,
} ,
} ,
} ,
} ,
Nesting : configschema . NestingSingle ,
MinItems : 1 ,
MaxItems : 1 ,
} ,
}
} ,
assertValidateDiags : func ( t * testing . T , diags tfdiags . Diagnostics ) {
tfdiags . AssertDiagnosticCount ( t , diags , 1 )
var exp tfdiags . Diagnostics
exp = exp . Append ( & hcl . Diagnostic {
Summary : "Missing config block" ,
Detail : "A block of type \"config\" is required here." ,
Subject : diags [ 0 ] . Source ( ) . Subject . ToHCL ( ) . Ptr ( ) ,
} )
tfdiags . AssertDiagnosticsMatch ( t , diags , exp )
} ,
} ,
{
name : "with empty optional config" ,
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
}
` ,
transformSchema : func ( schema * providers . GetProviderSchemaResponse ) {
schema . ListResourceTypes [ "test_resource" ] . Body . BlockTypes = map [ string ] * configschema . NestedBlock {
"config" : {
Block : configschema . Block {
Attributes : map [ string ] * configschema . Attribute {
"filter" : {
Optional : true ,
NestedType : & configschema . Object {
Nesting : configschema . NestingSingle ,
Attributes : map [ string ] * configschema . Attribute {
"attr" : {
Type : cty . String ,
Optional : true ,
} ,
} ,
} ,
} ,
} ,
} ,
Nesting : configschema . NestingSingle ,
} ,
}
} ,
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" }
actualResources := make ( [ ] string , 0 )
for _ , change := range changes . Queries {
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 )
}
// Verify instance types
expectedTypes := [ ] string { "ami-123456" , "ami-654321" }
actualTypes := make ( [ ] string , 0 )
obj := cs . Results . Value . 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 : `
@ -301,10 +483,17 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
` ,
diagCount : 1 ,
expectedErrMsg : [ ] string {
"Invalid list resource traversal" ,
"The first step in the traversal for a list resource must be an attribute \"data\"" ,
assertValidateDiags : func ( t * testing . T , diags tfdiags . Diagnostics ) {
tfdiags . AssertDiagnosticCount ( t , diags , 1 )
var exp tfdiags . Diagnostics
exp = exp . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid list resource traversal" ,
Detail : "The first step in the traversal for a list resource must be an attribute \"data\"." ,
Subject : diags [ 0 ] . Source ( ) . Subject . ToHCL ( ) . Ptr ( ) ,
} )
tfdiags . AssertDiagnosticsMatch ( t , diags , exp )
} ,
} ,
{
@ -332,9 +521,18 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
` ,
diagCount : 1 ,
expectedErrMsg : [ ] string {
"A list resource \"non_existent\" \"attr\" has not been declared in the root module." ,
assertValidateDiags : func ( t * testing . T , diags tfdiags . Diagnostics ) {
tfdiags . AssertDiagnosticCount ( t , diags , 1 )
var exp tfdiags . Diagnostics
exp = exp . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Reference to undeclared resource" ,
Detail : "A list resource \"non_existent\" \"attr\" has not been declared in the root module." ,
Subject : diags [ 0 ] . Source ( ) . Subject . ToHCL ( ) . Ptr ( ) ,
} )
tfdiags . AssertDiagnosticsMatch ( t , diags , exp )
} ,
} ,
{
@ -373,9 +571,18 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
` ,
diagCount : 1 ,
expectedErrMsg : [ ] string {
"Unsupported attribute: This object has no argument, nested block, or exported attribute named \"invalid_attr\"." ,
assertValidateDiags : func ( t * testing . T , diags tfdiags . Diagnostics ) {
tfdiags . AssertDiagnosticCount ( t , diags , 1 )
var exp tfdiags . Diagnostics
exp = exp . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unsupported attribute" ,
Detail : "This object has no argument, nested block, or exported attribute named \"invalid_attr\"." ,
Subject : diags [ 0 ] . Source ( ) . Subject . ToHCL ( ) . Ptr ( ) ,
} )
tfdiags . AssertDiagnosticsMatch ( t , diags , exp )
} ,
listResourceFn : func ( request providers . ListResourceRequest ) providers . ListResourceResponse {
madeUp := [ ] cty . Value {
@ -444,9 +651,14 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
` ,
diagCount : 1 ,
expectedErrMsg : [ ] string {
"Cycle: list.test_resource" ,
assertValidateDiags : func ( t * testing . T , diags tfdiags . Diagnostics ) {
tfdiags . AssertDiagnosticCount ( t , diags , 1 )
if ! strings . Contains ( diags [ 0 ] . Description ( ) . Summary , "Cycle: list.test_resource" ) {
t . Errorf ( "Expected error message to contain 'Cycle: list.test_resource', got %q" , diags [ 0 ] . Description ( ) . Summary )
}
if diags [ 0 ] . Severity ( ) != tfdiags . Error {
t . Errorf ( "Expected error severity to be Error, got %s" , diags [ 0 ] . Severity ( ) )
}
} ,
} ,
{
@ -564,8 +776,7 @@ func TestContext2Plan_queryList(t *testing.T) {
} ,
} ,
{
// Test list reference with index but without data field
name : "list reference with index but without data field" ,
name : "list reference as for_each" ,
mainConfig : `
terraform {
required_providers {
@ -688,8 +899,14 @@ func TestContext2Plan_queryList(t *testing.T) {
provider := testProvider ( "test" )
provider . ConfigureProvider ( providers . ConfigureProviderRequest { } )
provider . GetProviderSchemaResponse = getListProviderSchemaResp ( )
if tc . transformSchema != nil {
tc . transformSchema ( provider . GetProviderSchemaResponse )
}
var requestConfigs = make ( map [ string ] cty . Value )
provider . ListResourceFn = func ( request providers . ListResourceRequest ) providers . ListResourceResponse {
if request . Config . IsNull ( ) || request . Config . GetAttr ( "config" ) . IsNull ( ) {
t . Fatalf ( "config should never be null, got null for %s" , request . TypeName )
}
requestConfigs [ request . TypeName ] = request . Config
fn := tc . listResourceFn
if fn == nil {
@ -705,15 +922,21 @@ func TestContext2Plan_queryList(t *testing.T) {
} )
tfdiags . AssertNoDiagnostics ( t , diags )
diags = ctx . Validate ( mod , & ValidateOpts { } )
if tc . assertValidateDiags != nil {
tc . assertValidateDiags ( t , diags )
return
} else {
tfdiags . AssertNoDiagnostics ( t , diags )
}
plan , diags := ctx . Plan ( mod , states . NewState ( ) , & PlanOpts {
Mode : plans . NormalMode ,
SetVariables : testInputValuesUnset ( mod . Module . Variables ) ,
Query : true ,
GenerateConfigPath : tc . generatedPath ,
} )
if len ( diags ) != tc . diagCount {
t . Fatalf ( "expected %d diagnostics, got %d \n -diags: %s" , tc . diagCount , len ( diags ) , diags )
}
tfdiags . AssertNoDiagnostics ( t , diags )
if tc . assertChanges != nil {
sch , err := ctx . Schemas ( mod , states . NewState ( ) )
@ -722,15 +945,6 @@ func TestContext2Plan_queryList(t *testing.T) {
}
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 ( ) )
}
}
}
} )
}
}
@ -837,6 +1051,9 @@ func TestContext2Plan_queryListArgs(t *testing.T) {
provider . GetProviderSchemaResponse = getListProviderSchemaResp ( )
var recordedRequest providers . ListResourceRequest
provider . ListResourceFn = func ( request providers . ListResourceRequest ) providers . ListResourceResponse {
if request . Config . IsNull ( ) || request . Config . GetAttr ( "config" ) . IsNull ( ) {
t . Fatalf ( "config should never be null, got null for %s" , request . TypeName )
}
recordedRequest = request
return provider . ListResourceResponse
}