@@ -2725,6 +2725,213 @@ func GetTags(d *schema.ResourceData, meta interface{}) error {
27252725// return nil
27262726// }
27272727
2728+ // bulk
2729+ const (
2730+ // Set conservative limit to account for JSON encoding overhead
2731+ maxQueryLength = 900000 // ~900KB
2732+ )
2733+
2734+ // ResourceIdentifier contains the CRN and resource type for bulk tag operations
2735+ type ResourceIdentifier struct {
2736+ CRN string
2737+ ResourceType string
2738+ }
2739+
2740+ // GetGlobalTagsUsingCRNBulk fetches tags for multiple resources using bulk API calls
2741+ // This is significantly more efficient than making individual calls per resource
2742+ // Returns a map of CRN -> tag set for all resources
2743+ func GetGlobalTagsUsingCRNBulk (meta interface {}, resources []ResourceIdentifier , tagType string ) (map [string ]* schema.Set , error ) {
2744+ if len (resources ) == 0 {
2745+ return make (map [string ]* schema.Set ), nil
2746+ }
2747+ // Chunk resources to stay within API size limits
2748+ chunks := chunkResources (resources )
2749+
2750+ // Fetch tags for each chunk and merge results
2751+ allTags := make (map [string ]* schema.Set )
2752+
2753+ for chunkIdx , chunk := range chunks {
2754+ chunkTags , err := fetchTagsForChunk (meta , chunk , tagType )
2755+ if err != nil {
2756+ return nil , fmt .Errorf ("[ERROR] Error fetching tags for chunk %d: %s" , chunkIdx + 1 , err )
2757+ }
2758+
2759+ // Merge chunk results into final map
2760+ for crn , tags := range chunkTags {
2761+ allTags [crn ] = tags
2762+ }
2763+ }
2764+ return allTags , nil
2765+ }
2766+
2767+ // chunkResources splits resources into chunks that fit within API size limits
2768+ // Uses conservative estimation: each CRN ~120 chars, each query part ~130 chars with OR
2769+ func chunkResources (resources []ResourceIdentifier ) [][]ResourceIdentifier {
2770+ if len (resources ) == 0 {
2771+ return nil
2772+ }
2773+
2774+ chunks := [][]ResourceIdentifier {}
2775+ currentChunk := []ResourceIdentifier {}
2776+ currentLength := 0
2777+
2778+ for _ , res := range resources {
2779+ // Estimate query part length: crn:"<CRN>" OR or doc.id:<ID> OR
2780+ var queryPart string
2781+ if strings .Contains (res .ResourceType , "SoftLayer_" ) {
2782+ queryPart = fmt .Sprintf ("doc.id:%s" , res .CRN )
2783+ } else {
2784+ queryPart = fmt .Sprintf ("crn:\" %s\" " , res .CRN )
2785+ }
2786+
2787+ partLength := len (queryPart ) + 4 // +4 for " OR "
2788+
2789+ // Check if adding this resource would exceed limit
2790+ if currentLength > 0 && currentLength + partLength > maxQueryLength {
2791+ // Start new chunk
2792+ chunks = append (chunks , currentChunk )
2793+ currentChunk = []ResourceIdentifier {res }
2794+ currentLength = partLength
2795+ } else {
2796+ // Add to current chunk
2797+ currentChunk = append (currentChunk , res )
2798+ currentLength += partLength
2799+ }
2800+ }
2801+
2802+ // Add final chunk
2803+ if len (currentChunk ) > 0 {
2804+ chunks = append (chunks , currentChunk )
2805+ }
2806+
2807+ return chunks
2808+ }
2809+
2810+ // fetchTagsForChunk fetches tags for a single chunk of resources
2811+ func fetchTagsForChunk (meta interface {}, resources []ResourceIdentifier , tagType string ) (map [string ]* schema.Set , error ) {
2812+ // Initialize Global Search client
2813+ gsClient , err := meta .(conns.ClientSession ).GlobalSearchAPIV2 ()
2814+ if err != nil {
2815+ return nil , fmt .Errorf ("[ERROR] Error getting global search client settings: %s" , err )
2816+ }
2817+
2818+ // Build bulk query for this chunk
2819+ query := buildBulkQuery (resources )
2820+
2821+ // Configure search options
2822+ options := globalsearchv2.SearchOptions {
2823+ Query : core .StringPtr (query ),
2824+ Fields : []string {"crn" , "access_tags" , "tags" , "service_tags" },
2825+ Limit : core .Int64Ptr (500 ), // Maximum allowed by API
2826+ }
2827+
2828+ // For service tags, account ID is required
2829+ if tagType == "service" {
2830+ userDetails , err := meta .(conns.ClientSession ).BluemixUserDetails ()
2831+ if err != nil {
2832+ return nil , fmt .Errorf ("[ERROR] Error getting user details: %s" , err )
2833+ }
2834+ options .SetAccountID (userDetails .UserAccount )
2835+ }
2836+
2837+ // Fetch all pages of results
2838+ allItems := []globalsearchv2.ResultItem {}
2839+ pageCount := 0
2840+
2841+ for {
2842+ pageCount ++
2843+ result , resp , err := gsClient .Search (& options )
2844+ if err != nil {
2845+ return nil , fmt .Errorf ("[ERROR] Error to query the tags for the resource: %s %s" , err , resp )
2846+ }
2847+
2848+ allItems = append (allItems , result .Items ... )
2849+
2850+ // Check if more pages exist
2851+ if result .SearchCursor == nil || len (result .Items ) < int (* result .Limit ) {
2852+ break
2853+ }
2854+
2855+ options .SearchCursor = result .SearchCursor
2856+ }
2857+
2858+ // Map results back to CRNs
2859+ tagMap := mapTagsToResources (allItems , tagType )
2860+
2861+ return tagMap , nil
2862+ }
2863+
2864+ // buildBulkQuery constructs a Global Search query for multiple resources
2865+ // Supports both standard CRN-based queries and SoftLayer doc.id queries
2866+ func buildBulkQuery (resources []ResourceIdentifier ) string {
2867+ queries := make ([]string , 0 , len (resources ))
2868+ hasSoftLayer := false
2869+
2870+ for _ , res := range resources {
2871+ if strings .Contains (res .ResourceType , "SoftLayer_" ) {
2872+ hasSoftLayer = true
2873+ queries = append (queries , fmt .Sprintf ("doc.id:%s" , res .CRN ))
2874+ } else {
2875+ queries = append (queries , fmt .Sprintf ("crn:\" %s\" " , res .CRN ))
2876+ }
2877+ }
2878+
2879+ query := strings .Join (queries , " OR " )
2880+
2881+ // Add family filter for SoftLayer resources
2882+ if hasSoftLayer {
2883+ query = fmt .Sprintf ("(%s) AND family:ims" , query )
2884+ }
2885+
2886+ return query
2887+ }
2888+
2889+ // mapTagsToResources extracts tags from Global Search results and maps them to CRNs
2890+ // Uses direct field access (item.CRN) rather than GetProperty due to API response structure
2891+ func mapTagsToResources (items []globalsearchv2.ResultItem , tagType string ) map [string ]* schema.Set {
2892+ result := make (map [string ]* schema.Set )
2893+
2894+ for itemIdx , item := range items {
2895+ // Extract CRN using direct field access
2896+ // Note: GetProperty("crn") does not work - must use item.CRN
2897+ if item .CRN == nil {
2898+ log .Printf ("[WARN] Item %d missing CRN field, skipping" , itemIdx + 1 )
2899+ continue
2900+ }
2901+
2902+ crn := * item .CRN
2903+
2904+ // Extract tags based on type
2905+ var taglist []string
2906+ var tagProperty interface {}
2907+
2908+ switch tagType {
2909+ case "access" :
2910+ tagProperty = item .GetProperty ("access_tags" )
2911+ case "service" :
2912+ tagProperty = item .GetProperty ("service_tags" )
2913+ default :
2914+ tagProperty = item .GetProperty ("tags" )
2915+ }
2916+
2917+ // Process tags if present
2918+ if tagProperty != nil && reflect .TypeOf (tagProperty ).Kind () == reflect .Slice {
2919+ tagSlice := reflect .ValueOf (tagProperty )
2920+
2921+ for i := 0 ; i < tagSlice .Len (); i ++ {
2922+ tag := fmt .Sprintf ("%s" , tagSlice .Index (i ))
2923+ taglist = append (taglist , tag )
2924+ }
2925+
2926+ }
2927+
2928+ // Store in result map
2929+ result [crn ] = NewStringSet (ResourceIBMVPCHash , taglist )
2930+ }
2931+
2932+ return result
2933+ }
2934+
27282935func GetGlobalTagsUsingCRN (meta interface {}, resourceID , resourceType , tagType string ) (* schema.Set , error ) {
27292936 taggingResult , err := GetGlobalTagsUsingSearchAPI (meta , resourceID , resourceType , tagType )
27302937 if err != nil {
0 commit comments