queryfilters.go 15 KB

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