filterutil.go 22 KB

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