queryfilters.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. package allocationfilterutil
  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. )
  10. const (
  11. ParamFilterClusters = "filterClusters"
  12. ParamFilterNodes = "filterNodes"
  13. ParamFilterNamespaces = "filterNamespaces"
  14. ParamFilterControllerKinds = "filterControllerKinds"
  15. ParamFilterControllers = "filterControllers"
  16. ParamFilterPods = "filterPods"
  17. ParamFilterContainers = "filterContainers"
  18. ParamFilterDepartments = "filterDepartments"
  19. ParamFilterEnvironments = "filterEnvironments"
  20. ParamFilterOwners = "filterOwners"
  21. ParamFilterProducts = "filterProducts"
  22. ParamFilterTeams = "filterTeams"
  23. ParamFilterAnnotations = "filterAnnotations"
  24. ParamFilterLabels = "filterLabels"
  25. ParamFilterServices = "filterServices"
  26. )
  27. // AllHTTPParamKeys returns all HTTP GET parameters used for v1 filters. It is
  28. // intended to help validate HTTP queries in handlers to help avoid e.g.
  29. // spelling errors.
  30. func AllHTTPParamKeys() []string {
  31. return []string{
  32. ParamFilterClusters,
  33. ParamFilterNodes,
  34. ParamFilterNamespaces,
  35. ParamFilterControllerKinds,
  36. ParamFilterControllers,
  37. ParamFilterPods,
  38. ParamFilterContainers,
  39. ParamFilterDepartments,
  40. ParamFilterEnvironments,
  41. ParamFilterOwners,
  42. ParamFilterProducts,
  43. ParamFilterTeams,
  44. ParamFilterAnnotations,
  45. ParamFilterLabels,
  46. ParamFilterServices,
  47. }
  48. }
  49. // ============================================================================
  50. // This file contains:
  51. // Parsing (HTTP query params -> AllocationFilter) for V1 of filters
  52. //
  53. // e.g. "filterNamespaces=ku&filterControllers=deployment:kc"
  54. // ============================================================================
  55. // parseWildcardEnd checks if the given filter value is wildcarded, meaning
  56. // it ends in "*". If it does, it removes the suffix and returns the cleaned
  57. // string and true. Otherwise, it returns the same filter and false.
  58. //
  59. // parseWildcardEnd("kube*") = "kube", true
  60. // parseWildcardEnd("kube") = "kube", false
  61. func parseWildcardEnd(rawFilterValue string) (string, bool) {
  62. return strings.TrimSuffix(rawFilterValue, "*"), strings.HasSuffix(rawFilterValue, "*")
  63. }
  64. // AllocationFilterFromParamsV1 takes a set of HTTP query parameters and
  65. // converts them to an AllocationFilter, which is a structured in-Go
  66. // representation of a set of filters.
  67. //
  68. // The HTTP query parameters are the "v1" filters attached to the Allocation
  69. // API: "filterNamespaces=", "filterNodes=", etc.
  70. //
  71. // It takes an optional LabelConfig, which if provided enables "label-mapped"
  72. // filters like "filterDepartments".
  73. //
  74. // It takes an optional ClusterMap, which if provided enables cluster name
  75. // filtering. This turns all `filterClusters=foo` arguments into the equivalent
  76. // of `clusterID = "foo" OR clusterName = "foo"`.
  77. func AllocationFilterFromParamsV1(
  78. qp mapper.PrimitiveMapReader,
  79. labelConfig *kubecost.LabelConfig,
  80. clusterMap clusters.ClusterMap,
  81. ) kubecost.AllocationFilter {
  82. filter := kubecost.AllocationFilterAnd{
  83. Filters: []kubecost.AllocationFilter{},
  84. }
  85. // ClusterMap does not provide a cluster name -> cluster ID mapping in the
  86. // interface, probably because there could be multiple IDs with the same
  87. // name. However, V1 filter logic demands that the parameters to
  88. // filterClusters= be checked against both cluster ID AND cluster name.
  89. //
  90. // To support expected filterClusters= behavior, we construct a mapping
  91. // of cluster name -> cluster IDs (could be multiple IDs for the same name)
  92. // so that we can create AllocationFilters that use only ClusterIDEquals.
  93. //
  94. //
  95. // AllocationFilter intentionally does not support cluster name filters
  96. // because those should be considered presentation-layer only.
  97. clusterNameToIDs := map[string][]string{}
  98. if clusterMap != nil {
  99. cMap := clusterMap.AsMap()
  100. for _, info := range cMap {
  101. if info == nil {
  102. continue
  103. }
  104. if _, ok := clusterNameToIDs[info.Name]; ok {
  105. clusterNameToIDs[info.Name] = append(clusterNameToIDs[info.Name], info.ID)
  106. } else {
  107. clusterNameToIDs[info.Name] = []string{info.ID}
  108. }
  109. }
  110. }
  111. // The proliferation of > 0 guards in the function is to avoid constructing
  112. // empty filter structs. While it is functionally equivalent to add empty
  113. // filter structs (they evaluate to true always) there could be overhead
  114. // when calling Matches() repeatedly for no purpose.
  115. if filterClusters := qp.GetList(ParamFilterClusters, ","); len(filterClusters) > 0 {
  116. clustersOr := kubecost.AllocationFilterOr{
  117. Filters: []kubecost.AllocationFilter{},
  118. }
  119. if idFilters := filterV1SingleValueFromList(filterClusters, kubecost.FilterClusterID); len(idFilters.Filters) > 0 {
  120. clustersOr.Filters = append(clustersOr.Filters, idFilters)
  121. }
  122. for _, rawFilterValue := range filterClusters {
  123. clusterNameFilter, wildcard := parseWildcardEnd(rawFilterValue)
  124. clusterIDsToFilter := []string{}
  125. for clusterName := range clusterNameToIDs {
  126. if wildcard && strings.HasPrefix(clusterName, clusterNameFilter) {
  127. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  128. } else if !wildcard && clusterName == clusterNameFilter {
  129. clusterIDsToFilter = append(clusterIDsToFilter, clusterNameToIDs[clusterName]...)
  130. }
  131. }
  132. for _, clusterID := range clusterIDsToFilter {
  133. clustersOr.Filters = append(clustersOr.Filters,
  134. kubecost.AllocationFilterCondition{
  135. Field: kubecost.FilterClusterID,
  136. Op: kubecost.FilterEquals,
  137. Value: clusterID,
  138. },
  139. )
  140. }
  141. }
  142. filter.Filters = append(filter.Filters, clustersOr)
  143. }
  144. if raw := qp.GetList(ParamFilterNodes, ","); len(raw) > 0 {
  145. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterNode))
  146. }
  147. if raw := qp.GetList(ParamFilterNamespaces, ","); len(raw) > 0 {
  148. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterNamespace))
  149. }
  150. if raw := qp.GetList(ParamFilterControllerKinds, ","); len(raw) > 0 {
  151. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterControllerKind))
  152. }
  153. // filterControllers= accepts controllerkind:controllername filters, e.g.
  154. // "deployment:kubecost-cost-analyzer"
  155. //
  156. // Thus, we have to make a custom OR filter for this condition.
  157. if filterControllers := qp.GetList(ParamFilterControllers, ","); len(filterControllers) > 0 {
  158. controllersOr := kubecost.AllocationFilterOr{
  159. Filters: []kubecost.AllocationFilter{},
  160. }
  161. for _, rawFilterValue := range filterControllers {
  162. split := strings.Split(rawFilterValue, ":")
  163. if len(split) == 1 {
  164. filterValue, wildcard := parseWildcardEnd(split[0])
  165. subFilter := kubecost.AllocationFilterCondition{
  166. Field: kubecost.FilterControllerName,
  167. Op: kubecost.FilterEquals,
  168. Value: filterValue,
  169. }
  170. if wildcard {
  171. subFilter.Op = kubecost.FilterStartsWith
  172. }
  173. controllersOr.Filters = append(controllersOr.Filters, subFilter)
  174. } else if len(split) == 2 {
  175. kindFilterVal := split[0]
  176. nameFilterVal, wildcard := parseWildcardEnd(split[1])
  177. kindFilter := kubecost.AllocationFilterCondition{
  178. Field: kubecost.FilterControllerKind,
  179. Op: kubecost.FilterEquals,
  180. Value: kindFilterVal,
  181. }
  182. nameFilter := kubecost.AllocationFilterCondition{
  183. Field: kubecost.FilterControllerName,
  184. Op: kubecost.FilterEquals,
  185. Value: nameFilterVal,
  186. }
  187. if wildcard {
  188. nameFilter.Op = kubecost.FilterStartsWith
  189. }
  190. // The controller name AND the controller kind must match
  191. multiFilter := kubecost.AllocationFilterAnd{
  192. Filters: []kubecost.AllocationFilter{kindFilter, nameFilter},
  193. }
  194. controllersOr.Filters = append(controllersOr.Filters, multiFilter)
  195. } else {
  196. log.Warnf("illegal filter for controller: %s", rawFilterValue)
  197. }
  198. }
  199. if len(controllersOr.Filters) > 0 {
  200. filter.Filters = append(filter.Filters, controllersOr)
  201. }
  202. }
  203. if raw := qp.GetList(ParamFilterPods, ","); len(raw) > 0 {
  204. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterPod))
  205. }
  206. if raw := qp.GetList(ParamFilterContainers, ","); len(raw) > 0 {
  207. filter.Filters = append(filter.Filters, filterV1SingleValueFromList(raw, kubecost.FilterContainer))
  208. }
  209. // Label-mapped queries require a label config to be present.
  210. if labelConfig != nil {
  211. if raw := qp.GetList(ParamFilterDepartments, ","); len(raw) > 0 {
  212. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(raw, labelConfig.DepartmentLabel))
  213. }
  214. if raw := qp.GetList(ParamFilterEnvironments, ","); len(raw) > 0 {
  215. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(raw, labelConfig.EnvironmentLabel))
  216. }
  217. if raw := qp.GetList(ParamFilterOwners, ","); len(raw) > 0 {
  218. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(raw, labelConfig.OwnerLabel))
  219. }
  220. if raw := qp.GetList(ParamFilterProducts, ","); len(raw) > 0 {
  221. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(raw, labelConfig.ProductLabel))
  222. }
  223. if raw := qp.GetList(ParamFilterTeams, ","); len(raw) > 0 {
  224. filter.Filters = append(filter.Filters, filterV1LabelAliasMappedFromList(raw, labelConfig.TeamLabel))
  225. }
  226. } else {
  227. log.Debugf("No label config is available. Not creating filters for label-mapped 'fields'.")
  228. }
  229. if raw := qp.GetList(ParamFilterAnnotations, ","); len(raw) > 0 {
  230. filter.Filters = append(filter.Filters, filterV1DoubleValueFromList(raw, kubecost.FilterAnnotation))
  231. }
  232. if raw := qp.GetList(ParamFilterLabels, ","); len(raw) > 0 {
  233. filter.Filters = append(filter.Filters, filterV1DoubleValueFromList(raw, kubecost.FilterLabel))
  234. }
  235. if filterServices := qp.GetList(ParamFilterServices, ","); len(filterServices) > 0 {
  236. // filterServices= is the only filter that uses the "contains" operator.
  237. servicesFilter := kubecost.AllocationFilterOr{
  238. Filters: []kubecost.AllocationFilter{},
  239. }
  240. for _, filterValue := range filterServices {
  241. // TODO: wildcard support
  242. filterValue, wildcard := parseWildcardEnd(filterValue)
  243. subFilter := kubecost.AllocationFilterCondition{
  244. Field: kubecost.FilterServices,
  245. Op: kubecost.FilterContains,
  246. Value: filterValue,
  247. }
  248. if wildcard {
  249. subFilter.Op = kubecost.FilterContainsPrefix
  250. }
  251. servicesFilter.Filters = append(servicesFilter.Filters, subFilter)
  252. }
  253. filter.Filters = append(filter.Filters, servicesFilter)
  254. }
  255. return filter
  256. }
  257. // filterV1SingleValueFromList creates an OR of equality filters for a given
  258. // filter field.
  259. //
  260. // The v1 query language (e.g. "filterNamespaces=XYZ,ABC") uses OR within
  261. // a field (e.g. namespace = XYZ OR namespace = ABC)
  262. func filterV1SingleValueFromList(rawFilterValues []string, filterField kubecost.FilterField) kubecost.AllocationFilterOr {
  263. filter := kubecost.AllocationFilterOr{
  264. Filters: []kubecost.AllocationFilter{},
  265. }
  266. for _, filterValue := range rawFilterValues {
  267. filterValue = strings.TrimSpace(filterValue)
  268. filterValue, wildcard := parseWildcardEnd(filterValue)
  269. subFilter := kubecost.AllocationFilterCondition{
  270. Field: filterField,
  271. // All v1 filters are equality comparisons
  272. Op: kubecost.FilterEquals,
  273. Value: filterValue,
  274. }
  275. if wildcard {
  276. subFilter.Op = kubecost.FilterStartsWith
  277. }
  278. filter.Filters = append(filter.Filters, subFilter)
  279. }
  280. return filter
  281. }
  282. // filterV1LabelAliasMappedFromList is like filterV1SingleValueFromList but is
  283. // explicitly for labels and annotations because "label-mapped" filters (like filterTeams=)
  284. // are actually label filters with a fixed label key.
  285. func filterV1LabelAliasMappedFromList(rawFilterValues []string, labelName string) kubecost.AllocationFilterOr {
  286. filter := kubecost.AllocationFilterOr{
  287. Filters: []kubecost.AllocationFilter{},
  288. }
  289. labelName = prom.SanitizeLabelName(labelName)
  290. for _, filterValue := range rawFilterValues {
  291. filterValue = strings.TrimSpace(filterValue)
  292. filterValue, wildcard := parseWildcardEnd(filterValue)
  293. subFilter := kubecost.AllocationFilterCondition{
  294. Field: kubecost.FilterAlias,
  295. // All v1 filters are equality comparisons
  296. Op: kubecost.FilterEquals,
  297. Key: labelName,
  298. Value: filterValue,
  299. }
  300. if wildcard {
  301. subFilter.Op = kubecost.FilterStartsWith
  302. }
  303. filter.Filters = append(filter.Filters, subFilter)
  304. }
  305. return filter
  306. }
  307. // filterV1DoubleValueFromList creates an OR of key:value equality filters for
  308. // colon-split filter values.
  309. //
  310. // The v1 query language (e.g. "filterLabels=app:foo,l2:bar") uses OR within
  311. // a field (e.g. label[app] = foo OR label[l2] = bar)
  312. func filterV1DoubleValueFromList(rawFilterValuesUnsplit []string, filterField kubecost.FilterField) kubecost.AllocationFilterOr {
  313. filter := kubecost.AllocationFilterOr{
  314. Filters: []kubecost.AllocationFilter{},
  315. }
  316. for _, unsplit := range rawFilterValuesUnsplit {
  317. if unsplit != "" {
  318. split := strings.Split(unsplit, ":")
  319. if len(split) != 2 {
  320. log.Warnf("illegal key/value filter (ignoring): %s", unsplit)
  321. continue
  322. }
  323. labelName := prom.SanitizeLabelName(strings.TrimSpace(split[0]))
  324. val := strings.TrimSpace(split[1])
  325. val, wildcard := parseWildcardEnd(val)
  326. subFilter := kubecost.AllocationFilterCondition{
  327. Field: filterField,
  328. // All v1 filters are equality comparisons
  329. Op: kubecost.FilterEquals,
  330. Key: labelName,
  331. Value: val,
  332. }
  333. if wildcard {
  334. subFilter.Op = kubecost.FilterStartsWith
  335. }
  336. filter.Filters = append(filter.Filters, subFilter)
  337. }
  338. }
  339. return filter
  340. }