| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- package allocation
- import (
- "context"
- "fmt"
- "sort"
- "strings"
- "github.com/opencost/opencost/core/pkg/filter"
- "github.com/opencost/opencost/core/pkg/opencost"
- )
- const DefaultAutocompleteResultLimit = 100
- const MaxAutocompleteResultLimit = 1000
- type AllocationAutocompleteRequest struct {
- Search string
- Field string
- Limit int
- Window opencost.Window
- Filter filter.Filter
- LabelConfig *opencost.LabelConfig
- }
- type AllocationAutocompleteResponse struct {
- Data []string `json:"data"`
- }
- type AutocompleteQueryService interface {
- QueryAllocationAutocomplete(AllocationAutocompleteRequest, context.Context) (*AllocationAutocompleteResponse, error)
- }
- func QueryAllocationAutocompleteFromSetRange(asr *opencost.AllocationSetRange, req AllocationAutocompleteRequest) (*AllocationAutocompleteResponse, error) {
- field, err := validateAutocompleteField(req.Field)
- if err != nil {
- return nil, fmt.Errorf("invalid field: %w", err)
- }
- limit := req.Limit
- if limit <= 0 {
- limit = DefaultAutocompleteResultLimit
- }
- if limit > MaxAutocompleteResultLimit {
- return nil, fmt.Errorf("exceeded maxiumum autocomplete result limit of %d", MaxAutocompleteResultLimit)
- }
- var matcher opencost.AllocationMatcher
- if req.Filter != nil {
- compiler := opencost.NewAllocationMatchCompiler(req.LabelConfig)
- matcher, err = compiler.Compile(req.Filter)
- if err != nil {
- return nil, fmt.Errorf("failed to compile filter: %w", err)
- }
- }
- search := strings.ToLower(req.Search)
- results := map[string]struct{}{}
- for _, as := range asr.Slice() {
- for _, alloc := range as.Allocations {
- if alloc == nil || alloc.Properties == nil {
- continue
- }
- if matcher != nil && !matcher.Matches(alloc) {
- continue
- }
- values := allocationAutocompleteValues(alloc.Properties, field)
- for _, value := range values {
- if value == "" {
- continue
- }
- if search != "" && !strings.Contains(strings.ToLower(value), search) {
- continue
- }
- results[value] = struct{}{}
- }
- }
- }
- return &AllocationAutocompleteResponse{Data: uniqueSortedLimited(results, limit)}, nil
- }
- func validateAutocompleteField(field string) (string, error) {
- if field == "" {
- return "", fmt.Errorf("field is required")
- }
- f := strings.ToLower(field)
- switch f {
- case "cluster", "namespace", "node", "controllerkind", "controllername", "pod", "container", "account", "label", "namespacelabel":
- return f, nil
- }
- if strings.HasPrefix(f, "label:") {
- _, labelKey, _ := strings.Cut(field, ":")
- return "label:" + labelKey, nil
- }
- if strings.HasPrefix(f, "namespacelabel:") {
- _, labelKey, _ := strings.Cut(field, ":")
- return "namespacelabel:" + labelKey, nil
- }
- return "", fmt.Errorf("unrecognized field: %s", field)
- }
- func allocationAutocompleteValues(props *opencost.AllocationProperties, field string) []string {
- switch {
- case field == "cluster":
- return []string{props.Cluster}
- case field == "namespace":
- return []string{props.Namespace}
- case field == "node":
- return []string{props.Node}
- case field == "controllerkind":
- return []string{props.ControllerKind}
- case field == "controllername":
- return []string{props.Controller}
- case field == "pod":
- return []string{props.Pod}
- case field == "container":
- return []string{props.Container}
- case field == "account":
- return nil
- case field == "label":
- return mapKeys(props.Labels)
- case strings.HasPrefix(strings.ToLower(field), "label:"):
- label := strings.TrimPrefix(field, "label:")
- if v, ok := props.Labels[label]; ok {
- return []string{v}
- }
- case field == "namespacelabel":
- return mapKeys(props.NamespaceLabels)
- case strings.HasPrefix(strings.ToLower(field), "namespacelabel:"):
- label := strings.TrimPrefix(field, "namespacelabel:")
- if v, ok := props.NamespaceLabels[label]; ok {
- return []string{v}
- }
- }
- return nil
- }
- func mapKeys(values map[string]string) []string {
- result := make([]string, 0, len(values))
- for k := range values {
- result = append(result, k)
- }
- return result
- }
- func uniqueSortedLimited(values map[string]struct{}, limit int) []string {
- out := make([]string, 0, len(values))
- for v := range values {
- out = append(out, v)
- }
- sort.Strings(out)
- if len(out) > limit {
- return out[:limit]
- }
- return out
- }
|