queryfilters.go 12 KB

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