@@ -23,6 +23,9 @@ import (
2323 "google.golang.org/api/option"
2424 run "google.golang.org/api/run/v1"
2525 "google.golang.org/api/storage/v1"
26+ "google.golang.org/genproto/googleapis/rpc/errdetails"
27+ "google.golang.org/grpc/codes"
28+ "google.golang.org/grpc/status"
2629 "google.golang.org/protobuf/types/known/structpb"
2730 "google.golang.org/protobuf/types/known/timestamppb"
2831)
@@ -383,7 +386,7 @@ func (p *OrganizationProvider) Resources(ctx context.Context) (*schema.Resources
383386 gologger .Info ().Msgf ("Found 'all' service, starting comprehensive asset discovery" )
384387 allAssets , err := p .getAllAssets (ctx , parent )
385388 if err != nil {
386- gologger .Warning ().Msgf ("Could not get all assets: %s" , err )
389+ gologger .Info ().Msgf ("Could not get all assets: %s" , err )
387390 } else {
388391 finalResources .Merge (allAssets )
389392 }
@@ -392,7 +395,7 @@ func (p *OrganizationProvider) Resources(ctx context.Context) (*schema.Resources
392395 for _ , service := range p .services .Keys () {
393396 assets , err := p .getAssetsForService (ctx , parent , service )
394397 if err != nil {
395- gologger .Warning ().Msgf ("Could not get assets for service %s: %s" , service , err )
398+ gologger .Info ().Msgf ("Could not get assets for service %s: %s" , service , err )
396399 } else {
397400 finalResources .Merge (assets )
398401 }
@@ -456,16 +459,51 @@ func (p *OrganizationProvider) getAssetsForTypes(ctx context.Context, parent str
456459 resources := schema .NewResources ()
457460 it := p .assetClient .ListAssets (ctx , req )
458461
459- // Collect all assets first
462+ // Collect all assets, with retry on rate limit errors and partial result preservation
463+ const maxRateLimitRetries = 10
464+ const rateLimitWait = 90 * time .Second
460465 var assetInfos []assetInfo
466+ rateLimitRetries := 0
461467
468+ pagination:
462469 for {
463470 asset , err := it .Next ()
464471 if err != nil {
465472 if errors .Is (err , iterator .Done ) {
466473 break
467474 }
468- return nil , err
475+
476+ // On rate limit, wait and resume from where we left off
477+ if isRateLimit , retryDelay := rateLimitInfo (err ); isRateLimit && rateLimitRetries < maxRateLimitRetries {
478+ rateLimitRetries ++
479+ pageToken := it .PageInfo ().Token
480+
481+ wait := rateLimitWait
482+ if retryDelay > 0 {
483+ wait = retryDelay
484+ }
485+ gologger .Info ().Msgf ("Rate limit hit after %d assets, waiting %s before retry (%d/%d)" ,
486+ len (assetInfos ), wait , rateLimitRetries , maxRateLimitRetries )
487+
488+ if pageToken == "" {
489+ break
490+ }
491+
492+ select {
493+ case <- time .After (wait ):
494+ case <- ctx .Done ():
495+ break pagination
496+ }
497+
498+ // Resume with a new iterator using the page token
499+ req .PageToken = pageToken
500+ it = p .assetClient .ListAssets (ctx , req )
501+ continue
502+ }
503+
504+ // Non-rate-limit error or max retries exceeded: return partial results
505+ gologger .Info ().Msgf ("ListAssets pagination stopped after %d assets: %s" , len (assetInfos ), err )
506+ break
469507 }
470508
471509 resource := p .parseAssetToResource (asset )
@@ -678,6 +716,22 @@ func newOrganizationProvider(options schema.OptionBlock, id, JSONData, organizat
678716 return provider , nil
679717}
680718
719+ // rateLimitInfo returns whether the error is a rate limit error and the suggested retry delay
720+ func rateLimitInfo (err error ) (bool , time.Duration ) {
721+ s , ok := status .FromError (err )
722+ if ! ok || s .Code () != codes .ResourceExhausted {
723+ return false , 0
724+ }
725+ for _ , detail := range s .Details () {
726+ if retryInfo , ok := detail .(* errdetails.RetryInfo ); ok {
727+ if d := retryInfo .GetRetryDelay (); d != nil {
728+ return true , d .AsDuration ()
729+ }
730+ }
731+ }
732+ return true , 0
733+ }
734+
681735// Helper functions to reduce nesting and improve readability
682736
683737func getStringField (data * structpb.Struct , fieldNames ... string ) string {
0 commit comments