autocompletequeryservice.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package asset
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "github.com/opencost/opencost/core/pkg/filter"
  8. "github.com/opencost/opencost/core/pkg/opencost"
  9. )
  10. const DefaultAutocompleteResultLimit = 100
  11. const MaxAutocompleteResultLimit = 1000
  12. type AssetAutocompleteRequest struct {
  13. TenantID string
  14. Search string
  15. Field string
  16. Limit int
  17. Window opencost.Window
  18. Filter filter.Filter
  19. }
  20. type AssetAutocompleteResponse struct {
  21. Data []string `json:"data"`
  22. }
  23. type AutocompleteQueryService interface {
  24. QueryAssetAutocomplete(AssetAutocompleteRequest, context.Context) (*AssetAutocompleteResponse, error)
  25. }
  26. func QueryAssetAutocompleteFromSet(assetSet *opencost.AssetSet, req AssetAutocompleteRequest) (*AssetAutocompleteResponse, error) {
  27. if req.TenantID == "" {
  28. return nil, fmt.Errorf("tenant ID is required")
  29. }
  30. field, err := validateAutocompleteField(req.Field)
  31. if err != nil {
  32. return nil, fmt.Errorf("invalid field: %w", err)
  33. }
  34. limit := req.Limit
  35. if limit <= 0 {
  36. limit = DefaultAutocompleteResultLimit
  37. }
  38. if limit > MaxAutocompleteResultLimit {
  39. return nil, fmt.Errorf("exceeded maxiumum autocomplete result limit of %d", MaxAutocompleteResultLimit)
  40. }
  41. var matcher opencost.AssetMatcher
  42. if req.Filter != nil {
  43. compiler := opencost.NewAssetMatchCompiler()
  44. matcher, err = compiler.Compile(req.Filter)
  45. if err != nil {
  46. return nil, fmt.Errorf("failed to compile filter: %w", err)
  47. }
  48. }
  49. search := strings.ToLower(req.Search)
  50. results := map[string]struct{}{}
  51. for _, a := range assetSet.Assets {
  52. if a == nil {
  53. continue
  54. }
  55. if matcher != nil && !matcher.Matches(a) {
  56. continue
  57. }
  58. values := assetAutocompleteValues(a, field)
  59. for _, value := range values {
  60. if value == "" {
  61. continue
  62. }
  63. if search != "" && !strings.Contains(strings.ToLower(value), search) {
  64. continue
  65. }
  66. results[value] = struct{}{}
  67. }
  68. }
  69. data := make([]string, 0, len(results))
  70. for value := range results {
  71. data = append(data, value)
  72. }
  73. sort.Strings(data)
  74. if len(data) > limit {
  75. data = data[:limit]
  76. }
  77. return &AssetAutocompleteResponse{Data: data}, nil
  78. }
  79. func validateAutocompleteField(field string) (string, error) {
  80. f := strings.ToLower(field)
  81. switch f {
  82. case "account", "cluster", "name", "provider", "providerid", "type", "category":
  83. return f, nil
  84. }
  85. if strings.HasPrefix(f, "label") {
  86. return f, nil
  87. }
  88. return "", fmt.Errorf("unrecognized field: %s", field)
  89. }
  90. func assetAutocompleteValues(asset opencost.Asset, field string) []string {
  91. props := asset.GetProperties()
  92. if props == nil {
  93. return nil
  94. }
  95. switch {
  96. case field == "account":
  97. return []string{props.Account}
  98. case field == "cluster":
  99. return []string{props.Cluster}
  100. case field == "name":
  101. return []string{props.Name}
  102. case field == "provider":
  103. return []string{props.Provider}
  104. case field == "providerid":
  105. return []string{props.ProviderID}
  106. case field == "type":
  107. return []string{asset.Type().String()}
  108. case field == "category":
  109. return []string{props.Category}
  110. case field == "label":
  111. keys := make([]string, 0, len(asset.GetLabels()))
  112. for key := range asset.GetLabels() {
  113. keys = append(keys, key)
  114. }
  115. return keys
  116. case strings.HasPrefix(field, "label:"):
  117. labelName := strings.TrimPrefix(field, "label:")
  118. if value, ok := asset.GetLabels()[labelName]; ok {
  119. return []string{value}
  120. }
  121. }
  122. return nil
  123. }