@@ -254,25 +254,74 @@ func testZoneInRegion(bp config.Blueprint, inputs config.Dict) error {
254254 return TestZoneInRegion (m ["project_id" ], m ["zone" ], m ["region" ])
255255}
256256
257- // findReservationInOtherZones searches for a reservation by name across zones
258- // in the project.
259- func findReservationInOtherZones (s * compute.Service , projectID string , name string ) ([]string , error ) {
260- aggList , err := s .Reservations .AggregatedList (projectID ).Do ()
261- if err != nil {
262- return nil , err
263- }
257+ // Helper interface to treat Standard and Future reservations generically
258+ type zoneResource interface {
259+ GetName () string
260+ GetZone () string
261+ }
262+
263+ // Wrapper for compute.Reservation
264+ type stdRes struct { * compute.Reservation }
265+
266+ func (r stdRes ) GetName () string { return r .Name }
267+ func (r stdRes ) GetZone () string { return r .Zone }
268+
269+ // Wrapper for compute.FutureReservation
270+ type futRes struct { * compute.FutureReservation }
271+
272+ func (r futRes ) GetName () string { return r .Name }
273+ func (r futRes ) GetZone () string { return r .Zone }
264274
275+ func extractZonesFromItems [T any ](items map [string ]T , name string , extractor func (T ) []zoneResource ) []string {
265276 foundInZones := []string {}
266- for _ , scopedList := range aggList .Items {
267- for _ , res := range scopedList .Reservations {
268- if res .Name == name {
269- // res.Zone is a full URL, extract just the name (e.g., "us-central1-a")
270- parts := strings .Split (res .Zone , "/" )
277+ for _ , scopedList := range items {
278+ for _ , res := range extractor (scopedList ) {
279+ if res .GetName () == name {
280+ parts := strings .Split (res .GetZone (), "/" )
271281 foundInZones = append (foundInZones , parts [len (parts )- 1 ])
272282 }
273283 }
274284 }
275- return foundInZones , nil
285+ return foundInZones
286+ }
287+
288+ func findReservationInOtherZones (ctx context.Context , s * compute.Service , projectID string , name string ) ([]string , error ) {
289+ // 1. Search Standard Zonal Reservations
290+ aggList , err := s .Reservations .AggregatedList (projectID ).Context (ctx ).Do ()
291+ if err == nil {
292+ found := extractZonesFromItems (aggList .Items , name , func (l compute.ReservationsScopedList ) []zoneResource {
293+ res := make ([]zoneResource , len (l .Reservations ))
294+ for i , r := range l .Reservations {
295+ res [i ] = stdRes {r }
296+ }
297+ return res
298+ })
299+ if len (found ) > 0 {
300+ return found , nil
301+ }
302+ }
303+
304+ // 2. Search Future Reservations (Early return if Standard found, otherwise search here)
305+ fAggList , fErr := s .FutureReservations .AggregatedList (projectID ).Context (ctx ).Do ()
306+ if fErr == nil {
307+ found := extractZonesFromItems (fAggList .Items , name , func (l compute.FutureReservationsScopedList ) []zoneResource {
308+ res := make ([]zoneResource , len (l .FutureReservations ))
309+ for i , r := range l .FutureReservations {
310+ res [i ] = futRes {r }
311+ }
312+ return res
313+ })
314+ if len (found ) > 0 {
315+ return found , nil
316+ }
317+ }
318+
319+ // If both failed and we found nothing, return the errors
320+ if err != nil || fErr != nil {
321+ return nil , fmt .Errorf ("failed to list standard reservations: %v; failed to list future reservations: %v" , err , fErr )
322+ }
323+
324+ return []string {}, nil
276325}
277326
278327// TestReservationExists checks if a reservation exists in a project and zone.
@@ -286,21 +335,35 @@ func TestReservationExists(ctx context.Context, reservationProjectID string, zon
286335 return handleClientError (err )
287336 }
288337
289- // 1. Direct check: Try to Get the specific reservation
290- _ , err = s .Reservations .Get (reservationProjectID , zone , reservationName ).Do ()
338+ // 1. Direct check: Try Standard Zonal Reservation
339+ _ , err = s .Reservations .Get (reservationProjectID , zone , reservationName ).Context ( ctx ). Do ()
291340 if err == nil {
292- return nil // Success
341+ return nil
293342 }
294343
295- // 2. Access Check: If we can't even reach the project/API, issue soft warning
344+ // 2. Fallback: Try Future Reservation (Required for Blackwell/A4 hardware)
345+ _ , fErr := s .FutureReservations .Get (reservationProjectID , zone , reservationName ).Context (ctx ).Do ()
346+ if fErr == nil {
347+ return nil
348+ }
349+
350+ // 3. Access Check: If both failed, check for metadata blindness (403/400).
351+ // We handle this because users might be allowed to CONSUME but not DESCRIBE a shared reservation.
352+ // Case A: Standard API Access Check
296353 if msg , isSoft := getSoftWarningMessage (err , "test_reservation_exists" , reservationProjectID , "Compute Engine API" , "compute.reservations.get" ); isSoft {
297354 fmt .Println (msg )
298- return nil // Skip and continue
355+ return nil
356+ }
357+
358+ // Case B: Future API Access Check
359+ if msg , isSoft := getSoftWarningMessage (fErr , "test_reservation_exists" , reservationProjectID , "Compute Engine API" , "compute.futureReservations.get" ); isSoft {
360+ fmt .Println (msg )
361+ return nil
299362 }
300363
301- // 3 . Diagnostic Search: The reservation was not in the expected zone (404).
364+ // 4 . Diagnostic Search: The reservation was not in the expected zone (404).
302365 // We try to find where it actually is.
303- foundInZones , aggErr := findReservationInOtherZones (s , reservationProjectID , reservationName )
366+ foundInZones , aggErr := findReservationInOtherZones (ctx , s , reservationProjectID , reservationName )
304367
305368 if aggErr != nil {
306369 // If Discovery fails (403/400) and it's a SHARED project, we must skip
@@ -320,7 +383,7 @@ func TestReservationExists(ctx context.Context, reservationProjectID string, zon
320383 return fmt .Errorf ("reservation %q not found in project %q and zone %q" , reservationName , reservationProjectID , zone )
321384 }
322385
323- // 4 . Resource Found Discovery: If we found it elsewhere, provide a Hard Failure with Hint.
386+ // 5 . Resource Found Discovery: Provide Hint
324387 if len (foundInZones ) > 0 {
325388 zonesList := strings .Join (foundInZones , ", " )
326389 return config.HintError {
@@ -331,7 +394,7 @@ func TestReservationExists(ctx context.Context, reservationProjectID string, zon
331394 }
332395 }
333396
334- // 5 . Not Found Anywhere: Hard Failure
397+ // 6 . Not Found Anywhere: Hard Failure
335398 return fmt .Errorf ("reservation %q was not found in any zone of project %q" , reservationName , reservationProjectID )
336399}
337400
@@ -368,7 +431,7 @@ func testReservationExists(bp config.Blueprint, inputs config.Dict) error {
368431 targetName = matches [2 ]
369432 }
370433
371- // Pass both the owner project and the deployment project
434+ // Pass context from the caller to ensure cancellation/timeouts are respected
372435 ctx := context .Background ()
373436 return TestReservationExists (ctx , reservationProjectID , zone , targetName , deploymentProjectID )
374437}
0 commit comments