queryfilters.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. package allocationfilterutil
  2. import (
  3. "fmt"
  4. "reflect"
  5. "strings"
  6. "github.com/opencost/opencost/pkg/costmodel/clusters"
  7. "github.com/opencost/opencost/pkg/kubecost"
  8. "github.com/opencost/opencost/pkg/log"
  9. "github.com/opencost/opencost/pkg/prom"
  10. "github.com/opencost/opencost/pkg/util/mapper"
  11. )
  12. const (
  13. ParamFilterClusters = "filterClusters"
  14. ParamFilterNodes = "filterNodes"
  15. ParamFilterNamespaces = "filterNamespaces"
  16. ParamFilterControllerKinds = "filterControllerKinds"
  17. ParamFilterControllers = "filterControllers"
  18. ParamFilterPods = "filterPods"
  19. ParamFilterContainers = "filterContainers"
  20. ParamFilterDepartments = "filterDepartments"
  21. ParamFilterEnvironments = "filterEnvironments"
  22. ParamFilterOwners = "filterOwners"
  23. ParamFilterProducts = "filterProducts"
  24. ParamFilterTeams = "filterTeams"
  25. ParamFilterAnnotations = "filterAnnotations"
  26. ParamFilterLabels = "filterLabels"
  27. ParamFilterServices = "filterServices"
  28. )
  29. var allocationFilterFieldMap = map[string]string{
  30. kubecost.AllocationClusterProp: ParamFilterClusters,
  31. kubecost.FilterNode: ParamFilterNodes,
  32. kubecost.AllocationNamespaceProp: ParamFilterNamespaces,
  33. kubecost.AllocationControllerKindProp: ParamFilterControllerKinds,
  34. kubecost.AllocationControllerProp: ParamFilterControllers,
  35. kubecost.AllocationPodProp: ParamFilterPods,
  36. kubecost.AllocationContainerProp: ParamFilterContainers,
  37. kubecost.AllocationDepartmentProp: ParamFilterDepartments,
  38. kubecost.AllocationEnvironmentProp: ParamFilterEnvironments,
  39. kubecost.AllocationOwnerProp: ParamFilterOwners,
  40. kubecost.AllocationProductProp: ParamFilterProducts,
  41. kubecost.AllocationTeamProp: ParamFilterTeams,
  42. kubecost.AllocationAnnotationProp: ParamFilterAnnotations,
  43. kubecost.AllocationLabelProp: ParamFilterLabels,
  44. kubecost.AllocationServiceProp: ParamFilterServices,
  45. }
  46. func GetAllocationFilterForTheAllocationProperty(allocationProp string) (string, error) {
  47. if _, ok := allocationFilterFieldMap[allocationProp]; !ok {
  48. return "", fmt.Errorf("unknown allocation property %s", allocationProp)
  49. }
  50. return allocationFilterFieldMap[allocationProp], nil
  51. }
  52. // AllHTTPParamKeys returns all HTTP GET parameters used for v1 filters. It is
  53. // intended to help validate HTTP queries in handlers to help avoid e.g.
  54. // spelling errors.
  55. func AllHTTPParamKeys() []string {
  56. return []string{
  57. ParamFilterClusters,
  58. ParamFilterNodes,
  59. ParamFilterNamespaces,
  60. ParamFilterControllerKinds,
  61. ParamFilterControllers,
  62. ParamFilterPods,
  63. ParamFilterContainers,
  64. ParamFilterDepartments,
  65. ParamFilterEnvironments,
  66. ParamFilterOwners,
  67. ParamFilterProducts,
  68. ParamFilterTeams,
  69. ParamFilterAnnotations,
  70. ParamFilterLabels,
  71. ParamFilterServices,
  72. }
  73. }
  74. type FilterV1 struct {
  75. Annotations []string `json:"annotations,omitempty"`
  76. Containers []string `json:"containers,omitempty"`
  77. Controllers []string `json:"controllers,omitempty"`
  78. ControllerKinds []string `json:"controllerKinds,omitempty"`
  79. Clusters []string `json:"clusters,omitempty"`
  80. Departments []string `json:"departments,omitempty"`
  81. Environments []string `json:"environments,omitempty"`
  82. Labels []string `json:"labels,omitempty"`
  83. Namespaces []string `json:"namespaces,omitempty"`
  84. Nodes []string `json:"nodes,omitempty"`
  85. Owners []string `json:"owners,omitempty"`
  86. Pods []string `json:"pods,omitempty"`
  87. Products []string `json:"products,omitempty"`
  88. Services []string `json:"services,omitempty"`
  89. Teams []string `json:"teams,omitempty"`
  90. }
  91. func (f FilterV1) Equals(that FilterV1) bool {
  92. return reflect.DeepEqual(f.Annotations, that.Annotations) &&
  93. reflect.DeepEqual(f.Containers, that.Containers) &&
  94. reflect.DeepEqual(f.Controllers, that.Controllers) &&
  95. reflect.DeepEqual(f.ControllerKinds, that.ControllerKinds) &&
  96. reflect.DeepEqual(f.Clusters, that.Clusters) &&
  97. reflect.DeepEqual(f.Departments, that.Departments) &&
  98. reflect.DeepEqual(f.Environments, that.Environments) &&
  99. reflect.DeepEqual(f.Labels, that.Labels) &&
  100. reflect.DeepEqual(f.Namespaces, that.Namespaces) &&
  101. reflect.DeepEqual(f.Nodes, that.Nodes) &&
  102. reflect.DeepEqual(f.Owners, that.Owners) &&
  103. reflect.DeepEqual(f.Pods, that.Pods) &&
  104. reflect.DeepEqual(f.Products, that.Products) &&
  105. reflect.DeepEqual(f.Services, that.Services) &&
  106. reflect.DeepEqual(f.Teams, that.Teams)
  107. }
  108. // ============================================================================
  109. // This file contains:
  110. // Parsing (HTTP query params -> AllocationFilter) for V1 of filters
  111. //
  112. // e.g. "filterNamespaces=ku&filterControllers=deployment:kc"
  113. // ============================================================================
  114. // parseWildcardEnd checks if the given filter value is wildcarded, meaning
  115. // it ends in "*". If it does, it removes the suffix and returns the cleaned
  116. // string and true. Otherwise, it returns the same filter and false.
  117. //
  118. // parseWildcardEnd("kube*") = "kube", true
  119. // parseWildcardEnd("kube") = "kube", false
  120. func parseWildcardEnd(rawFilterValue string) (string, bool) {
  121. return strings.TrimSuffix(rawFilterValue, "*"), strings.HasSuffix(rawFilterValue, "*")
  122. }
  123. // ParseAllocationFilterV1 takes a FilterV1 struct and
  124. // converts them to an AllocationFilter, which is a structured in-Go
  125. // representation of a set of filters.
  126. //
  127. // The HTTP query parameters are the "v1" filters attached to the Allocation
  128. // API: "filterNamespaces=", "filterNodes=", etc.
  129. //
  130. // It takes an optional LabelConfig, which if provided enables "label-mapped"
  131. // filters like "filterDepartments".
  132. //
  133. // It takes an optional ClusterMap, which if provided enables cluster name
  134. // filtering. This turns all `filterClusters=foo` arguments into the equivalent
  135. // of `clusterID = "foo" OR clusterName = "foo"`.
  136. func ParseAllocationFilterV1(filters FilterV1, labelConfig *kubecost.LabelConfig, clusterMap clusters.ClusterMap) kubecost.AllocationFilter {
  137. filter := kubecost.AllocationFilterAnd{
  138. Filters: []kubecost.AllocationFilter{},
  139. }
  140. // ClusterMap does not provide a cluster name -> cluster ID mapping in the
  141. // interface, probably because there could be multiple IDs with the same
  142. // name. However, V1 filter logic demands that the parameters to
  143. // filterClusters= be checked against both cluster ID AND cluster name.
  144. //
  145. // To support expected filterClusters= behavior, we construct a mapping
  146. // of cluster name -> cluster IDs (could be multiple IDs for the same name)
  147. // so that we can create AllocationFilters that use only ClusterIDEquals.
  148. //
  149. //
  150. // AllocationFilter intentionally does not support cluster name filters
  151. // because those should be considered presentation-layer only.
  152. clusterNameToIDs := map[string][]string{}
  153. if clusterMap != nil {
  154. cMap := clusterMap.AsMap()
  155. for _, info := range cMap {
  156. if info == nil {
  157. continue
  158. }
  159. if _, ok := clusterNameToIDs[info.Name]; ok {
  160. clusterNameToIDs[info.Name] = append(clusterNameToIDs[info.Name], info.ID)
  161. } else {
  162. clusterNameToIDs[info.Name] = []string{info.ID}
  163. }
  164. }
  165. }
  166. // The proliferation of > 0 guards in the function is to avoid constructing
  167. // empty filter structs. While it is functionally equivalent to add empty
  168. // filter structs (they evaluate to true always) there could be overhead
  169. // when calling Matches() repeatedly for no purpose.
  170. if len(filters.Clusters) > 0 {
  171. clustersOr := kubecost.AllocationFilterOr{
  172. Filters: []kubecost.AllocationFilter{},
  173. }
  174. if idFilters := filterV1SingleValueFromList(filters.Clusters, kubecost.FilterClusterID); len(idFilters.Filters) > 0 {
  175. clustersOr.Filters = append(clustersOr.Filters, idFilters)
  176. }
  177. for _, rawFilterValue := range filters.Clusters {
  178. clusterNameFilter, wildcard := parseWildcardEnd(rawFilterValue)
  179. clusterIDsToFilter := []string{}
  180. for clusterName := range clusterNameToIDs {
  181. if wildcard && strings.HasPrefix(clusterName, clusterNameFilter) {
  182. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  183. } else if !wildcard && clusterName == clusterNameFilter {
  184. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  185. }
  186. }
  187. for _, clusterID := range clusterIDsToFilter {
  188. clustersOr.Filters = append(clustersOr.Filters,
  189. kubecost.AllocationFilterCondition{
  190. Field: kubecost.FilterClusterID,
  191. Op: kubecost.FilterEquals,
  192. Value: clusterID,
  193. },
  194. )
  195. }
  196. }
  197. filter.Filters = append(filter.Filters, clustersOr)
  198. }
  199. if len(filters.Nodes) > 0 {
  200. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(filters.Nodes, kubecost.FilterNode))
  201. }
  202. if len(filters.Namespaces) > 0 {
  203. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(filters.Namespaces, kubecost.FilterNamespace))
  204. }
  205. if len(filters.ControllerKinds) > 0 {
  206. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(filters.ControllerKinds, kubecost.FilterControllerKind))
  207. }
  208. // filterControllers= accepts controllerkind:controllername filters, e.g.
  209. // "deployment:kubecost-cost-analyzer"
  210. //
  211. // Thus, we have to make a custom OR filter for this condition.
  212. if len(filters.Controllers) > 0 {
  213. controllersOr := kubecost.AllocationFilterOr{
  214. Filters: []kubecost.AllocationFilter{},
  215. }
  216. for _, rawFilterValue := range filters.Controllers {
  217. split := strings.Split(rawFilterValue, ":")
  218. if len(split) == 1 {
  219. filterValue, wildcard := parseWildcardEnd(split[0])
  220. subFilter := kubecost.AllocationFilterCondition{
  221. Field: kubecost.FilterControllerName,
  222. Op: kubecost.FilterEquals,
  223. Value: filterValue,
  224. }
  225. if wildcard {
  226. subFilter.Op = kubecost.FilterStartsWith
  227. }
  228. controllersOr.Filters = append(controllersOr.Filters, subFilter)
  229. } else if len(split) == 2 {
  230. kindFilterVal := split[0]
  231. nameFilterVal, wildcard := parseWildcardEnd(split[1])
  232. kindFilter := kubecost.AllocationFilterCondition{
  233. Field: kubecost.FilterControllerKind,
  234. Op: kubecost.FilterEquals,
  235. Value: kindFilterVal,
  236. }
  237. nameFilter := kubecost.AllocationFilterCondition{
  238. Field: kubecost.FilterControllerName,
  239. Op: kubecost.FilterEquals,
  240. Value: nameFilterVal,
  241. }
  242. if wildcard {
  243. nameFilter.Op = kubecost.FilterStartsWith
  244. }
  245. // The controller name AND the controller kind must match
  246. multiFilter := kubecost.AllocationFilterAnd{
  247. Filters: []kubecost.AllocationFilter{kindFilter, nameFilter},
  248. }
  249. controllersOr.Filters = append(controllersOr.Filters, multiFilter)
  250. } else {
  251. log.Warnf("illegal filter for controller: %s", rawFilterValue)
  252. }
  253. }
  254. if len(controllersOr.Filters) > 0 {
  255. filter.Filters = append(filter.Filters, controllersOr)
  256. }
  257. }
  258. if len(filters.Pods) > 0 {
  259. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(filters.Pods, kubecost.FilterPod))
  260. }
  261. if len(filters.Containers) > 0 {
  262. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(filters.Containers, kubecost.FilterContainer))
  263. }
  264. // Label-mapped queries require a label config to be present.
  265. if labelConfig != nil {
  266. if len(filters.Departments) > 0 {
  267. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(filters.Departments, labelConfig.DepartmentLabel))
  268. }
  269. if len(filters.Environments) > 0 {
  270. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(filters.Environments, labelConfig.EnvironmentLabel))
  271. }
  272. if len(filters.Owners) > 0 {
  273. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(filters.Owners, labelConfig.OwnerLabel))
  274. }
  275. if len(filters.Products) > 0 {
  276. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(filters.Products, labelConfig.ProductLabel))
  277. }
  278. if len(filters.Teams) > 0 {
  279. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(filters.Teams, labelConfig.TeamLabel))
  280. }
  281. } else {
  282. log.Debugf("No label config is available. Not creating filters for label-mapped 'fields'.")
  283. }
  284. if len(filters.Annotations) > 0 {
  285. filter.Filters = append(filter.Filters, filterV1DoubleValueFromList(filters.Annotations, kubecost.FilterAnnotation))
  286. }
  287. if len(filters.Labels) > 0 {
  288. filter.Filters = append(filter.Filters, filterV1DoubleValueFromList(filters.Labels, kubecost.FilterLabel))
  289. }
  290. if len(filters.Services) > 0 {
  291. // filterServices= is the only filter that uses the "contains" operator.
  292. servicesFilter := kubecost.AllocationFilterOr{
  293. Filters: []kubecost.AllocationFilter{},
  294. }
  295. for _, filterValue := range filters.Services {
  296. // TODO: wildcard support
  297. filterValue, wildcard := parseWildcardEnd(filterValue)
  298. subFilter := kubecost.AllocationFilterCondition{
  299. Field: kubecost.FilterServices,
  300. Op: kubecost.FilterContains,
  301. Value: filterValue,
  302. }
  303. if wildcard {
  304. subFilter.Op = kubecost.FilterContainsPrefix
  305. }
  306. servicesFilter.Filters = append(servicesFilter.Filters, subFilter)
  307. }
  308. filter.Filters = append(filter.Filters, servicesFilter)
  309. }
  310. return filter
  311. }
  312. // AllocationFilterFromParamsV1 takes a set of HTTP query parameters and
  313. // converts them to an AllocationFilter, which is a structured in-Go
  314. // representation of a set of filters.
  315. //
  316. // The HTTP query parameters are the "v1" filters attached to the Allocation
  317. // API: "filterNamespaces=", "filterNodes=", etc.
  318. //
  319. // It takes an optional LabelConfig, which if provided enables "label-mapped"
  320. // filters like "filterDepartments".
  321. //
  322. // It takes an optional ClusterMap, which if provided enables cluster name
  323. // filtering. This turns all `filterClusters=foo` arguments into the equivalent
  324. // of `clusterID = "foo" OR clusterName = "foo"`.
  325. func AllocationFilterFromParamsV1(
  326. qp mapper.PrimitiveMapReader,
  327. labelConfig *kubecost.LabelConfig,
  328. clusterMap clusters.ClusterMap,
  329. ) kubecost.AllocationFilter {
  330. filter := ConvertFilterQueryParams(qp, labelConfig)
  331. return ParseAllocationFilterV1(filter, labelConfig, clusterMap)
  332. }
  333. // filterV1SingleValueFromList creates an OR of equality filters for a given
  334. // filter field.
  335. //
  336. // The v1 query language (e.g. "filterNamespaces=XYZ,ABC") uses OR within
  337. // a field (e.g. namespace = XYZ OR namespace = ABC)
  338. func filterV1SingleValueFromList(rawFilterValues []string, filterField kubecost.FilterField) kubecost.AllocationFilterOr {
  339. filter := kubecost.AllocationFilterOr{
  340. Filters: []kubecost.AllocationFilter{},
  341. }
  342. for _, filterValue := range rawFilterValues {
  343. filterValue = strings.TrimSpace(filterValue)
  344. filterValue, wildcard := parseWildcardEnd(filterValue)
  345. subFilter := kubecost.AllocationFilterCondition{
  346. Field: filterField,
  347. // All v1 filters are equality comparisons
  348. Op: kubecost.FilterEquals,
  349. Value: filterValue,
  350. }
  351. if wildcard {
  352. subFilter.Op = kubecost.FilterStartsWith
  353. }
  354. filter.Filters = append(filter.Filters, subFilter)
  355. }
  356. return filter
  357. }
  358. func ConvertFilterQueryParams(qp mapper.PrimitiveMapReader, labelConfig *kubecost.LabelConfig) FilterV1 {
  359. filter := FilterV1{
  360. Annotations: qp.GetList(ParamFilterAnnotations, ","),
  361. Containers: qp.GetList(ParamFilterContainers, ","),
  362. Controllers: qp.GetList(ParamFilterControllers, ","),
  363. ControllerKinds: qp.GetList(ParamFilterControllerKinds, ","),
  364. Clusters: qp.GetList(ParamFilterClusters, ","),
  365. Labels: qp.GetList(ParamFilterLabels, ","),
  366. Namespaces: qp.GetList(ParamFilterNamespaces, ","),
  367. Nodes: qp.GetList(ParamFilterNodes, ","),
  368. Pods: qp.GetList(ParamFilterPods, ","),
  369. Services: qp.GetList(ParamFilterServices, ","),
  370. }
  371. if labelConfig != nil {
  372. filter.Departments = qp.GetList(ParamFilterDepartments, ",")
  373. filter.Environments = qp.GetList(ParamFilterEnvironments, ",")
  374. filter.Owners = qp.GetList(ParamFilterOwners, ",")
  375. filter.Products = qp.GetList(ParamFilterProducts, ",")
  376. filter.Teams = qp.GetList(ParamFilterTeams, ",")
  377. } else {
  378. log.Debugf("No label config is available. Not creating filters for label-mapped 'fields'.")
  379. }
  380. return filter
  381. }
  382. // filterV1LabelAliasMappedFromList is like filterV1SingleValueFromList but is
  383. // explicitly for labels and annotations because "label-mapped" filters (like filterTeams=)
  384. // are actually label filters with a fixed label key.
  385. func filterV1LabelAliasMappedFromList(rawFilterValues []string, labelName string) kubecost.AllocationFilterOr {
  386. filter := kubecost.AllocationFilterOr{
  387. Filters: []kubecost.AllocationFilter{},
  388. }
  389. labelName = prom.SanitizeLabelName(labelName)
  390. for _, filterValue := range rawFilterValues {
  391. filterValue = strings.TrimSpace(filterValue)
  392. filterValue, wildcard := parseWildcardEnd(filterValue)
  393. subFilter := kubecost.AllocationFilterCondition{
  394. Field: kubecost.FilterAlias,
  395. // All v1 filters are equality comparisons
  396. Op: kubecost.FilterEquals,
  397. Key: labelName,
  398. Value: filterValue,
  399. }
  400. if wildcard {
  401. subFilter.Op = kubecost.FilterStartsWith
  402. }
  403. filter.Filters = append(filter.Filters, subFilter)
  404. }
  405. return filter
  406. }
  407. // filterV1DoubleValueFromList creates an OR of key:value equality filters for
  408. // colon-split filter values.
  409. //
  410. // The v1 query language (e.g. "filterLabels=app:foo,l2:bar") uses OR within
  411. // a field (e.g. label[app] = foo OR label[l2] = bar)
  412. func filterV1DoubleValueFromList(rawFilterValuesUnsplit []string, filterField kubecost.FilterField) kubecost.AllocationFilterOr {
  413. filter := kubecost.AllocationFilterOr{
  414. Filters: []kubecost.AllocationFilter{},
  415. }
  416. for _, unsplit := range rawFilterValuesUnsplit {
  417. if unsplit != "" {
  418. split := strings.Split(unsplit, ":")
  419. if len(split) != 2 {
  420. log.Warnf("illegal key/value filter (ignoring): %s", unsplit)
  421. continue
  422. }
  423. labelName := prom.SanitizeLabelName(strings.TrimSpace(split[0]))
  424. val := strings.TrimSpace(split[1])
  425. val, wildcard := parseWildcardEnd(val)
  426. subFilter := kubecost.AllocationFilterCondition{
  427. Field: filterField,
  428. // All v1 filters are equality comparisons
  429. Op: kubecost.FilterEquals,
  430. Key: labelName,
  431. Value: val,
  432. }
  433. if wildcard {
  434. subFilter.Op = kubecost.FilterStartsWith
  435. }
  436. filter.Filters = append(filter.Filters, subFilter)
  437. }
  438. }
  439. return filter
  440. }