github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/filter_engine.go (about) 1 package govcd 2 3 /* 4 * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. 5 */ 6 7 import ( 8 "fmt" 9 "os" 10 "regexp" 11 "strings" 12 "time" 13 14 "github.com/kr/pretty" 15 16 "github.com/vmware/go-vcloud-director/v2/types/v56" 17 "github.com/vmware/go-vcloud-director/v2/util" 18 ) 19 20 type queryWithMetadataFunc func(queryType string, params, notEncodedParams map[string]string, 21 metadataFields []string, isSystem bool) (Results, error) 22 23 type queryByMetadataFunc func(queryType string, params, notEncodedParams map[string]string, 24 metadataFilters map[string]MetadataFilter, isSystem bool) (Results, error) 25 26 type resultsConverterFunc func(queryType string, results Results) ([]QueryItem, error) 27 28 // searchByFilter is a generic filter that can operate on entities that implement the QueryItem interface 29 // It requires a queryType and a set of criteria. 30 // Returns a list of QueryItem interface elements, which can be cast back to the wanted real type 31 // Also returns a human readable text of the conditions being passed and how they matched the data found 32 func searchByFilter(queryByMetadata queryByMetadataFunc, queryWithMetadataFields queryWithMetadataFunc, 33 converter resultsConverterFunc, queryType string, criteria *FilterDef) ([]QueryItem, string, error) { 34 35 // Set of conditions to be evaluated (will be filled from criteria) 36 var conditions []conditionDef 37 // List of candidate items that match all conditions 38 var candidatesByConditions []QueryItem 39 40 // List of metadata fields that will be added to the query 41 var metadataFields []string 42 43 // If set, metadata fields will be passed as 'metadata@SYSTEM:fieldName' 44 var isSystem bool 45 var params = make(map[string]string) 46 47 // Will search the latest item if requested 48 searchLatest := false 49 // Will search the earliest item if requested 50 searchEarliest := false 51 52 // A null filter is converted into an empty object. 53 // Using an empty filter is equivalent to fetching all items without filtering 54 if criteria == nil { 55 criteria = &FilterDef{} 56 } 57 58 // A text containing the human-readable form of the criteria being used, and the detail on how they matched the 59 // data being fetched 60 explanation := conditionText(criteria) 61 62 // A collection of matching information for the conditions being applied 63 var matches []matchResult 64 65 // Parse criteria and build the condition list 66 for key, value := range criteria.Filters { 67 // Empty values could be leftovers from the criteria build-up prior to calling this function 68 if value == "" { 69 continue 70 } 71 switch key { 72 case types.FilterNameRegex: 73 re, err := regexp.Compile(value) 74 if err != nil { 75 return nil, explanation, fmt.Errorf("error compiling regular expression '%s' : %s ", value, err) 76 } 77 conditions = append(conditions, conditionDef{key, nameCondition{re}}) 78 case types.FilterDate: 79 conditions = append(conditions, conditionDef{key, dateCondition{value}}) 80 case types.FilterIp: 81 re, err := regexp.Compile(value) 82 if err != nil { 83 return nil, explanation, fmt.Errorf("error compiling regular expression '%s' : %s ", value, err) 84 } 85 conditions = append(conditions, conditionDef{key, ipCondition{re}}) 86 case types.FilterParent: 87 conditions = append(conditions, conditionDef{key, parentCondition{value}}) 88 case types.FilterParentId: 89 conditions = append(conditions, conditionDef{key, parentIdCondition{value}}) 90 91 case types.FilterLatest: 92 searchLatest = stringToBool(value) 93 94 case types.FilterEarliest: 95 searchEarliest = stringToBool(value) 96 97 default: 98 return nil, explanation, fmt.Errorf("[SearchByFilter] filter '%s' not supported (only allowed %v)", key, supportedFilters) 99 } 100 } 101 102 // We can't allow the search for both the oldest and the newest item 103 if searchEarliest && searchLatest { 104 return nil, explanation, fmt.Errorf("only one of '%s' or '%s' can be used for a set of criteria", types.FilterEarliest, types.FilterLatest) 105 } 106 107 var metadataFilter = make(map[string]MetadataFilter) 108 // Fill metadata filters 109 if len(criteria.Metadata) > 0 { 110 for _, cond := range criteria.Metadata { 111 k := cond.Key 112 v := cond.Value 113 isSystem = cond.IsSystem 114 if k == "" { 115 return nil, explanation, fmt.Errorf("metadata condition without key detected") 116 } 117 if v == "" { 118 return nil, explanation, fmt.Errorf("empty value for metadata condition with key '%s'", k) 119 } 120 121 // If we use the metadata search through the API, we must make sure that the type is set 122 if criteria.UseMetadataApiFilter { 123 if cond.Type == "" || strings.EqualFold(cond.Type, "none") { 124 return nil, explanation, fmt.Errorf("requested search by metadata field '%s' must provide a valid type", cond.Key) 125 } 126 127 // The type must be one of the expected values 128 err := validateMetadataType(cond.Type) 129 if err != nil { 130 return nil, explanation, fmt.Errorf("type '%s' for metadata field '%s' is invalid. :%s", cond.Type, cond.Key, err) 131 } 132 metadataFilter[cond.Key] = MetadataFilter{ 133 Type: cond.Type, 134 Value: fmt.Sprintf("%v", cond.Value), 135 } 136 } 137 138 // If we don't use metadata search via the API, we add the field to the list, and 139 // also add a condition, using regular expressions 140 if !criteria.UseMetadataApiFilter { 141 metadataFields = append(metadataFields, k) 142 re, err := regexp.Compile(v.(string)) 143 if err != nil { 144 return nil, explanation, fmt.Errorf("error compiling regular expression '%s' : %s ", v, err) 145 } 146 conditions = append(conditions, conditionDef{"metadata", metadataRegexpCondition{k, re}}) 147 } 148 } 149 } else { 150 criteria.UseMetadataApiFilter = false 151 } 152 153 var itemResult Results 154 var err error 155 156 if criteria.UseMetadataApiFilter { 157 // This result will not include metadata fields. The query will use metadata parameters to restrict the search 158 itemResult, err = queryByMetadata(queryType, nil, params, metadataFilter, isSystem) 159 } else { 160 // This result includes metadata fields, if they exist. 161 itemResult, err = queryWithMetadataFields(queryType, nil, params, metadataFields, isSystem) 162 } 163 164 if err != nil { 165 return nil, explanation, fmt.Errorf("[SearchByFilter] error retrieving query item list: %s", err) 166 } 167 if dataInspectionRequested("QE1") { 168 util.Logger.Printf("[INSPECT-QE1-SearchByFilter] list of retrieved items %# v\n", pretty.Formatter(itemResult.Results)) 169 } 170 var itemList []QueryItem 171 172 // Converting the query result into a list of QueryItems 173 itemList, err = converter(queryType, itemResult) 174 if err != nil { 175 return nil, explanation, fmt.Errorf("[SearchByFilter] error converting QueryItem item list: %s", err) 176 } 177 if dataInspectionRequested("QE2") { 178 util.Logger.Printf("[INSPECT-QE2-SearchByFilter] list of converted items %# v\n", pretty.Formatter(itemList)) 179 } 180 181 // Process the list using the conditions gathered above 182 for _, item := range itemList { 183 numOfMatches := 0 184 185 for _, condition := range conditions { 186 187 if dataInspectionRequested("QE3") { 188 util.Logger.Printf("[INSPECT-QE3-SearchByFilter]\ncondition %# v\nitem %# v\n", pretty.Formatter(condition), pretty.Formatter(item)) 189 } 190 result, definition, err := conditionMatches(condition.conditionType, condition.stored, item) 191 if err != nil { 192 return nil, explanation, fmt.Errorf("[SearchByFilter] error applying condition %v: %s", condition, err) 193 } 194 195 // Saves matching information, which will be consolidated in the final explanation text 196 matches = append(matches, matchResult{ 197 Name: item.GetName(), 198 Type: condition.conditionType, 199 Definition: definition, 200 Result: result, 201 }) 202 if !result { 203 continue 204 } 205 206 numOfMatches++ 207 } 208 if numOfMatches == len(conditions) { 209 // All conditions were met 210 candidatesByConditions = append(candidatesByConditions, item) 211 } 212 } 213 214 // Consolidates the explanation with information about which conditions did actually match 215 matchesText := matchesToText(matches) 216 explanation += fmt.Sprintf("\n%s", matchesText) 217 util.Logger.Printf("[SearchByFilter] conditions matching\n%s", explanation) 218 219 // Once all the conditions have been evaluated, we check whether we got any items left. 220 // 221 // We consider an empty result to be a valid one: it's up to the caller to evaluate the result 222 // and eventually use the explanation to provide an error message 223 if len(candidatesByConditions) == 0 { 224 return nil, explanation, nil 225 } 226 227 // If we have only one item, there is no reason to search further for the newest or oldest item 228 if len(candidatesByConditions) == 1 { 229 return candidatesByConditions, explanation, nil 230 } 231 var emptyDatesFound []string 232 if searchLatest { 233 // By setting the latest date to the early possible date, we make sure that it will be swapped 234 // at the first comparison 235 var latestDate = "1970-01-01 00:00:00" 236 // item with the latest date among the candidates 237 var candidateByLatest QueryItem 238 for _, candidate := range candidatesByConditions { 239 itemDate := candidate.GetDate() 240 if itemDate == "" { 241 emptyDatesFound = append(emptyDatesFound, candidate.GetName()) 242 continue 243 } 244 util.Logger.Printf("[SearchByFilter] search latest: comparing %s to %s", latestDate, itemDate) 245 greater, err := compareDate(fmt.Sprintf("> %s", latestDate), itemDate) 246 if err != nil { 247 return nil, explanation, fmt.Errorf("[SearchByFilter] error comparing dates %s > %s : %s", 248 candidate.GetDate(), latestDate, err) 249 } 250 util.Logger.Printf("[SearchByFilter] result %v: ", greater) 251 if greater { 252 latestDate = candidate.GetDate() 253 candidateByLatest = candidate 254 } 255 } 256 if candidateByLatest != nil { 257 explanation += "\nlatest item found" 258 return []QueryItem{candidateByLatest}, explanation, nil 259 } else { 260 return nil, explanation, fmt.Errorf("search for newest item failed. Empty dates found for items %v", emptyDatesFound) 261 } 262 } 263 if searchEarliest { 264 // earliest date is set to a date in the future (10 years from now), so that any date found will be evaluated as 265 // earlier than this one 266 var earliestDate = time.Now().AddDate(10, 0, 0).String() 267 // item with the earliest date among the candidates 268 var candidateByEarliest QueryItem 269 for _, candidate := range candidatesByConditions { 270 itemDate := candidate.GetDate() 271 if itemDate == "" { 272 emptyDatesFound = append(emptyDatesFound, candidate.GetName()) 273 continue 274 } 275 util.Logger.Printf("[SearchByFilter] search earliest: comparing %s to %s", earliestDate, candidate.GetDate()) 276 greater, err := compareDate(fmt.Sprintf("< %s", earliestDate), candidate.GetDate()) 277 if err != nil { 278 return nil, explanation, fmt.Errorf("[SearchByFilter] error comparing dates %s > %s: %s", 279 candidate.GetDate(), earliestDate, err) 280 } 281 util.Logger.Printf("[SearchByFilter] result %v: ", greater) 282 if greater { 283 earliestDate = candidate.GetDate() 284 candidateByEarliest = candidate 285 } 286 } 287 if candidateByEarliest != nil { 288 explanation += "\nearliest item found" 289 return []QueryItem{candidateByEarliest}, explanation, nil 290 } else { 291 return nil, explanation, fmt.Errorf("search for oldest item failed. Empty dates found for items %v", emptyDatesFound) 292 } 293 } 294 return candidatesByConditions, explanation, nil 295 } 296 297 // conditionMatches performs the appropriate condition evaluation, 298 // depending on conditionType 299 func conditionMatches(conditionType string, stored, item interface{}) (bool, string, error) { 300 switch conditionType { 301 case types.FilterNameRegex: 302 return matchName(stored, item) 303 case types.FilterDate: 304 return matchDate(stored, item) 305 case types.FilterIp: 306 return matchIp(stored, item) 307 case types.FilterParent: 308 return matchParent(stored, item) 309 case types.FilterParentId: 310 return matchParentId(stored, item) 311 case "metadata": 312 return matchMetadata(stored, item) 313 } 314 return false, "", fmt.Errorf("unsupported condition type '%s'", conditionType) 315 } 316 317 // SearchByFilter is a generic filter that can operate on entities that implement the QueryItem interface 318 // It requires a queryType and a set of criteria. 319 // Returns a list of QueryItem interface elements, which can be cast back to the wanted real type 320 // Also returns a human readable text of the conditions being passed and how they matched the data found 321 // See "## Query engine" in CODING_GUIDELINES.md for more info 322 func (client *Client) SearchByFilter(queryType string, criteria *FilterDef) ([]QueryItem, string, error) { 323 return searchByFilter(client.queryByMetadataFilter, client.queryWithMetadataFields, resultToQueryItems, queryType, criteria) 324 } 325 326 // SearchByFilter runs the search for a specific catalog 327 // The 'parentField' argument defines which filter will be added, depending on the items we search for: 328 // - 'catalog' contains the catalog HREF or ID 329 // - 'catalogName' contains the catalog name 330 func (catalog *Catalog) SearchByFilter(queryType, parentField string, criteria *FilterDef) ([]QueryItem, string, error) { 331 var err error 332 switch parentField { 333 case "catalog": 334 err = criteria.AddFilter(types.FilterParentId, catalog.Catalog.ID) 335 case "catalogName": 336 err = criteria.AddFilter(types.FilterParent, catalog.Catalog.Name) 337 default: 338 return nil, "", fmt.Errorf("unrecognized filter field '%s'", parentField) 339 } 340 if err != nil { 341 return nil, "", fmt.Errorf("error setting parent filter for catalog %s with fieldName '%s'", catalog.Catalog.Name, parentField) 342 } 343 return catalog.client.SearchByFilter(queryType, criteria) 344 } 345 346 // SearchByFilter runs the search for a specific VDC 347 // The 'parentField' argument defines which filter will be added, depending on the items we search for: 348 // - 'vdc' contains the VDC HREF or ID 349 // - 'vdcName' contains the VDC name 350 func (vdc *Vdc) SearchByFilter(queryType, parentField string, criteria *FilterDef) ([]QueryItem, string, error) { 351 var err error 352 switch parentField { 353 case "vdc": 354 err = criteria.AddFilter(types.FilterParentId, vdc.Vdc.ID) 355 case "vdcName": 356 err = criteria.AddFilter(types.FilterParent, vdc.Vdc.Name) 357 default: 358 return nil, "", fmt.Errorf("unrecognized filter field '%s'", parentField) 359 } 360 if err != nil { 361 return nil, "", fmt.Errorf("error setting parent filter for VDC %s with fieldName '%s'", vdc.Vdc.Name, parentField) 362 } 363 return vdc.client.SearchByFilter(queryType, criteria) 364 } 365 366 // SearchByFilter runs the search for a specific Org 367 func (org *AdminOrg) SearchByFilter(queryType string, criteria *FilterDef) ([]QueryItem, string, error) { 368 err := criteria.AddFilter(types.FilterParent, org.AdminOrg.Name) 369 if err != nil { 370 return nil, "", fmt.Errorf("error setting parent filter for Org %s with fieldName 'orgName'", org.AdminOrg.Name) 371 } 372 return org.client.SearchByFilter(queryType, criteria) 373 } 374 375 // SearchByFilter runs the search for a specific Org 376 func (org *Org) SearchByFilter(queryType string, criteria *FilterDef) ([]QueryItem, string, error) { 377 err := criteria.AddFilter(types.FilterParent, org.Org.Name) 378 if err != nil { 379 return nil, "", fmt.Errorf("error setting parent filter for Org %s with fieldName 'orgName'", org.Org.Name) 380 } 381 return org.client.SearchByFilter(queryType, criteria) 382 } 383 384 // dataInspectionRequested checks if the given code was found in the inspection environment variable. 385 func dataInspectionRequested(code string) bool { 386 govcdInspect := os.Getenv("GOVCD_INSPECT") 387 return strings.Contains(govcdInspect, code) 388 }