filterutil.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  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. // Types have a special situation where we allow users to enter them
  394. // capitalized or uncapitalized
  395. for i := range raw {
  396. raw[i] = strings.ToLower(raw[i])
  397. }
  398. filterOps = push(filterOps, filterV1SingleValueFromList(raw, assetfilter.FieldType))
  399. }
  400. if raw := qp.GetList(ParamFilterLabels, ","); len(raw) > 0 {
  401. filterOps = push(filterOps, filterV1DoubleValueFromList(raw, assetfilter.FieldLabel))
  402. }
  403. if raw := qp.GetList(ParamFilterRegions, ","); len(raw) > 0 {
  404. filterOps = push(filterOps, filterV1SingleLabelKeyFromList(raw, "label_topology_kubernetes_io_region", assetfilter.FieldLabel))
  405. }
  406. andFilter := opsToAnd(filterOps)
  407. if andFilter == nil {
  408. return &ast.VoidOp{} // no filter
  409. }
  410. return andFilter
  411. }
  412. // filterV1SingleValueFromList creates an OR of equality filters for a given
  413. // filter field.
  414. //
  415. // The v1 query language (e.g. "filterNamespaces=XYZ,ABC") uses OR within
  416. // a field (e.g. namespace = XYZ OR namespace = ABC)
  417. func filterV1SingleValueFromList[T ~string](rawFilterValues []string, filterField T) ast.FilterNode {
  418. var ops []ast.FilterNode
  419. for _, filterValue := range rawFilterValues {
  420. filterValue = strings.TrimSpace(filterValue)
  421. filterValue, wildcard := parseWildcardEnd(filterValue)
  422. subFilter := toEqualOp(filterField, "", filterValue, wildcard)
  423. ops = append(ops, subFilter)
  424. }
  425. return opsToOr(ops)
  426. }
  427. func filterV1SingleLabelKeyFromList[T ~string](rawFilterValues []string, labelName string, labelField T) ast.FilterNode {
  428. var ops []ast.FilterNode
  429. labelName = prom.SanitizeLabelName(labelName)
  430. for _, filterValue := range rawFilterValues {
  431. filterValue = strings.TrimSpace(filterValue)
  432. filterValue, wildcard := parseWildcardEnd(filterValue)
  433. subFilter := toEqualOp(labelField, labelName, filterValue, wildcard)
  434. ops = append(ops, subFilter)
  435. }
  436. return opsToOr(ops)
  437. }
  438. // filterV1LabelAliasMappedFromList is like filterV1SingleValueFromList but is
  439. // explicitly for labels and annotations because "label-mapped" filters (like filterTeams=)
  440. // are actually label filters with a fixed label key.
  441. func filterV1LabelAliasMappedFromList(rawFilterValues []string, labelName string) ast.FilterNode {
  442. var ops []ast.FilterNode
  443. labelName = prom.SanitizeLabelName(labelName)
  444. for _, filterValue := range rawFilterValues {
  445. filterValue = strings.TrimSpace(filterValue)
  446. filterValue, wildcard := parseWildcardEnd(filterValue)
  447. subFilter := toAllocationAliasOp(labelName, filterValue, wildcard)
  448. ops = append(ops, subFilter)
  449. }
  450. return opsToOr(ops)
  451. }
  452. // filterV1DoubleValueFromList creates an OR of key:value equality filters for
  453. // colon-split filter values.
  454. //
  455. // The v1 query language (e.g. "filterLabels=app:foo,l2:bar") uses OR within
  456. // a field (e.g. label[app] = foo OR label[l2] = bar)
  457. func filterV1DoubleValueFromList[T ~string](rawFilterValuesUnsplit []string, filterField T) ast.FilterNode {
  458. var ops []ast.FilterNode
  459. for _, unsplit := range rawFilterValuesUnsplit {
  460. if unsplit != "" {
  461. split := strings.Split(unsplit, ":")
  462. if len(split) != 2 {
  463. log.Warnf("illegal key/value filter (ignoring): %s", unsplit)
  464. continue
  465. }
  466. labelName := prom.SanitizeLabelName(strings.TrimSpace(split[0]))
  467. val := strings.TrimSpace(split[1])
  468. val, wildcard := parseWildcardEnd(val)
  469. subFilter := toEqualOp(filterField, labelName, val, wildcard)
  470. ops = append(ops, subFilter)
  471. }
  472. }
  473. return opsToOr(ops)
  474. }
  475. // parseWildcardEnd checks if the given filter value is wildcarded, meaning
  476. // it ends in "*". If it does, it removes the suffix and returns the cleaned
  477. // string and true. Otherwise, it returns the same filter and false.
  478. //
  479. // parseWildcardEnd("kube*") = "kube", true
  480. // parseWildcardEnd("kube") = "kube", false
  481. func parseWildcardEnd(rawFilterValue string) (string, bool) {
  482. return strings.TrimSuffix(rawFilterValue, "*"), strings.HasSuffix(rawFilterValue, "*")
  483. }
  484. func push(a []ast.FilterNode, item ast.FilterNode) []ast.FilterNode {
  485. if item == nil {
  486. return a
  487. }
  488. return append(a, item)
  489. }
  490. func opsToOr(ops []ast.FilterNode) ast.FilterNode {
  491. if len(ops) == 0 {
  492. return nil
  493. }
  494. if len(ops) == 1 {
  495. return ops[0]
  496. }
  497. return &ast.OrOp{
  498. Operands: ops,
  499. }
  500. }
  501. func opsToAnd(ops []ast.FilterNode) ast.FilterNode {
  502. if len(ops) == 0 {
  503. return nil
  504. }
  505. if len(ops) == 1 {
  506. return ops[0]
  507. }
  508. return &ast.AndOp{
  509. Operands: ops,
  510. }
  511. }
  512. func toEqualOp[T ~string](field T, key string, value string, wildcard bool) ast.FilterNode {
  513. left := ast.Identifier{
  514. Field: DefaultFieldByName(field),
  515. Key: key,
  516. }
  517. right := value
  518. if wildcard {
  519. return &ast.ContainsPrefixOp{
  520. Left: left,
  521. Right: right,
  522. }
  523. }
  524. return &ast.EqualOp{
  525. Left: left,
  526. Right: right,
  527. }
  528. }
  529. func toContainsOp[T ~string](field T, key string, value string, wildcard bool) ast.FilterNode {
  530. left := ast.Identifier{
  531. Field: DefaultFieldByName(field),
  532. Key: key,
  533. }
  534. right := value
  535. if wildcard {
  536. return &ast.ContainsPrefixOp{
  537. Left: left,
  538. Right: right,
  539. }
  540. }
  541. return &ast.ContainsOp{
  542. Left: left,
  543. Right: right,
  544. }
  545. }
  546. func toAllocationAliasOp(labelName string, filterValue string, wildcard bool) *ast.OrOp {
  547. // labels.Contains(labelName)
  548. labelContainsKey := &ast.ContainsOp{
  549. Left: ast.Identifier{
  550. Field: afilter.DefaultFieldByName(afilter.FieldLabel),
  551. Key: "",
  552. },
  553. Right: labelName,
  554. }
  555. // annotations.Contains(labelName)
  556. annotationContainsKey := &ast.ContainsOp{
  557. Left: ast.Identifier{
  558. Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
  559. Key: "",
  560. },
  561. Right: labelName,
  562. }
  563. // labels[labelName] equals/startswith filterValue
  564. var labelSubFilter ast.FilterNode
  565. if wildcard {
  566. labelSubFilter = &ast.ContainsPrefixOp{
  567. Left: ast.Identifier{
  568. Field: afilter.DefaultFieldByName(afilter.FieldLabel),
  569. Key: labelName,
  570. },
  571. Right: filterValue,
  572. }
  573. } else {
  574. labelSubFilter = &ast.EqualOp{
  575. Left: ast.Identifier{
  576. Field: afilter.DefaultFieldByName(afilter.FieldLabel),
  577. Key: labelName,
  578. },
  579. Right: filterValue,
  580. }
  581. }
  582. // annotations[labelName] equals/startswith filterValue
  583. var annotationSubFilter ast.FilterNode
  584. if wildcard {
  585. annotationSubFilter = &ast.ContainsPrefixOp{
  586. Left: ast.Identifier{
  587. Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
  588. Key: labelName,
  589. },
  590. Right: filterValue,
  591. }
  592. } else {
  593. annotationSubFilter = &ast.EqualOp{
  594. Left: ast.Identifier{
  595. Field: afilter.DefaultFieldByName(afilter.FieldAnnotation),
  596. Key: labelName,
  597. },
  598. Right: filterValue,
  599. }
  600. }
  601. // Logically, this is equivalent to:
  602. // (labels.Contains(labelName) && labels[labelName] = filterValue) ||
  603. // (!labels.Contains(labelName) && annotations.Contains(labelName) && annotations[labelName] = filterValue)
  604. return &ast.OrOp{
  605. Operands: []ast.FilterNode{
  606. &ast.AndOp{
  607. Operands: []ast.FilterNode{
  608. labelContainsKey,
  609. labelSubFilter,
  610. },
  611. },
  612. &ast.AndOp{
  613. Operands: []ast.FilterNode{
  614. &ast.NotOp{
  615. Operand: ast.Clone(labelContainsKey),
  616. },
  617. annotationContainsKey,
  618. annotationSubFilter,
  619. },
  620. },
  621. },
  622. }
  623. }
  624. // GetList provides a list of values from the first key if they exist, otherwise, it returns
  625. // the values from the second key.
  626. func GetList(primaryKey, secondaryKey string, qp mapper.PrimitiveMapReader) []string {
  627. if raw := qp.GetList(primaryKey, ","); len(raw) > 0 {
  628. return raw
  629. }
  630. return qp.GetList(secondaryKey, ",")
  631. }