Skip to content

Commit 8f8658d

Browse files
dylanratcliffeactions-user
authored andcommitted
Fixed mappings for a number of broken adapters (#3782)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Touches adapter query/mapping behavior that affects resource resolution and caching for BigQuery/KMS/IAM keys; changes are localized but could alter how Terraform IDs resolve (SEARCH vs GET) if mappings or interception assumptions are wrong. > > **Overview** > Improves Terraform interoperability for multiple GCP manual adapters by switching their `TerraformMappings` from `GET`/name-based fields to `SEARCH` using the resource `.id` field, relying on the framework’s full-path (`projects/...`) interception to perform `GET` where appropriate. > > Adds missing `SearchStream` implementation for `BigQueryRoutine` (including caching of streamed items), and introduces Terraform-style and legacy-format search tests across BigQuery Routine and Cloud KMS adapters (`CryptoKey`, `CryptoKeyVersion`, `KeyRing`) to validate both ID parsing and cache key behavior. Also enables Terraform mappings for `CloudKMSCryptoKey` (previously `nil`) and updates mappings for `BigQueryTable` and `IAMServiceAccountKey`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9a43c6bf2230c9140ae400ac610abc3ec1899ad7. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> GitOrigin-RevId: b5163225ab07244d89ba567d10d5b0834959944b
1 parent 1b86e3b commit 8f8658d

10 files changed

Lines changed: 477 additions & 17 deletions

sources/gcp/manual/big-query-routine.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ func (b BigQueryRoutineWrapper) PotentialLinks() map[shared.ItemType]bool {
5757
func (b BigQueryRoutineWrapper) TerraformMappings() []*sdp.TerraformMapping {
5858
return []*sdp.TerraformMapping{
5959
{
60-
TerraformMethod: sdp.QueryMethod_GET,
60+
TerraformMethod: sdp.QueryMethod_SEARCH,
6161
// https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_routine
62-
// projects/{{project}}/datasets/{{dataset_id}}/routines/{{routine_id}}
63-
TerraformQueryMap: "google_bigquery_routine.routine_id",
62+
// ID format: projects/{{project}}/datasets/{{dataset_id}}/routines/{{routine_id}}
63+
// The framework automatically intercepts queries starting with "projects/" and converts
64+
// them to GET operations by extracting the last N path parameters (based on GetLookups count).
65+
TerraformQueryMap: "google_bigquery_routine.id",
6466
},
6567
}
6668
}
@@ -121,7 +123,32 @@ func (b BigQueryRoutineWrapper) Search(ctx context.Context, scope string, queryP
121123
}
122124

123125
func (b BigQueryRoutineWrapper) SearchStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string, queryParts ...string) {
124-
// SearchStream not implemented for BigQueryRoutine
126+
location, err := b.LocationFromScope(scope)
127+
if err != nil {
128+
stream.SendError(&sdp.QueryError{
129+
ErrorType: sdp.QueryError_NOSCOPE,
130+
ErrorString: err.Error(),
131+
})
132+
return
133+
}
134+
135+
toItem := func(metadata *bigquery.RoutineMetadata, datasetID, routineID string) (*sdp.Item, *sdp.QueryError) {
136+
item, qerr := b.gcpBigQueryRoutineToItem(metadata, datasetID, routineID, location)
137+
if qerr == nil && item != nil {
138+
cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey)
139+
}
140+
return item, qerr
141+
}
142+
143+
items, listErr := b.client.List(ctx, location.ProjectID, queryParts[0], toItem)
144+
if listErr != nil {
145+
stream.SendError(gcpshared.QueryError(listErr, scope, b.Type()))
146+
return
147+
}
148+
149+
for _, item := range items {
150+
stream.SendItem(item)
151+
}
125152
}
126153

