Skip to content

Commit d2c5ed5

Browse files
committed
feat: optimised access tags retrieval for vpc is_images datasource
1 parent 7836604 commit d2c5ed5

File tree

2 files changed

+445
-146
lines changed

2 files changed

+445
-146
lines changed

ibm/flex/structures.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
27282935
func 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

Comments
 (0)