allocationfilters.go 12 KB

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