127154
func (b BigQueryRoutineWrapper) gcpBigQueryRoutineToItem(metadata *bigquery.RoutineMetadata, datasetID, routineID string, location gcpshared.LocationInfo) (*sdp.Item, *sdp.QueryError) {

sources/gcp/manual/big-query-routine_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,71 @@ func TestBigQueryRoutine(t *testing.T) {
183183
t.Fatalf("Expected error, got nil")
184184
}
185185
})
186+
187+
t.Run("Search with terraform format", func(t *testing.T) {
188+
wrapper := manual.NewBigQueryRoutine(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)})
189+
adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache())
190+
191+
// Use terraform-style path format
192+
terraformStyleQuery := "projects/test-project/datasets/test_dataset/routines/test_routine"
193+
194+
// Mock Get (called internally when terraform format is detected)
195+
mockClient.EXPECT().Get(ctx, projectID, datasetID, routineID).Return(createRoutineMetadata("terraform format test"), nil)
196+
197+
searchable, ok := adapter.(discovery.SearchableAdapter)
198+
if !ok {
199+
t.Fatalf("Adapter does not support Search operation")
200+
}
201+
202+
items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], terraformStyleQuery, true)
203+
if qErr != nil {
204+
t.Fatalf("Expected no error with terraform format, got: %v", qErr)
205+
}
206+
if len(items) != 1 {
207+
t.Fatalf("Expected 1 item, got: %d", len(items))
208+
}
209+
if items[0].GetType() != gcpshared.BigQueryRoutine.String() {
210+
t.Fatalf("Expected type %s, got: %s", gcpshared.BigQueryRoutine.String(), items[0].GetType())
211+
}
212+
})
213+
214+
t.Run("Search with legacy pipe format", func(t *testing.T) {
215+
wrapper := manual.NewBigQueryRoutine(mockClient, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)})
216+
adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache())
217+
218+
// Use legacy dataset ID format
219+
legacyQuery := datasetID
220+
221+
// Mock the List function
222+
mockClient.EXPECT().List(
223+
gomock.Any(),
224+
projectID,
225+
datasetID,
226+
gomock.Any(),
227+
).DoAndReturn(func(ctx context.Context, projectID string, datasetID string, converter func(routine *bigquery.RoutineMetadata, datasetID, routineID string) (*sdp.Item, *sdp.QueryError)) ([]*sdp.Item, *sdp.QueryError) {
228+
items := make([]*sdp.Item, 0, 1)
229+
routine := createRoutineMetadata("legacy format test")
230+
item, qErr := converter(routine, datasetID, routineID)
231+
if qErr != nil {
232+
return nil, qErr
233+
}
234+
items = append(items, item)
235+
return items, nil
236+
})
237+
238+
searchable, ok := adapter.(discovery.SearchableAdapter)
239+
if !ok {
240+
t.Fatalf("Adapter does not support Search operation")
241+
}
242+
243+
items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], legacyQuery, true)
244+
if qErr != nil {
245+
t.Fatalf("Expected no error with legacy format, got: %v", qErr)
246+
}
247+
if len(items) != 1 {
248+
t.Fatalf("Expected 1 item, got: %d", len(items))
249+
}
250+
})
186251
}
187252

