@@ -1042,6 +1042,239 @@ func getListProviderSchemaResp() *providers.GetProviderSchemaResponse {
10421042 })
10431043}
10441044
1045+ func TestContext2Plan_queryListConfigGeneration (t * testing.T ) {
1046+ listResourceFn := func (request providers.ListResourceRequest ) providers.ListResourceResponse {
1047+ instanceTypes := []string {"ami-123456" , "ami-654321" , "ami-789012" }
1048+ madeUp := []cty.Value {}
1049+ for i := range len (instanceTypes ) {
1050+ madeUp = append (madeUp , cty .ObjectVal (map [string ]cty.Value {"instance_type" : cty .StringVal (instanceTypes [i ])}))
1051+ }
1052+
1053+ ids := []cty.Value {}
1054+ for i := range madeUp {
1055+ ids = append (ids , cty .ObjectVal (map [string ]cty.Value {
1056+ "id" : cty .StringVal (fmt .Sprintf ("i-v%d" , i + 1 )),
1057+ }))
1058+ }
1059+
1060+ resp := []cty.Value {}
1061+ for i , v := range madeUp {
1062+ mp := map [string ]cty.Value {
1063+ "identity" : ids [i ],
1064+ "display_name" : cty .StringVal (fmt .Sprintf ("Instance %d" , i + 1 )),
1065+ }
1066+ if request .IncludeResourceObject {
1067+ mp ["state" ] = v
1068+ }
1069+ resp = append (resp , cty .ObjectVal (mp ))
1070+ }
1071+
1072+ ret := map [string ]cty.Value {
1073+ "data" : cty .TupleVal (resp ),
1074+ }
1075+ for k , v := range request .Config .AsValueMap () {
1076+ if k != "data" {
1077+ ret [k ] = v
1078+ }
1079+ }
1080+
1081+ return providers.ListResourceResponse {Result : cty .ObjectVal (ret )}
1082+ }
1083+
1084+ mainConfig := `
1085+ terraform {
1086+ required_providers {
1087+ test = {
1088+ source = "hashicorp/test"
1089+ version = "1.0.0"
1090+ }
1091+ }
1092+ }
1093+ `
1094+ queryConfig := `
1095+ variable "input" {
1096+ type = string
1097+ default = "foo"
1098+ }
1099+
1100+ list "test_resource" "test2" {
1101+ for_each = toset(["§us-east-2", "§us-west-1"])
1102+ provider = test
1103+
1104+ config {
1105+ filter = {
1106+ attr = var.input
1107+ }
1108+ }
1109+ }
1110+ `
1111+
1112+ configFiles := map [string ]string {"main.tf" : mainConfig }
1113+ configFiles ["main.tfquery.hcl" ] = queryConfig
1114+
1115+ mod := testModuleInline (t , configFiles , configs .MatchQueryFiles ())
1116+ providerAddr := addrs .NewDefaultProvider ("test" )
1117+ provider := testProvider ("test" )
1118+ provider .ConfigureProvider (providers.ConfigureProviderRequest {})
1119+ provider .GetProviderSchemaResponse = getListProviderSchemaResp ()
1120+
1121+ var requestConfigs = make (map [string ]cty.Value )
1122+ provider .ListResourceFn = func (request providers.ListResourceRequest ) providers.ListResourceResponse {
1123+ if request .Config .IsNull () || request .Config .GetAttr ("config" ).IsNull () {
1124+ t .Fatalf ("config should never be null, got null for %s" , request .TypeName )
1125+ }
1126+ requestConfigs [request .TypeName ] = request .Config
1127+ return listResourceFn (request )
1128+ }
1129+
1130+ ctx , diags := NewContext (& ContextOpts {
1131+ Providers : map [addrs.Provider ]providers.Factory {
1132+ providerAddr : testProviderFuncFixed (provider ),
1133+ },
1134+ })
1135+ tfdiags .AssertNoDiagnostics (t , diags )
1136+
1137+ diags = ctx .Validate (mod , & ValidateOpts {
1138+ Query : true ,
1139+ })
1140+ tfdiags .AssertNoDiagnostics (t , diags )
1141+
1142+ generatedPath := t .TempDir ()
1143+ plan , diags := ctx .Plan (mod , states .NewState (), & PlanOpts {
1144+ Mode : plans .NormalMode ,
1145+ SetVariables : testInputValuesUnset (mod .Module .Variables ),
1146+ Query : true ,
1147+ GenerateConfigPath : generatedPath ,
1148+ })
1149+ tfdiags .AssertNoDiagnostics (t , diags )
1150+
1151+ sch , err := ctx .Schemas (mod , states .NewState ())
1152+ if err != nil {
1153+ t .Fatalf ("failed to get schemas: %s" , err )
1154+ }
1155+
1156+ expectedResources := []string {
1157+ `list.test_resource.test2["§us-east-2"]` ,
1158+ `list.test_resource.test2["§us-west-1"]` ,
1159+ }
1160+ actualResources := make ([]string , 0 )
1161+ generatedCfgs := make ([]string , 0 )
1162+ uniqCfgs := make (map [string ]struct {})
1163+
1164+ for _ , change := range plan .Changes .Queries {
1165+ actualResources = append (actualResources , change .Addr .String ())
1166+ schema := sch .Providers [providerAddr ].ListResourceTypes [change .Addr .Resource .Resource .Type ]
1167+ cs , err := change .Decode (schema )
1168+ if err != nil {
1169+ t .Fatalf ("failed to decode change: %s" , err )
1170+ }
1171+
1172+ // Verify data. If the state is included, we check that, otherwise we check the id.
1173+ expectedData := []string {"ami-123456" , "ami-654321" , "ami-789012" }
1174+ includeState := change .Addr .String () == "list.test_resource.test"
1175+ if ! includeState {
1176+ expectedData = []string {"i-v1" , "i-v2" , "i-v3" }
1177+ }
1178+ actualData := make ([]string , 0 )
1179+ obj := cs .Results .Value .GetAttr ("data" )
1180+ if obj .IsNull () {
1181+ t .Fatalf ("Expected 'data' attribute to be present, but it is null" )
1182+ }
1183+ obj .ForEachElement (func (key cty.Value , val cty.Value ) bool {
1184+ if includeState {
1185+ val = val .GetAttr ("state" )
1186+ if val .IsNull () {
1187+ t .Fatalf ("Expected 'state' attribute to be present, but it is null" )
1188+ }
1189+ if val .GetAttr ("instance_type" ).IsNull () {
1190+ t .Fatalf ("Expected 'instance_type' attribute to be present, but it is missing" )
1191+ }
1192+ actualData = append (actualData , val .GetAttr ("instance_type" ).AsString ())
1193+ } else {
1194+ val = val .GetAttr ("identity" )
1195+ if val .IsNull () {
1196+ t .Fatalf ("Expected 'identity' attribute to be present, but it is null" )
1197+ }
1198+ if val .GetAttr ("id" ).IsNull () {
1199+ t .Fatalf ("Expected 'id' attribute to be present, but it is missing" )
1200+ }
1201+ actualData = append (actualData , val .GetAttr ("id" ).AsString ())
1202+ }
1203+ return false
1204+ })
1205+ sort .Strings (actualData )
1206+ sort .Strings (expectedData )
1207+ if diff := cmp .Diff (expectedData , actualData ); diff != "" {
1208+ t .Fatalf ("Expected instance types to match, but they differ: %s" , diff )
1209+ }
1210+
1211+ generatedCfgs = append (generatedCfgs , change .Generated .String ())
1212+ uniqCfgs [change .Addr .String ()] = struct {}{}
1213+ }
1214+
1215+ sort .Strings (actualResources )
1216+ sort .Strings (expectedResources )
1217+ if diff := cmp .Diff (expectedResources , actualResources ); diff != "" {
1218+ t .Fatalf ("Expected resources to match, but they differ: %s" , diff )
1219+ }
1220+
1221+ // Verify no managed resources are created
1222+ if len (plan .Changes .Resources ) != 0 {
1223+ t .Fatalf ("Expected no managed resources, but got %d" , len (plan .Changes .Resources ))
1224+ }
1225+
1226+ // Verify generated configs match expected
1227+ expected := `resource "test_resource" "test2_0_0" {
1228+ provider = test
1229+ instance_type = "ami-123456"
1230+ }
1231+
1232+ import {
1233+ to = test_resource.test2_0_0
1234+ provider = test
1235+ identity = {
1236+ id = "i-v1"
1237+ }
1238+ }
1239+
1240+ resource "test_resource" "test2_0_1" {
1241+ provider = test
1242+ instance_type = "ami-654321"
1243+ }
1244+
1245+ import {
1246+ to = test_resource.test2_0_1
1247+ provider = test
1248+ identity = {
1249+ id = "i-v2"
1250+ }
1251+ }
1252+
1253+ resource "test_resource" "test2_0_2" {
1254+ provider = test
1255+ instance_type = "ami-789012"
1256+ }
1257+
1258+ import {
1259+ to = test_resource.test2_0_2
1260+ provider = test
1261+ identity = {
1262+ id = "i-v3"
1263+ }
1264+ }
1265+ `
1266+ joinedCfgs := strings .Join (generatedCfgs , "\n " )
1267+ if ! strings .Contains (joinedCfgs , expected ) {
1268+ t .Fatalf ("Expected config to contain expected resource, but it does not: %s" , cmp .Diff (expected , joinedCfgs ))
1269+ }
1270+
1271+ // Verify that the generated config is valid.
1272+ // The function panics if the config is invalid.
1273+ testModuleInline (t , map [string ]string {
1274+ "main.tf" : strings .Join (generatedCfgs , "\n " ),
1275+ })
1276+ }
1277+
10451278var (
10461279 testResourceCfg = `resource "test_resource" "test_0" {
10471280 provider = test
0 commit comments