autocompletequeryservice.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package allocation
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "sort"
  7. "strings"
  8. "github.com/opencost/opencost/core/pkg/filter"
  9. "github.com/opencost/opencost/core/pkg/opencost"
  10. )
  11. // ErrAutocompleteBadRequest indicates a client error in an autocomplete request.
  12. var ErrAutocompleteBadRequest = errors.New("autocomplete bad request")
  13. // IsAutocompleteBadRequest reports whether err is a client validation error.
  14. func IsAutocompleteBadRequest(err error) bool {
  15. return errors.Is(err, ErrAutocompleteBadRequest)
  16. }
  17. const DefaultAutocompleteResultLimit = 100
  18. const MaxAutocompleteResultLimit = 1000
  19. type AllocationAutocompleteRequest struct {
  20. Search string
  21. Field string
  22. Limit int
  23. Window opencost.Window
  24. Filter filter.Filter
  25. LabelConfig *opencost.LabelConfig
  26. }
  27. type AllocationAutocompleteResponse struct {
  28. Data []string `json:"data"`
  29. }
  30. type AutocompleteQueryService interface {
  31. QueryAllocationAutocomplete(AllocationAutocompleteRequest, context.Context) (*AllocationAutocompleteResponse, error)
  32. }
  33. func QueryAllocationAutocompleteFromSetRange(asr *opencost.AllocationSetRange, req AllocationAutocompleteRequest) (*AllocationAutocompleteResponse, error) {
  34. field, err := validateAutocompleteField(req.Field)
  35. if err != nil {
  36. return nil, fmt.Errorf("%w: invalid field: %w", ErrAutocompleteBadRequest, err)
  37. }
  38. limit := req.Limit
  39. if limit <= 0 {
  40. limit = DefaultAutocompleteResultLimit
  41. }
  42. if limit > MaxAutocompleteResultLimit {
  43. return nil, fmt.Errorf("%w: exceeded maximum autocomplete result limit of %d", ErrAutocompleteBadRequest, MaxAutocompleteResultLimit)
  44. }
  45. var matcher opencost.AllocationMatcher
  46. if req.Filter != nil {
  47. compiler := opencost.NewAllocationMatchCompiler(req.LabelConfig)
  48. matcher, err = compiler.Compile(req.Filter)
  49. if err != nil {
  50. return nil, fmt.Errorf("%w: failed to compile filter: %w", ErrAutocompleteBadRequest, err)
  51. }
  52. }
  53. search := strings.ToLower(req.Search)
  54. results := map[string]struct{}{}
  55. for _, as := range asr.Allocations {
  56. if as == nil {
  57. continue
  58. }
  59. for _, alloc := range as.Allocations {
  60. if alloc == nil || alloc.Properties == nil {
  61. continue
  62. }
  63. if matcher != nil && !matcher.Matches(alloc) {
  64. continue
  65. }
  66. values := allocationAutocompleteValues(alloc.Properties, field)
  67. for _, value := range values {
  68. if value == "" {
  69. continue
  70. }
  71. if search != "" && !strings.Contains(strings.ToLower(value), search) {
  72. continue
  73. }
  74. results[value] = struct{}{}
  75. }
  76. }
  77. }
  78. return &AllocationAutocompleteResponse{Data: uniqueSortedLimited(results, limit)}, nil
  79. }
  80. func validateAutocompleteField(field string) (string, error) {
  81. if field == "" {
  82. return "", fmt.Errorf("field is required")
  83. }
  84. f := strings.ToLower(field)
  85. switch f {
  86. case "cluster", "namespace", "node", "controllerkind", "controllername", "pod", "container", "label", "namespacelabel":
  87. return f, nil
  88. }
  89. if strings.HasPrefix(f, "label:") {
  90. _, labelKey, _ := strings.Cut(f, ":")
  91. return "label:" + labelKey, nil
  92. }
  93. if strings.HasPrefix(f, "namespacelabel:") {
  94. _, labelKey, _ := strings.Cut(f, ":")
  95. return "namespacelabel:" + labelKey, nil
  96. }
  97. return "", fmt.Errorf("unrecognized field: %s", field)
  98. }
  99. func allocationAutocompleteValues(props *opencost.AllocationProperties, field string) []string {
  100. switch {
  101. case field == "cluster":
  102. return []string{props.Cluster}
  103. case field == "namespace":
  104. return []string{props.Namespace}
  105. case field == "node":
  106. return []string{props.Node}
  107. case field == "controllerkind":
  108. return []string{props.ControllerKind}
  109. case field == "controllername":
  110. return []string{props.Controller}
  111. case field == "pod":
  112. return []string{props.Pod}
  113. case field == "container":
  114. return []string{props.Container}
  115. case field == "label":
  116. return mapKeys(props.Labels)
  117. case strings.HasPrefix(field, "label:"):
  118. label := strings.TrimPrefix(field, "label:")
  119. if v, ok := mapValueFold(props.Labels, label); ok {
  120. return []string{v}
  121. }
  122. case field == "namespacelabel":
  123. return mapKeys(props.NamespaceLabels)
  124. case strings.HasPrefix(field, "namespacelabel:"):
  125. label := strings.TrimPrefix(field, "namespacelabel:")
  126. if v, ok := mapValueFold(props.NamespaceLabels, label); ok {
  127. return []string{v}
  128. }
  129. }
  130. return nil
  131. }
  132. func mapKeys(values map[string]string) []string {
  133. result := make([]string, 0, len(values))
  134. for k := range values {
  135. result = append(result, k)
  136. }
  137. return result
  138. }
  139. func mapValueFold(values map[string]string, key string) (string, bool) {
  140. if v, ok := values[key]; ok {
  141. return v, true
  142. }
  143. for k, v := range values {
  144. if strings.EqualFold(k, key) {
  145. return v, true
  146. }
  147. }
  148. return "", false
  149. }
  150. func uniqueSortedLimited(values map[string]struct{}, limit int) []string {
  151. out := make([]string, 0, len(values))
  152. for v := range values {
  153. out = append(out, v)
  154. }
  155. sort.Strings(out)
  156. if len(out) > limit {
  157. return out[:limit]
  158. }
  159. return out
  160. }