188253
func createRoutineMetadata(description string) *bigquery.RoutineMetadata {

sources/gcp/manual/big-query-table.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@ func (b BigQueryTableWrapper) PotentialLinks() map[shared.ItemType]bool {
6161
func (b BigQueryTableWrapper) TerraformMappings() []*sdp.TerraformMapping {
6262
return []*sdp.TerraformMapping{
6363
{
64-
TerraformMethod: sdp.QueryMethod_GET,
64+
TerraformMethod: sdp.QueryMethod_SEARCH,
6565
// https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table
66-
// projects/{{project}}/datasets/{{dataset}}/tables/{{name}}
66+
// ID format: projects/{{project}}/datasets/{{dataset}}/tables/{{name}}
67+
// The framework automatically intercepts queries starting with "projects/" and converts
68+
// them to GET operations by extracting the last N path parameters (based on GetLookups count).
6769
TerraformQueryMap: "google_bigquery_table.id",
6870
},
6971
}

sources/gcp/manual/cloud-kms-crypto-key-version.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ func (c cloudKMSCryptoKeyVersionWrapper) PotentialLinks() map[shared.ItemType]bo
5555
func (c cloudKMSCryptoKeyVersionWrapper) TerraformMappings() []*sdp.TerraformMapping {
5656
return []*sdp.TerraformMapping{
5757
{
58-
TerraformMethod: sdp.QueryMethod_GET,
58+
TerraformMethod: sdp.QueryMethod_SEARCH,
59+
// https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_crypto_key_version
60+
// ID format: projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{cryptoKey}/cryptoKeyVersions/{version}
61+
// The framework automatically intercepts queries starting with "projects/" and converts
62+
// them to GET operations by extracting the last N path parameters (based on GetLookups count).
5963
TerraformQueryMap: "google_kms_crypto_key_version.id",
6064
},
6165
}

sources/gcp/manual/cloud-kms-crypto-key-version_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,126 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) {
206206
}
207207
})
208208

209+
t.Run("Search_TerraformFormat", func(t *testing.T) {
210+
cache := sdpcache.NewCache(ctx)
211+
defer cache.Clear()
212+
213+
// Pre-populate cache with CryptoKeyVersion items under SEARCH cache key (by cryptoKey)
214+
attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{
215+
"name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1",
216+
"uniqueAttr": "us-central1|my-keyring|my-key|1",
217+
})
218+
_ = attrs1.Set("uniqueAttr", "us-central1|my-keyring|my-key|1")
219+
220+
attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{
221+
"name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/2",
222+
"uniqueAttr": "us-central1|my-keyring|my-key|2",
223+
})
224+
_ = attrs2.Set("uniqueAttr", "us-central1|my-keyring|my-key|2")
225+
226+
item1 := &sdp.Item{
227+
Type: gcpshared.CloudKMSCryptoKeyVersion.String(),
228+
UniqueAttribute: "uniqueAttr",
229+
Attributes: attrs1,
230+
Scope: projectID,
231+
Health: sdp.Health_HEALTH_OK.Enum(),
232+
}
233+
item2 := &sdp.Item{
234+
Type: gcpshared.CloudKMSCryptoKeyVersion.String(),
235+
UniqueAttribute: "uniqueAttr",
236+
Attributes: attrs2,
237+
Scope: projectID,
238+
Health: sdp.Health_HEALTH_OK.Enum(),
239+
}
240+
241+
// Search by location|keyRing|cryptoKey (what the terraform format will be converted to)
242+
searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), "us-central1|my-keyring|my-key")
243+
cache.StoreItem(ctx, item1, shared.DefaultCacheDuration, searchCacheKey)
244+
cache.StoreItem(ctx, item2, shared.DefaultCacheDuration, searchCacheKey)
245+
246+
loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)})
247+
248+
wrapper := manual.NewCloudKMSCryptoKeyVersion(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)})
249+
adapter := sources.WrapperToAdapter(wrapper, cache)
250+
251+
searchable, ok := adapter.(discovery.SearchableAdapter)
252+
if !ok {
253+
t.Fatalf("Adapter does not support Search operation")
254+
}
255+
256+
// Use terraform-style path format
257+
terraformStyleQuery := "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1"
258+
259+
items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], terraformStyleQuery, false)
260+
if qErr != nil {
261+
t.Fatalf("Expected no error with terraform format, got: %v", qErr)
262+
}
263+
264+
// Verify we got at least one item back
265+
if len(items) == 0 {
266+
t.Fatalf("Expected at least 1 item with terraform format, got: %d", len(items))
267+
}
268+
269+
// Verify the items have the expected unique attributes
270+
foundVersion1 := false
271+
for _, item := range items {
272+
uniqueAttr, err := item.GetAttributes().Get("uniqueAttr")
273+
if err == nil && (uniqueAttr == "us-central1|my-keyring|my-key|1" || uniqueAttr == "us-central1|my-keyring|my-key|2") {
274+
if uniqueAttr == "us-central1|my-keyring|my-key|1" {
275+
foundVersion1 = true
276+
}
277+
}
278+
}
279+
280+
if !foundVersion1 {
281+
t.Fatalf("Expected to find version 1 in results")
282+
}
283+
})
284+
285+
t.Run("Search_LegacyPipeFormat", func(t *testing.T) {
286+
cache := sdpcache.NewCache(ctx)
287+
defer cache.Clear()
288+
289+
// Pre-populate cache with CryptoKeyVersion items
290+
attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{
291+
"name": "projects/test-project-id/locations/europe-west1/keyRings/prod-keyring/cryptoKeys/prod-key/cryptoKeyVersions/1",
292+
"uniqueAttr": "europe-west1|prod-keyring|prod-key|1",
293+
})
294+
_ = attrs1.Set("uniqueAttr", "europe-west1|prod-keyring|prod-key|1")
295+
296+
item1 := &sdp.Item{
297+
Type: gcpshared.CloudKMSCryptoKeyVersion.String(),
298+
UniqueAttribute: "uniqueAttr",
299+
Attributes: attrs1,
300+
Scope: projectID,
301+
Health: sdp.Health_HEALTH_OK.Enum(),
302+
}
303+
304+
// Search by location|keyRing|cryptoKey (legacy format)
305+
searchCacheKey := sdpcache.CacheKeyFromParts("gcp-source", sdp.QueryMethod_SEARCH, projectID, gcpshared.CloudKMSCryptoKeyVersion.String(), "europe-west1|prod-keyring|prod-key")
306+
cache.StoreItem(ctx, item1, shared.DefaultCacheDuration, searchCacheKey)
307+
308+
loader := gcpshared.NewCloudKMSAssetLoader(nil, projectID, cache, "gcp-source", []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)})
309+
310+
wrapper := manual.NewCloudKMSCryptoKeyVersion(loader, []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)})
311+
adapter := sources.WrapperToAdapter(wrapper, cache)
312+
313+
searchable, ok := adapter.(discovery.SearchableAdapter)
314+
if !ok {
315+
t.Fatalf("Adapter does not support Search operation")
316+
}
317+
318+
// Use legacy pipe-separated format with multiple query parts
319+
items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], "europe-west1|prod-keyring|prod-key", false)
320+
if qErr != nil {
321+
t.Fatalf("Expected no error with legacy format, got: %v", qErr)
322+
}
323+
324+
if len(items) != 1 {
325+
t.Fatalf("Expected 1 item with legacy format, got: %d", len(items))
326+
}
327+
})
328+
209329
t.Run("StaticTests", func(t *testing.T) {
210330
cache := sdpcache.NewMemoryCache()
211331
defer cache.Clear()

sources/gcp/manual/cloud-kms-crypto-key.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,16 @@ func (c cloudKMSCryptoKeyWrapper) PotentialLinks() map[shared.ItemType]bool {
5555

5656
// TerraformMappings returns the Terraform mappings for the CryptoKey wrapper.
5757
func (c cloudKMSCryptoKeyWrapper) TerraformMappings() []*sdp.TerraformMapping {
58-
// TODO: Revisit this when working on this ticket:
59-
// https://linear.app/overmind/issue/ENG-706/fix-terraform-mappings-for-crypto-key
60-
return nil
58+
return []*sdp.TerraformMapping{
59+
{
60+
TerraformMethod: sdp.QueryMethod_SEARCH,
61+
// https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_crypto_key
62+
// ID format: projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{name}}
63+
// The framework automatically intercepts queries starting with "projects/" and converts
64+
// them to GET operations by extracting the last N path parameters (based on GetLookups count).
65+
TerraformQueryMap: "google_kms_crypto_key.id",
66+
},
67+
}
6168
}
6269

6370
// GetLookups returns the lookups for the CryptoKey wrapper.

0 commit comments

Comments
 (0)