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  }