filterutil.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. package filterutil
  2. import (
  3. "strings"
  4. "github.com/opencost/opencost/pkg/costmodel/clusters"
  5. "github.com/opencost/opencost/pkg/kubecost"
  6. "github.com/opencost/opencost/pkg/log"
  7. "github.com/opencost/opencost/pkg/prom"
  8. "github.com/opencost/opencost/pkg/util/mapper"
  9. "github.com/opencost/opencost/pkg/util/typeutil"
  10. filter "github.com/opencost/opencost/pkg/filter21"
  11. afilter "github.com/opencost/opencost/pkg/filter21/allocation"
  12. assetfilter "github.com/opencost/opencost/pkg/filter21/asset"
  13. "github.com/opencost/opencost/pkg/filter21/ast"
  14. // cloudfilter "github.com/opencost/opencost/pkg/filter/cloud"
  15. )
  16. // ============================================================================
  17. // This file contains:
  18. // Parsing (HTTP query params -> v2.1 filter) for V1 of query param filters
  19. //
  20. // e.g. "filterNamespaces=ku&filterControllers=deployment:kc"
  21. // ============================================================================
  22. // This is somewhat of a fancy solution, but allows us to "register" DefaultFieldByName funcs
  23. // funcs by Field type.
  24. var defaultFieldByType = map[string]any{
  25. // typeutil.TypeOf[cloudfilter.CloudAggregationField](): cloudfilter.DefaultFieldByName,
  26. typeutil.TypeOf[afilter.AllocationField](): afilter.DefaultFieldByName,
  27. typeutil.TypeOf[assetfilter.AssetField](): assetfilter.DefaultFieldByName,
  28. }
  29. // DefaultFieldByName looks up a specific T field instance by name and returns the default
  30. // ast.Field value for that type.
  31. func DefaultFieldByName[T ~string](field T) *ast.Field {
  32. lookup, ok := defaultFieldByType[typeutil.TypeOf[T]()]
  33. if !ok {
  34. log.Errorf("Failed to get default field lookup for: %s", typeutil.TypeOf[T]())
  35. return nil
  36. }
  37. defaultLookup, ok := lookup.(func(T) *ast.Field)
  38. if !ok {
  39. log.Errorf("Failed to cast default field lookup for: %s", typeutil.TypeOf[T]())
  40. return nil
  41. }
  42. return defaultLookup(field)
  43. }
  44. const (
  45. ParamFilterClusters = "filterClusters"
  46. ParamFilterNodes = "filterNodes"
  47. ParamFilterNamespaces = "filterNamespaces"
  48. ParamFilterControllerKinds = "filterControllerKinds"
  49. ParamFilterControllers = "filterControllers"
  50. ParamFilterPods = "filterPods"
  51. ParamFilterContainers = "filterContainers"
  52. ParamFilterDepartments = "filterDepartments"
  53. ParamFilterEnvironments = "filterEnvironments"
  54. ParamFilterOwners = "filterOwners"
  55. ParamFilterProducts = "filterProducts"
  56. ParamFilterTeams = "filterTeams"
  57. ParamFilterAnnotations = "filterAnnotations"
  58. ParamFilterLabels = "filterLabels"
  59. ParamFilterServices = "filterServices"
  60. ParamFilterAccounts = "filterAccounts"
  61. ParamFilterCategories = "filterCategories"
  62. ParamFilterNames = "filterNames"
  63. ParamFilterProjects = "filterProjects"
  64. ParamFilterProviders = "filterProviders"
  65. ParamFilterProviderIDs = "filterProviderIDs"
  66. ParamFilterProviderIDsV2 = "filterProviderIds"
  67. ParamFilterRegions = "filterRegions"
  68. ParamFilterTypes = "filterTypes"
  69. )
  70. // ValidAssetFilterParams returns a list of all possible filter parameters
  71. func ValidAssetFilterParams() []string {
  72. return []string{
  73. ParamFilterAccounts,
  74. ParamFilterCategories,
  75. ParamFilterClusters,
  76. ParamFilterLabels,
  77. ParamFilterNames,
  78. ParamFilterProjects,
  79. ParamFilterProviders,
  80. ParamFilterProviderIDs,
  81. ParamFilterProviderIDsV2,
  82. ParamFilterRegions,
  83. ParamFilterServices,
  84. ParamFilterTypes,
  85. }
  86. }
  87. // AllocationPropToV1FilterParamKey maps allocation string property
  88. // representations to v1 filter param keys for legacy filter config support
  89. // (e.g. reports). Example mapping: "cluster" -> "filterClusters"
  90. var AllocationPropToV1FilterParamKey = map[string]string{
  91. kubecost.AllocationClusterProp: ParamFilterClusters,
  92. kubecost.AllocationNodeProp: ParamFilterNodes,
  93. kubecost.AllocationNamespaceProp: ParamFilterNamespaces,
  94. kubecost.AllocationControllerProp: ParamFilterControllers,
  95. kubecost.AllocationControllerKindProp: ParamFilterControllerKinds,
  96. kubecost.AllocationPodProp: ParamFilterPods,
  97. kubecost.AllocationLabelProp: ParamFilterLabels,
  98. kubecost.AllocationServiceProp: ParamFilterServices,
  99. kubecost.AllocationDepartmentProp: ParamFilterDepartments,
  100. kubecost.AllocationEnvironmentProp: ParamFilterEnvironments,
  101. kubecost.AllocationOwnerProp: ParamFilterOwners,
  102. kubecost.AllocationProductProp: ParamFilterProducts,
  103. kubecost.AllocationTeamProp: ParamFilterTeams,
  104. }
  105. // Map to store Kubecost Asset property to Asset Filter types.
  106. // AssetPropToV1FilterParamKey maps asset string property representations to v1
  107. // filter param keys for legacy filter config support (e.g. reports). Example
  108. // mapping: "category" -> "filterCategories"
  109. var AssetPropToV1FilterParamKey = map[kubecost.AssetProperty]string{
  110. kubecost.AssetNameProp: ParamFilterNames,
  111. kubecost.AssetTypeProp: ParamFilterTypes,
  112. kubecost.AssetAccountProp: ParamFilterAccounts,
  113. kubecost.AssetCategoryProp: ParamFilterCategories,
  114. kubecost.AssetClusterProp: ParamFilterClusters,
  115. kubecost.AssetProjectProp: ParamFilterProjects,
  116. kubecost.AssetProviderProp: ParamFilterProviders,
  117. kubecost.AssetProviderIDProp: ParamFilterProviderIDs,
  118. kubecost.AssetServiceProp: ParamFilterServices,
  119. }
  120. // AllHTTPParamKeys returns all HTTP GET parameters used for v1 filters. It is
  121. // intended to help validate HTTP queries in handlers to help avoid e.g.
  122. // spelling errors.
  123. func AllHTTPParamKeys() []string {
  124. return []string{
  125. ParamFilterClusters,
  126. ParamFilterNodes,
  127. ParamFilterNamespaces,
  128. ParamFilterControllerKinds,
  129. ParamFilterControllers,
  130. ParamFilterPods,
  131. ParamFilterContainers,
  132. ParamFilterDepartments,
  133. ParamFilterEnvironments,
  134. ParamFilterOwners,
  135. ParamFilterProducts,
  136. ParamFilterTeams,
  137. ParamFilterAnnotations,
  138. ParamFilterLabels,
  139. ParamFilterServices,
  140. }
  141. }
  142. // AllocationFilterFromParamsV1 takes a set of HTTP query parameters and
  143. // converts them to an AllocationFilter, which is a structured in-Go
  144. // representation of a set of filters.
  145. //
  146. // The HTTP query parameters are the "v1" filters attached to the Allocation
  147. // API: "filterNamespaces=", "filterNodes=", etc.
  148. //
  149. // It takes an optional LabelConfig, which if provided enables "label-mapped"
  150. // filters like "filterDepartments".
  151. //
  152. // It takes an optional ClusterMap, which if provided enables cluster name
  153. // filtering. This turns all `filterClusters=foo` arguments into the equivalent
  154. // of `clusterID = "foo" OR clusterName = "foo"`.
  155. func AllocationFilterFromParamsV1(
  156. params AllocationFilterV1,
  157. labelConfig *kubecost.LabelConfig,
  158. clusterMap clusters.ClusterMap,
  159. ) filter.Filter {
  160. var filterOps []ast.FilterNode
  161. // ClusterMap does not provide a cluster name -> cluster ID mapping in the
  162. // interface, probably because there could be multiple IDs with the same
  163. // name. However, V1 filter logic demands that the parameters to
  164. // filterClusters= be checked against both cluster ID AND cluster name.
  165. //
  166. // To support expected filterClusters= behavior, we construct a mapping
  167. // of cluster name -> cluster IDs (could be multiple IDs for the same name)
  168. // so that we can create AllocationFilters that use only ClusterIDEquals.
  169. //
  170. //
  171. // AllocationFilter intentionally does not support cluster name filters
  172. // because those should be considered presentation-layer only.
  173. clusterNameToIDs := map[string][]string{}
  174. if clusterMap != nil {
  175. cMap := clusterMap.AsMap()
  176. for _, info := range cMap {
  177. if info == nil {
  178. continue
  179. }
  180. if _, ok := clusterNameToIDs[info.Name]; ok {
  181. clusterNameToIDs[info.Name] = append(clusterNameToIDs[info.Name], info.ID)
  182. } else {
  183. clusterNameToIDs[info.Name] = []string{info.ID}
  184. }
  185. }
  186. }
  187. // The proliferation of > 0 guards in the function is to avoid constructing
  188. // empty filter structs. While it is functionally equivalent to add empty
  189. // filter structs (they evaluate to true always) there could be overhead
  190. // when calling Matches() repeatedly for no purpose.
  191. if len(params.Clusters) > 0 {
  192. var ops []ast.FilterNode
  193. // filter my cluster identifier
  194. ops = push(ops, filterV1SingleValueFromList(params.Clusters, afilter.FieldClusterID))
  195. for _, rawFilterValue := range params.Clusters {
  196. clusterNameFilter, wildcard := parseWildcardEnd(rawFilterValue)
  197. clusterIDsToFilter := []string{}
  198. for clusterName := range clusterNameToIDs {
  199. if wildcard && strings.HasPrefix(clusterName, clusterNameFilter) {
  200. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  201. } else if !wildcard && clusterName == clusterNameFilter {
  202. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  203. }
  204. }
  205. for _, clusterID := range clusterIDsToFilter {
  206. ops = append(ops, &ast.EqualOp{
  207. Left: ast.Identifier{
  208. Field: afilter.DefaultFieldByName(afilter.FieldClusterID),
  209. Key: "",
  210. },
  211. Right: clusterID,
  212. })
  213. }
  214. }
  215. //
  216. clustersOp := opsToOr(ops)
  217. filterOps = push(filterOps, clustersOp)
  218. }
  219. if len(params.Nodes) > 0 {
  220. filterOps = push(filterOps, filterV1SingleValueFromList(params.Nodes, afilter.FieldNode))
  221. }
  222. if len(params.Namespaces) > 0 {
  223. filterOps = push(filterOps, filterV1SingleValueFromList(params.Namespaces, afilter.FieldNamespace))
  224. }
  225. if len(params.ControllerKinds) > 0 {
  226. filterOps = push(filterOps, filterV1SingleValueFromList(params.ControllerKinds, afilter.FieldControllerKind))
  227. }
  228. // filterControllers= accepts controllerkind:controllername filters, e.g.
  229. // "deployment:kubecost-cost-analyzer"
  230. //
  231. // Thus, we have to make a custom OR filter for this condition.
  232. if len(params.Controllers) > 0 {
  233. var ops []ast.FilterNode
  234. for _, rawFilterValue := range params.Controllers {
  235. split := strings.Split(rawFilterValue, ":")
  236. if len(split) == 1 {
  237. filterValue, wildcard := parseWildcardEnd(split[0])
  238. subFilter := toEqualOp(afilter.FieldControllerName, "", filterValue, wildcard)
  239. ops = append(ops, subFilter)
  240. } else if len(split) == 2 {
  241. kindFilterVal := split[0]
  242. nameFilterVal, wildcard := parseWildcardEnd(split[1])
  243. kindFilter := toEqualOp(afilter.FieldControllerKind, "", kindFilterVal, false)
  244. nameFilter := toEqualOp(afilter.FieldControllerName, "", nameFilterVal, wildcard)
  245. // The controller name AND the controller kind must match
  246. ops = append(ops, &ast.AndOp{
  247. Operands: []ast.FilterNode{
  248. kindFilter,
  249. nameFilter,
  250. },
  251. })
  252. } else {
  253. log.Warnf("illegal filter for controller: %s", rawFilterValue)
  254. }
  255. }
  256. controllersOp := opsToOr(ops)
  257. filterOps = push(filterOps, controllersOp)
  258. }
  259. if len(params.Pods) > 0 {
  260. filterOps = push(filterOps, filterV1SingleValueFromList(params.Pods, afilter.FieldPod))
  261. }
  262. if len(params.Containers) > 0 {
  263. filterOps = push(filterOps, filterV1SingleValueFromList(params.Containers, afilter.FieldContainer))
  264. }
  265. // Label-mapped queries require a label config to be present.
  266. if labelConfig != nil {
  267. if len(params.Departments) > 0 {
  268. filterOps = push(filterOps, filterV1LabelAliasMappedFromList(params.Departments, labelConfig.DepartmentLabel))
  269. }
  270. if len(params.Environments) > 0 {
  271. filterOps = push(filterOps, filterV1LabelAliasMappedFromList(params.Environments, labelConfig.EnvironmentLabel))
  272. }
  273. if len(params.Owners) > 0 {
  274. filterOps = push(filterOps, filterV1LabelAliasMappedFromList(params.Owners, labelConfig.OwnerLabel))
  275. }
  276. if len(params.Products) > 0 {
  277. filterOps = push(filterOps, filterV1LabelAliasMappedFromList(params.Products, labelConfig.ProductLabel))
  278. }
  279. if len(params.Teams) > 0 {
  280. filterOps = push(filterOps, filterV1LabelAliasMappedFromList(params.Teams, labelConfig.TeamLabel))
  281. }
  282. } else {
  283. log.Debugf("No label config is available. Not creating filters for label-mapped 'fields'.")
  284. }
  285. if len(params.Annotations) > 0 {
  286. filterOps = push(filterOps, filterV1DoubleValueFromList(params.Annotations, afilter.FieldAnnotation))
  287. }
  288. if len(params.Labels) > 0 {
  289. filterOps = push(filterOps, filterV1DoubleValueFromList(params.Labels, afilter.FieldLabel))
  290. }
  291. if len(params.Services) > 0 {
  292. var ops []ast.FilterNode
  293. // filterServices= is the only filter that uses the "contains" operator.
  294. for _, filterValue := range params.Services {
  295. // TODO: wildcard support
  296. filterValue, wildcard := parseWildcardEnd(filterValue)
  297. subFilter := toContainsOp(afilter.FieldServices, "", filterValue, wildcard)
  298. ops = append(ops, subFilter)
  299. }
  300. serviceOps := opsToOr(ops)
  301. filterOps = push(filterOps, serviceOps)
  302. }
  303. andFilter := opsToAnd(filterOps)
  304. if andFilter == nil {
  305. return &ast.VoidOp{} // no filter
  306. }
  307. return andFilter
  308. }
  309. func AssetFilterFromParamsV1(
  310. qp mapper.PrimitiveMapReader,
  311. clusterMap clusters.ClusterMap,
  312. ) filter.Filter {
  313. var filterOps []ast.FilterNode
  314. // ClusterMap does not provide a cluster name -> cluster ID mapping in the
  315. // interface, probably because there could be multiple IDs with the same
  316. // name. However, V1 filter logic demands that the parameters to
  317. // filterClusters= be checked against both cluster ID AND cluster name.
  318. //
  319. // To support expected filterClusters= behavior, we construct a mapping
  320. // of cluster name -> cluster IDs (could be multiple IDs for the same name)
  321. // so that we can create AllocationFilters that use only ClusterIDEquals.
  322. //
  323. //
  324. // AllocationFilter intentionally does not support cluster name filters
  325. // because those should be considered presentation-layer only.
  326. clusterNameToIDs := map[string][]string{}
  327. if clusterMap != nil {
  328. cMap := clusterMap.AsMap()
  329. for _, info := range cMap {
  330. if info == nil {
  331. continue
  332. }
  333. if _, ok := clusterNameToIDs[info.Name]; ok {
  334. clusterNameToIDs[info.Name] = append(clusterNameToIDs[info.Name], info.ID)
  335. } else {
  336. clusterNameToIDs[info.Name] = []string{info.ID}
  337. }
  338. }
  339. }
  340. // The proliferation of > 0 guards in the function is to avoid constructing
  341. // empty filter structs. While it is functionally equivalent to add empty
  342. // filter structs (they evaluate to true always) there could be overhead
  343. // when calling Matches() repeatedly for no purpose.
  344. if filterClusters := qp.GetList(ParamFilterClusters, ","); len(filterClusters) > 0 {
  345. var ops []ast.FilterNode
  346. // filter my cluster identifier
  347. ops = push(ops, filterV1SingleValueFromList(filterClusters, assetfilter.FieldClusterID))
  348. for _, rawFilterValue := range filterClusters {
  349. clusterNameFilter, wildcard := parseWildcardEnd(rawFilterValue)
  350. clusterIDsToFilter := []string{}
  351. for clusterName := range clusterNameToIDs {
  352. if wildcard && strings.HasPrefix(clusterName, clusterNameFilter) {
  353. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  354. } else if !wildcard && clusterName == clusterNameFilter {
  355. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  356. }
  357. }
  358. for _, clusterID := range clusterIDsToFilter {
  359. ops = append(ops, &ast.EqualOp{
  360. Left: ast.Identifier{
  361. Field: assetfilter.DefaultFieldByName(assetfilter.FieldClusterID),
  362. Key: "",
  363. },
  364. Right: clusterID,
  365. })
  366. }
  367. }
  368. clustersOp := opsToOr(ops)
  369. filterOps = push(filterOps, clustersOp)
  370. }
  371. if raw := qp.GetList(ParamFilterAccounts, ","); len(raw) > 0 {
  372. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldAccount))
  373. }
  374. if raw := qp.GetList(ParamFilterCategories, ","); len(raw) > 0 {
  375. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldCategory))
  376. }
  377. if raw := qp.GetList(ParamFilterNames, ","); len(raw) > 0 {
  378. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldName))
  379. }
  380. if raw := qp.GetList(ParamFilterProjects, ","); len(raw) > 0 {
  381. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldProject))
  382. }
  383. if raw := qp.GetList(ParamFilterProviders, ","); len(raw) > 0 {
  384. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldProvider))
  385. }
  386. if raw := GetList(ParamFilterProviderIDs, ParamFilterProviderIDsV2, qp); len(raw) > 0 {
  387. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldProviderID))
  388. }
  389. if raw := qp.GetList(ParamFilterServices, ","); len(raw) > 0 {
  390. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldService))
  391. }
  392. if raw := qp.GetList(ParamFilterTypes, ","); len(raw) > 0 {
  393. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldType))
  394. }
  395. if raw := qp.GetList(ParamFilterLabels, ","); len(raw) > 0 {
  396. filterOps = push(filterOps, filterV1DoubleValueFromList(raw, assetfilter.FieldLabel))
  397. }
  398. if raw := qp.GetList(ParamFilterRegions, ","); len(raw) > 0 {
  399. filterOps = push(filterOps, filterV1SingleLabelKeyFromList(raw, "label_topology_kubernetes_io_region", assetfilter.FieldLabel))
  400. }
  401. andFilter := opsToAnd(filterOps)
  402. if andFilter == nil {
  403. return &ast.VoidOp{} // no filter
  404. }
  405. return andFilter
  406. }
  407. // filterV1SingleValueFromList creates an OR of equality filters for a given
  408. // filter field.
  409. //
  410. // The v1 query language (e.g. "filterNamespaces=XYZ,ABC") uses OR within
  411. // a field (e.g. namespace = XYZ OR namespace = ABC)
  412. func filterV1SingleValueFromList[T ~string](rawFilterValues []string, filterField T) ast.FilterNode {
  413. var ops []ast.FilterNode
  414. for _, filterValue := range rawFilterValues {
  415. filterValue = strings.TrimSpace(filterValue)
  416. filterValue, wildcard := parseWildcardEnd(filterValue)
  417. subFilter := toEqualOp(filterField, "", filterValue, wildcard)
  418. ops = append(ops, subFilter)
  419. }
  420. return opsToOr(ops)
  421. }
  422. func filterV1SingleLabelKeyFromList[T ~string](rawFilterValues []string, labelName string, labelField T) ast.FilterNode {
  423. var ops []ast.FilterNode
  424. labelName = prom.SanitizeLabelName(labelName)
  425. for _, filterValue := range rawFilterValues {
  426. filterValue = strings.TrimSpace(filterValue)
  427. filterValue, wildcard := parseWildcardEnd(filterValue)
  428. subFilter := toEqualOp(labelField, labelName, filterValue, wildcard)
  429. ops = append(ops, subFilter)
  430. }
  431. return opsToOr(ops)
  432. }
  433. // filterV1LabelAliasMappedFromList is like filterV1SingleValueFromList but is
  434. // explicitly for labels and annotations because "label-mapped" filters (like filterTeams=)
  435. // are actually label filters with a fixed label key.
  436. func filterV1LabelAliasMappedFromList(rawFilterValues []string, labelName string) ast.FilterNode {
  437. var ops []ast.FilterNode
  438. labelName = prom.SanitizeLabelName(labelName)
  439. for _, filterValue := range rawFilterValues {
  440. filterValue = strings.TrimSpace(filterValue)
  441. filterValue, wildcard := parseWildcardEnd(filterValue)
  442. subFilter := toAllocationAliasOp(labelName, filterValue, wildcard)
  443. ops = append(ops, subFilter)
  444. }
  445. return opsToOr(ops)
  446. }
  447. // filterV1DoubleValueFromList creates an OR of key:value equality filters for
  448. // colon-split filter values.
  449. //
  450. // The v1 query language (e.g. "filterLabels=app:foo,l2:bar") uses OR within
  451. // a field (e.g. label[app] = foo OR label[l2] = bar)
  452. func filterV1DoubleValueFromList[T ~string](rawFilterValuesUnsplit []string, filterField T) ast.FilterNode {
  453. var ops []ast.FilterNode
  454. for _, unsplit := range rawFilterValuesUnsplit {
  455. if unsplit != "" {
  456. split := strings.Split(unsplit, ":")
  457. if len(split) != 2 {
  458. log.Warnf("illegal key/value filter (ignoring): %s", unsplit)
  459. continue
  460. }
  461. labelName := prom.SanitizeLabelName(strings.TrimSpace(split[0]))
  462. val := strings.TrimSpace(split[1])
  463. val, wildcard := parseWildcardEnd(val)
  464. subFilter := toEqualOp(filterField, labelName, val, wildcard)
  465. ops = append(ops, subFilter)
  466. }
  467. }
  468. return opsToOr(ops)
  469. }
  470. // parseWildcardEnd checks if the given filter value is wildcarded, meaning
  471. // it ends in "*". If it does, it removes the suffix and returns the cleaned
  472. // string and true. Otherwise, it returns the same filter and false.
  473. //
  474. // parseWildcardEnd("kube*") = "kube", true
  475. // parseWildcardEnd("kube") = "kube", false
  476. func parseWildcardEnd(rawFilterValue string) (string, bool) {
  477. return strings.TrimSuffix(rawFilterValue, "*"), strings.HasSuffix(rawFilterValue, "*")
  478. }
  479. func push(a []ast.FilterNode, item ast.FilterNode) []ast.FilterNode {
  480. if item == nil {
  481. return a
  482. }
  483. return append(a, item)
  484. }
  485. func opsToOr(ops []ast.FilterNode) ast.FilterNode {
  486. if len(ops) == 0 {
  487. return nil
  488. }
  489. if len(ops) == 1 {
  490. return ops[0]
  491. }
  492. return &ast.OrOp{
  493. Operands: ops,
  494. }
  495. }
  496. func opsToAnd(ops []ast.FilterNode) ast.FilterNode {
  497. if len(ops) == 0 {
  498. return nil
  499. }
  500. if len(ops) == 1 {
  501. return ops[0]
  502. }
  503. return &ast.AndOp{
  504. Operands: ops,
  505. }
  506. }
  507. func toEqualOp[T ~string](field T, key string, value string, wildcard bool) ast.FilterNode {
  508. left := ast.Identifier{
  509. Field: DefaultFieldByName(field),
  510. Key: key,
  511. }
  512. right := value
  513. if wildcard {
  514. return &ast.ContainsPrefixOp{
  515. Left: left,
  516. Right: right,
  517. }
  518. }
  519. return &ast.EqualOp{
  520. Left: left,
  521. Right: right,
  522. }
  523. }
  524. func toContainsOp[T ~string](field T, key string, value string, wildcard bool) ast.FilterNode {
  525. left := ast.Identifier{
  526. Field: DefaultFieldByName(field),
  527. Key: key,
  528. }
  529. right := value
  530. if wildcard {
  531. return &ast.ContainsPrefixOp{
  532. Left: left,
  533. Right: right,
  534. }
  535. }
  536. return &ast.ContainsOp{
  537. Left: left,
  538. Right: right,
  539. }
  540. }
  541. func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *ast.OrOp {
  542. // labels.Contains(labelName)
  543. labelContainsKey := &ast.ContainsOp{
  544. Left: ast.Identifier{
  545. Field: afilter.DefaultFieldByName(afilter.FieldLabel),
  546. Key: "",
  547. },
  548. Right: labelName,
  549. }
  550. // annotations.Contains(labelName)
  551. annotationContainsKey := &ast.ContainsOp{
  552. Left: ast.Identifier{
  553. Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
  554. Key: "",
  555. },
  556. Right: labelName,
  557. }
  558. // labels[labelName] equals/startswith filterValue
  559. var labelSubFilter ast.FilterNode
  560. if wildcard {
  561. labelSubFilter = &ast.ContainsPrefixOp{
  562. Left: ast.Identifier{
  563. Field: afilter.DefaultFieldByName(afilter.FieldLabel),
  564. Key: labelName,
  565. },
  566. Right: filterValue,
  567. }
  568. } else {
  569. labelSubFilter = &ast.EqualOp{
  570. Left: ast.Identifier{
  571. Field: afilter.DefaultFieldByName(afilter.FieldLabel),
  572. Key: labelName,
  573. },
  574. Right: filterValue,
  575. }
  576. }
  577. // annotations[labelName] equals/startswith filterValue
  578. var annotationSubFilter ast.FilterNode
  579. if wildcard {
  580. annotationSubFilter = &ast.ContainsPrefixOp{
  581. Left: ast.Identifier{
  582. Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
  583. Key: labelName,
  584. },
  585. Right: filterValue,
  586. }
  587. } else {
  588. annotationSubFilter = &ast.EqualOp{
  589. Left: ast.Identifier{
  590. Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
  591. Key: labelName,
  592. },
  593. Right: filterValue,
  594. }
  595. }
  596. // Logically, this is equivalent to:
  597. // (labels.Contains(labelName) && labels[labelName] = filterValue) ||
  598. // (!labels.Contains(labelName) && annotations.Contains(labelName) && annotations[labelName] = filterValue)
  599. return &ast.OrOp{
  600. Operands: []ast.FilterNode{
  601. &ast.AndOp{
  602. Operands: []ast.FilterNode{
  603. labelContainsKey,
  604. labelSubFilter,
  605. },
  606. },
  607. &ast.AndOp{
  608. Operands: []ast.FilterNode{
  609. &ast.NotOp{
  610. Operand: ast.Clone(labelContainsKey),
  611. },
  612. annotationContainsKey,
  613. annotationSubFilter,
  614. },
  615. },
  616. },
  617. }
  618. }
  619. // GetList provides a list of values from the first key if they exist, otherwise, it returns
  620. // the values from the second key.
  621. func GetList(primaryKey, secondaryKey string, qp mapper.PrimitiveMapReader) []string {
  622. if raw := qp.GetList(primaryKey, ","); len(raw) > 0 {
  623. return raw
  624. }
  625. return qp.GetList(secondaryKey, ",")
  626. }