allocationfilter.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. package kubecost
  2. import (
  3. "fmt"
  4. "sort"
  5. "strings"
  6. "github.com/opencost/opencost/pkg/log"
  7. )
  8. // FilterField is an enum that represents Allocation-specific fields that can be
  9. // filtered on (namespace, label, etc.)
  10. type FilterField string
  11. // If you add a FilterField, MAKE SURE TO UPDATE ALL FILTER IMPLEMENTATIONS! Go
  12. // does not enforce exhaustive pattern matching on "enum" types.
  13. const (
  14. FilterClusterID FilterField = "clusterid"
  15. FilterNode = "node"
  16. FilterNamespace = "namespace"
  17. FilterControllerKind = "controllerkind"
  18. FilterControllerName = "controllername"
  19. FilterPod = "pod"
  20. FilterContainer = "container"
  21. // Filtering based on label aliases (team, department, etc.) should be a
  22. // responsibility of the query handler. By the time it reaches this
  23. // structured representation, we shouldn't have to be aware of what is
  24. // aliased to what.
  25. FilterLabel = "label"
  26. FilterAnnotation = "annotation"
  27. FilterServices = "services"
  28. )
  29. // FilterOp is an enum that represents operations that can be performed
  30. // when filtering (equality, inequality, etc.)
  31. type FilterOp string
  32. // If you add a FilterOp, MAKE SURE TO UPDATE ALL FILTER IMPLEMENTATIONS! Go
  33. // does not enforce exhaustive pattern matching on "enum" types.
  34. const (
  35. // FilterEquals is the equality operator
  36. // "kube-system" FilterEquals "kube-system" = true
  37. // "kube-syste" FilterEquals "kube-system" = false
  38. FilterEquals FilterOp = "equals"
  39. // FilterNotEquals is the inequality operator
  40. FilterNotEquals = "notequals"
  41. // FilterContains is an array/slice membership operator
  42. // ["a", "b", "c"] FilterContains "a" = true
  43. FilterContains = "contains"
  44. // FilterNotContains is an array/slice non-membership operator
  45. // ["a", "b", "c"] FilterNotContains "d" = true
  46. FilterNotContains = "notcontains"
  47. // FilterStartsWith matches strings with the given prefix.
  48. // "kube-system" StartsWith "kube" = true
  49. //
  50. // When comparing with a field represented by an array/slice, this is like
  51. // applying FilterContains to every element of the slice.
  52. FilterStartsWith = "startswith"
  53. // FilterContainsPrefix is like FilterContains, but using StartsWith instead
  54. // of Equals.
  55. // ["kube-system", "abc123"] ContainsPrefix ["kube"] = true
  56. FilterContainsPrefix = "containsprefix"
  57. )
  58. // AllocationFilter represents anything that can be used to filter an
  59. // Allocation.
  60. //
  61. // Implement this interface with caution. While it is generic, it
  62. // is intended to be introspectable so query handlers can perform various
  63. // optimizations. These optimizations include:
  64. // - Routing a query to the most optimal cache
  65. // - Querying backing data stores efficiently (e.g. translation to SQL)
  66. //
  67. // Custom implementations of this interface outside of this package should not
  68. // expect to receive these benefits. Passing a custom implementation to a
  69. // handler may in errors.
  70. type AllocationFilter interface {
  71. // Matches is the canonical in-Go function for determing if an Allocation
  72. // matches a filter.
  73. Matches(a *Allocation) bool
  74. // Flattened converts a filter into a minimal form, removing unnecessary
  75. // intermediate objects, like single-element or zero-element AND and OR
  76. // conditions.
  77. //
  78. // It returns nil if the filter is filtering nothing.
  79. //
  80. // Example:
  81. // (and (or (namespaceequals "kubecost")) (or)) ->
  82. // (namespaceequals "kubecost")
  83. //
  84. // (and (or)) -> nil
  85. Flattened() AllocationFilter
  86. String() string
  87. // Equals returns true if the two AllocationFilters are logically
  88. // equivalent.
  89. Equals(AllocationFilter) bool
  90. }
  91. // AllocationFilterCondition is the lowest-level type of filter. It represents
  92. // the a filter operation (equality, inequality, etc.) on a field (namespace,
  93. // label, etc.).
  94. type AllocationFilterCondition struct {
  95. Field FilterField
  96. Op FilterOp
  97. // Key is for filters that require key-value pairs, like labels or
  98. // annotations.
  99. //
  100. // A filter of 'label[app]:"foo"' has Key="app" and Value="foo"
  101. Key string
  102. // Value is for _all_ filters. A filter of 'namespace:"kubecost"' has
  103. // Value="kubecost"
  104. Value string
  105. }
  106. func (afc AllocationFilterCondition) String() string {
  107. if afc.Key == "" {
  108. return fmt.Sprintf(`(%s %s "%s")`, afc.Op, afc.Field, afc.Value)
  109. }
  110. return fmt.Sprintf(`(%s %s[%s] "%s")`, afc.Op, afc.Field, afc.Key, afc.Value)
  111. }
  112. // Flattened returns itself because you cannot flatten a base condition further
  113. func (filter AllocationFilterCondition) Flattened() AllocationFilter {
  114. return filter
  115. }
  116. func (left AllocationFilterCondition) Equals(right AllocationFilter) bool {
  117. if rightAFC, ok := right.(AllocationFilterCondition); ok {
  118. return left == rightAFC
  119. }
  120. return false
  121. }
  122. // AllocationFilterOr is a set of filters that should be evaluated as a logical
  123. // OR.
  124. type AllocationFilterOr struct {
  125. Filters []AllocationFilter
  126. }
  127. func (af AllocationFilterOr) String() string {
  128. s := "(or"
  129. for _, f := range af.Filters {
  130. s += fmt.Sprintf(" %s", f)
  131. }
  132. s += ")"
  133. return s
  134. }
  135. // flattened returns a new slice of filters after flattening.
  136. func flattened(filters []AllocationFilter) []AllocationFilter {
  137. var flattenedFilters []AllocationFilter
  138. for _, innerFilter := range filters {
  139. if innerFilter == nil {
  140. continue
  141. }
  142. flattenedInner := innerFilter.Flattened()
  143. if flattenedInner != nil {
  144. flattenedFilters = append(flattenedFilters, flattenedInner)
  145. }
  146. }
  147. return flattenedFilters
  148. }
  149. // Flattened converts a filter into a minimal form, removing unnecessary
  150. // intermediate objects
  151. //
  152. // Flattened returns:
  153. // - nil if filter contains no filters
  154. // - the inner filter if filter contains one filter
  155. // - an equivalent AllocationFilterOr if filter contains more than one filter
  156. func (filter AllocationFilterOr) Flattened() AllocationFilter {
  157. flattenedFilters := flattened(filter.Filters)
  158. if len(flattenedFilters) == 0 {
  159. return nil
  160. }
  161. if len(flattenedFilters) == 1 {
  162. return flattenedFilters[0]
  163. }
  164. return AllocationFilterOr{Filters: flattenedFilters}
  165. }
  166. func (filter AllocationFilterOr) sort() {
  167. for _, inner := range filter.Filters {
  168. if and, ok := inner.(AllocationFilterAnd); ok {
  169. and.sort()
  170. } else if or, ok := inner.(AllocationFilterOr); ok {
  171. or.sort()
  172. }
  173. }
  174. // While a slight hack, we can rely on the string serialization of the
  175. // inner filters to get a sortable representation.
  176. sort.SliceStable(filter.Filters, func(i, j int) bool {
  177. return filter.Filters[i].String() < filter.Filters[j].String()
  178. })
  179. }
  180. func (left AllocationFilterOr) Equals(right AllocationFilter) bool {
  181. // The type cast takes care of right == nil as well
  182. rightOr, ok := right.(AllocationFilterOr)
  183. if !ok {
  184. return false
  185. }
  186. if len(left.Filters) != len(rightOr.Filters) {
  187. return false
  188. }
  189. left.sort()
  190. rightOr.sort()
  191. for i := range left.Filters {
  192. if !left.Filters[i].Equals(rightOr.Filters[i]) {
  193. return false
  194. }
  195. }
  196. return true
  197. }
  198. // AllocationFilterOr is a set of filters that should be evaluated as a logical
  199. // AND.
  200. type AllocationFilterAnd struct {
  201. Filters []AllocationFilter
  202. }
  203. func (af AllocationFilterAnd) String() string {
  204. s := "(and"
  205. for _, f := range af.Filters {
  206. s += fmt.Sprintf(" %s", f)
  207. }
  208. s += ")"
  209. return s
  210. }
  211. // Flattened converts a filter into a minimal form, removing unnecessary
  212. // intermediate objects
  213. //
  214. // Flattened returns:
  215. // - nil if filter contains no filters
  216. // - the inner filter if filter contains one filter
  217. // - an equivalent AllocationFilterAnd if filter contains more than one filter
  218. func (filter AllocationFilterAnd) Flattened() AllocationFilter {
  219. flattenedFilters := flattened(filter.Filters)
  220. if len(flattenedFilters) == 0 {
  221. return nil
  222. }
  223. if len(flattenedFilters) == 1 {
  224. return flattenedFilters[0]
  225. }
  226. return AllocationFilterAnd{Filters: flattenedFilters}
  227. }
  228. func (filter AllocationFilterAnd) sort() {
  229. for _, inner := range filter.Filters {
  230. if and, ok := inner.(AllocationFilterAnd); ok {
  231. and.sort()
  232. } else if or, ok := inner.(AllocationFilterOr); ok {
  233. or.sort()
  234. }
  235. }
  236. // While a slight hack, we can rely on the string serialization of the
  237. // inner filters.
  238. sort.SliceStable(filter.Filters, func(i, j int) bool {
  239. return filter.Filters[i].String() < filter.Filters[j].String()
  240. })
  241. }
  242. func (left AllocationFilterAnd) Equals(right AllocationFilter) bool {
  243. // The type cast takes care of right == nil as well
  244. rightAnd, ok := right.(AllocationFilterAnd)
  245. if !ok {
  246. return false
  247. }
  248. if len(left.Filters) != len(rightAnd.Filters) {
  249. return false
  250. }
  251. left.sort()
  252. rightAnd.sort()
  253. for i := range left.Filters {
  254. if !left.Filters[i].Equals(rightAnd.Filters[i]) {
  255. return false
  256. }
  257. }
  258. return true
  259. }
  260. func (filter AllocationFilterCondition) Matches(a *Allocation) bool {
  261. if a == nil {
  262. return false
  263. }
  264. if a.Properties == nil {
  265. return false
  266. }
  267. // The Allocation's value for the field to compare
  268. // We use an interface{} so this can contain the services []string slice
  269. var valueToCompare interface{}
  270. // toCompareMissing will be true if the value to be compared is missing in
  271. // the Allocation. For example, if we're filtering based on the value of
  272. // the "app" label, but the Allocation doesn't have an "app" label, this
  273. // will become true. This lets us deal with != gracefully.
  274. toCompareMissing := false
  275. // This switch maps the filter.Field to the field to be compared in
  276. // a.Properties and sets valueToCompare from the value in a.Properties.
  277. switch filter.Field {
  278. case FilterClusterID:
  279. valueToCompare = a.Properties.Cluster
  280. case FilterNode:
  281. valueToCompare = a.Properties.Node
  282. case FilterNamespace:
  283. valueToCompare = a.Properties.Namespace
  284. case FilterControllerKind:
  285. valueToCompare = a.Properties.ControllerKind
  286. case FilterControllerName:
  287. valueToCompare = a.Properties.Controller
  288. case FilterPod:
  289. valueToCompare = a.Properties.Pod
  290. case FilterContainer:
  291. valueToCompare = a.Properties.Container
  292. // Comes from GetAnnotation/LabelFilterFunc in KCM
  293. case FilterLabel:
  294. val, ok := a.Properties.Labels[filter.Key]
  295. if !ok {
  296. toCompareMissing = true
  297. } else {
  298. valueToCompare = val
  299. }
  300. case FilterAnnotation:
  301. val, ok := a.Properties.Annotations[filter.Key]
  302. if !ok {
  303. toCompareMissing = true
  304. } else {
  305. valueToCompare = val
  306. }
  307. case FilterServices:
  308. valueToCompare = a.Properties.Services
  309. default:
  310. log.Errorf("Allocation Filter: Unhandled filter field. This is a filter implementation error and requires immediate patching. Field: %s", filter.Field)
  311. return false
  312. }
  313. switch filter.Op {
  314. case FilterEquals:
  315. // namespace:"__unallocated__" should match a.Properties.Namespace = ""
  316. // label[app]:"__unallocated__" should match _, ok := Labels[app]; !ok
  317. if toCompareMissing || valueToCompare == "" {
  318. return filter.Value == UnallocatedSuffix
  319. }
  320. if valueToCompare == filter.Value {
  321. return true
  322. }
  323. case FilterNotEquals:
  324. // namespace!:"__unallocated__" should match
  325. // a.Properties.Namespace != ""
  326. // label[app]!:"__unallocated__" should match _, ok := Labels[app]; ok
  327. if filter.Value == UnallocatedSuffix {
  328. if toCompareMissing {
  329. return false
  330. }
  331. return valueToCompare != ""
  332. }
  333. if toCompareMissing {
  334. return true
  335. }
  336. if valueToCompare != filter.Value {
  337. return true
  338. }
  339. case FilterContains:
  340. if stringSlice, ok := valueToCompare.([]string); ok {
  341. if len(stringSlice) == 0 {
  342. return filter.Value == UnallocatedSuffix
  343. }
  344. for _, s := range stringSlice {
  345. if s == filter.Value {
  346. return true
  347. }
  348. }
  349. } else {
  350. log.Warnf("Allocation Filter: invalid 'contains' call for non-list filter value")
  351. }
  352. case FilterNotContains:
  353. if stringSlice, ok := valueToCompare.([]string); ok {
  354. // services!:"__unallocated__" should match
  355. // len(a.Properties.Services) > 0
  356. //
  357. // TODO: is this true?
  358. if filter.Value == UnallocatedSuffix {
  359. return len(stringSlice) > 0
  360. }
  361. for _, s := range stringSlice {
  362. if s == filter.Value {
  363. return false
  364. }
  365. }
  366. return true
  367. } else {
  368. log.Warnf("Allocation Filter: invalid 'notcontains' call for non-list filter value")
  369. }
  370. case FilterStartsWith:
  371. if toCompareMissing {
  372. return false
  373. }
  374. // We don't need special __unallocated__ logic here because a query
  375. // asking for "__unallocated__" won't have a wildcard and unallocated
  376. // properties are the empty string.
  377. s, ok := valueToCompare.(string)
  378. if !ok {
  379. log.Warnf("Allocation Filter: invalid 'startswith' call for field with unsupported type")
  380. return false
  381. }
  382. return strings.HasPrefix(s, filter.Value)
  383. case FilterContainsPrefix:
  384. if toCompareMissing {
  385. return false
  386. }
  387. // We don't need special __unallocated__ logic here because a query
  388. // asking for "__unallocated__" won't have a wildcard and unallocated
  389. // properties are the empty string.
  390. values, ok := valueToCompare.([]string)
  391. if !ok {
  392. log.Warnf("Allocation Filter: invalid '%s' call for field with unsupported type", FilterContainsPrefix)
  393. return false
  394. }
  395. for _, s := range values {
  396. if strings.HasPrefix(s, filter.Value) {
  397. return true
  398. }
  399. }
  400. return false
  401. default:
  402. log.Errorf("Allocation Filter: Unhandled filter op. This is a filter implementation error and requires immediate patching. Op: %s", filter.Op)
  403. return false
  404. }
  405. return false
  406. }
  407. func (and AllocationFilterAnd) Matches(a *Allocation) bool {
  408. filters := and.Filters
  409. if len(filters) == 0 {
  410. return true
  411. }
  412. for _, filter := range filters {
  413. if !filter.Matches(a) {
  414. return false
  415. }
  416. }
  417. return true
  418. }
  419. func (or AllocationFilterOr) Matches(a *Allocation) bool {
  420. filters := or.Filters
  421. if len(filters) == 0 {
  422. return true
  423. }
  424. for _, filter := range filters {
  425. if filter.Matches(a) {
  426. return true
  427. }
  428. }
  429. return false
  430. }
  431. // AllocationFilterNone is a filter that matches no allocations. This is useful
  432. // for applications like authorization, where a user/group/role may be disallowed
  433. // from viewing Allocation data entirely.
  434. type AllocationFilterNone struct{}
  435. func (afn AllocationFilterNone) String() string { return "(none)" }
  436. func (afn AllocationFilterNone) Flattened() AllocationFilter { return afn }
  437. func (afn AllocationFilterNone) Matches(a *Allocation) bool { return false }
  438. func (left AllocationFilterNone) Equals(right AllocationFilter) bool {
  439. _, ok := right.(AllocationFilterNone)
  440. return ok
  441. }