allocationmatcher.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. package opencost
  2. import (
  3. "fmt"
  4. afilter "github.com/opencost/opencost/core/pkg/filter/allocation"
  5. "github.com/opencost/opencost/core/pkg/filter/ast"
  6. "github.com/opencost/opencost/core/pkg/filter/matcher"
  7. "github.com/opencost/opencost/core/pkg/filter/ops"
  8. "github.com/opencost/opencost/core/pkg/filter/transform"
  9. )
  10. // AllocationMatcher is a matcher implementation for Allocation instances,
  11. // compiled using the matcher.MatchCompiler for allocations.
  12. type AllocationMatcher matcher.Matcher[*Allocation]
  13. // NewAllocationMatchCompiler creates a new instance of a
  14. // matcher.MatchCompiler[*Allocation] which can be used to compile filter.Filter
  15. // ASTs into matcher.Matcher[*Allocation] implementations.
  16. //
  17. // If the label config is nil, the compiler will fail to compile alias filters
  18. // if any are present in the AST.
  19. //
  20. // If storage interfaces every support querying natively by alias (e.g. if a
  21. // data store contained a "product" attribute on an Allocation row), that should
  22. // be handled by a purpose-built AST compiler.
  23. func NewAllocationMatchCompiler(labelConfig *LabelConfig) *matcher.MatchCompiler[*Allocation] {
  24. passes := []transform.CompilerPass{}
  25. // The label config pass should be the first pass
  26. if labelConfig != nil {
  27. passes = append(passes, NewAllocationAliasPass(*labelConfig))
  28. }
  29. passes = append(passes,
  30. transform.PrometheusKeySanitizePass(),
  31. transform.UnallocatedReplacementPass(),
  32. )
  33. return matcher.NewMatchCompiler(
  34. allocationFieldMap,
  35. allocationSliceFieldMap,
  36. allocationMapFieldMap,
  37. passes...,
  38. )
  39. }
  40. // Maps fields from an allocation to a string value based on an identifier
  41. func allocationFieldMap(a *Allocation, identifier ast.Identifier) (string, error) {
  42. if a == nil {
  43. return "", fmt.Errorf("cannot map to nil allocation")
  44. }
  45. if a.Properties == nil {
  46. return "", fmt.Errorf("cannot map to nil properties")
  47. }
  48. if identifier.Field == nil {
  49. return "", fmt.Errorf("cannot map field from identifier with nil field")
  50. }
  51. switch afilter.AllocationField(identifier.Field.Name) {
  52. case afilter.FieldNamespace:
  53. return a.Properties.Namespace, nil
  54. case afilter.FieldNode:
  55. return a.Properties.Node, nil
  56. case afilter.FieldClusterID:
  57. return a.Properties.Cluster, nil
  58. case afilter.FieldControllerName:
  59. return a.Properties.Controller, nil
  60. case afilter.FieldControllerKind:
  61. return a.Properties.ControllerKind, nil
  62. case afilter.FieldPod:
  63. return a.Properties.Pod, nil
  64. case afilter.FieldContainer:
  65. return a.Properties.Container, nil
  66. case afilter.FieldProvider:
  67. return a.Properties.ProviderID, nil
  68. case afilter.FieldLabel:
  69. return a.Properties.Labels[identifier.Key], nil
  70. case afilter.FieldAnnotation:
  71. return a.Properties.Annotations[identifier.Key], nil
  72. }
  73. return "", fmt.Errorf("Failed to find string identifier on Allocation: %s", identifier.Field.Name)
  74. }
  75. // Maps slice fields from an allocation to a []string value based on an identifier
  76. func allocationSliceFieldMap(a *Allocation, identifier ast.Identifier) ([]string, error) {
  77. switch afilter.AllocationField(identifier.Field.Name) {
  78. case afilter.FieldServices:
  79. return a.Properties.Services, nil
  80. }
  81. return nil, fmt.Errorf("Failed to find []string identifier on Allocation: %s", identifier.Field.Name)
  82. }
  83. // Maps map fields from an allocation to a map[string]string value based on an identifier
  84. func allocationMapFieldMap(a *Allocation, identifier ast.Identifier) (map[string]string, error) {
  85. switch afilter.AllocationField(identifier.Field.Name) {
  86. case afilter.FieldLabel:
  87. return a.Properties.Labels, nil
  88. case afilter.FieldAnnotation:
  89. return a.Properties.Annotations, nil
  90. }
  91. return nil, fmt.Errorf("Failed to find map[string]string identifier on Allocation: %s", identifier.Field.Name)
  92. }
  93. // allocatioAliasPass implements the transform.CompilerPass interface, providing
  94. // a pass which converts alias nodes to logically-equivalent label/annotation
  95. // filter nodes based on the label config.
  96. type allocationAliasPass struct {
  97. Config LabelConfig
  98. AliasNameToAliasKey map[afilter.AllocationAlias]string
  99. }
  100. // NewAliasPass creates a compiler pass that converts alias nodes to
  101. // logically-equivalent label/annotation nodes based on the label config.
  102. //
  103. // Due to the special alias logic that combines label and annotation behavior
  104. // when filtering on alias, an alias filter is logically equivalent to the
  105. // following expression:
  106. //
  107. // (or
  108. //
  109. // (and (contains labels <parseraliaskey>)
  110. // (<op> labels[<parseraliaskey>] <filtervalue>))
  111. // (and (not (contains labels <parseraliaskey>))
  112. // (and (contains annotations departmentkey)
  113. // (<op> annotations[<parseraliaskey>] <filtervalue>))))
  114. func NewAllocationAliasPass(config LabelConfig) transform.CompilerPass {
  115. aliasNameToAliasKey := map[afilter.AllocationAlias]string{
  116. afilter.AliasDepartment: config.DepartmentLabel,
  117. afilter.AliasEnvironment: config.EnvironmentLabel,
  118. afilter.AliasOwner: config.OwnerLabel,
  119. afilter.AliasProduct: config.ProductLabel,
  120. afilter.AliasTeam: config.TeamLabel,
  121. }
  122. return &allocationAliasPass{
  123. Config: config,
  124. AliasNameToAliasKey: aliasNameToAliasKey,
  125. }
  126. }
  127. // Exec implements the transform.CompilerPass interface for an alias pass.
  128. // See aliasPass struct documentation for an explanation.
  129. func (p *allocationAliasPass) Exec(filter ast.FilterNode) (ast.FilterNode, error) {
  130. if p.AliasNameToAliasKey == nil {
  131. return nil, fmt.Errorf("cannot perform alias conversion with nil mapping of alias name -> key")
  132. }
  133. var transformErr error
  134. leafTransformerFunc := func(node ast.FilterNode) ast.FilterNode {
  135. if transformErr != nil {
  136. return node
  137. }
  138. var field *ast.Field
  139. var filterValue string
  140. var filterOp ast.FilterOp
  141. switch concrete := node.(type) {
  142. // These ops are not alias ops, alias ops can only be base-level ops
  143. // like =, !=, etc. No modification required here.
  144. case *ast.AndOp, *ast.OrOp, *ast.NotOp, *ast.VoidOp, *ast.ContradictionOp:
  145. return node
  146. case *ast.EqualOp:
  147. field = concrete.Left.Field
  148. filterValue = concrete.Right
  149. filterOp = ast.FilterOpEquals
  150. case *ast.ContainsOp:
  151. field = concrete.Left.Field
  152. filterValue = concrete.Right
  153. filterOp = ast.FilterOpContains
  154. case *ast.ContainsPrefixOp:
  155. field = concrete.Left.Field
  156. filterValue = concrete.Right
  157. filterOp = ast.FilterOpContainsPrefix
  158. case *ast.ContainsSuffixOp:
  159. field = concrete.Left.Field
  160. filterValue = concrete.Right
  161. filterOp = ast.FilterOpContainsSuffix
  162. default:
  163. transformErr = fmt.Errorf("unknown op '%s' during alias pass", concrete.Op())
  164. return node
  165. }
  166. if field == nil {
  167. return node
  168. }
  169. if !field.IsAlias() {
  170. return node
  171. }
  172. filterFieldAlias := afilter.AllocationAlias(field.Name)
  173. parserAliasKey, ok := p.AliasNameToAliasKey[filterFieldAlias]
  174. if !ok {
  175. transformErr = fmt.Errorf("unknown alias field '%s'", filterFieldAlias)
  176. return node
  177. }
  178. newFilter, err := convertAliasFilterToLabelAnnotationFilter(parserAliasKey, filterValue, filterOp)
  179. if err != nil {
  180. transformErr = fmt.Errorf("performing alias conversion for node '%+v': %w", node, err)
  181. return node
  182. }
  183. return newFilter
  184. }
  185. newFilter := ast.TransformLeaves(filter, leafTransformerFunc)
  186. if transformErr != nil {
  187. return nil, fmt.Errorf("alias pass transform: %w", transformErr)
  188. }
  189. return newFilter, nil
  190. }
  191. // convertAliasFilterToLabelAnnotationFilter constructs a new filter node using
  192. // only operations on labels and annotations that is logically equivalent to an
  193. // alias node from relevant data extracted from the original alias node.
  194. func convertAliasFilterToLabelAnnotationFilter(aliasKey string, filterValue string, op ast.FilterOp) (ast.FilterNode, error) {
  195. labelKey := ops.WithKey(afilter.FieldLabel, aliasKey)
  196. annotationKey := ops.WithKey(afilter.FieldAnnotation, aliasKey)
  197. var labelOp ast.FilterNode
  198. var annotationOp ast.FilterNode
  199. // This should only need to implement conversion for base-level ops like
  200. // equals, contains, etc.
  201. switch op {
  202. case ast.FilterOpEquals:
  203. labelOp = ops.Eq(labelKey, filterValue)
  204. annotationOp = ops.Eq(annotationKey, filterValue)
  205. case ast.FilterOpContains:
  206. labelOp = ops.Contains(labelKey, filterValue)
  207. annotationOp = ops.Contains(annotationKey, filterValue)
  208. case ast.FilterOpContainsPrefix:
  209. labelOp = ops.ContainsPrefix(labelKey, filterValue)
  210. annotationOp = ops.ContainsPrefix(annotationKey, filterValue)
  211. case ast.FilterOpContainsSuffix:
  212. labelOp = ops.ContainsSuffix(labelKey, filterValue)
  213. annotationOp = ops.ContainsSuffix(annotationKey, filterValue)
  214. default:
  215. return nil, fmt.Errorf("unsupported op type '%s' for alias conversion", op)
  216. }
  217. // This handles the case where a label EXISTS/IS PRESENT for (is extant)
  218. // for an aliased field. That's the primary case.
  219. extantCaseNode := ops.Or(
  220. ops.And(
  221. ops.Contains(afilter.FieldLabel, aliasKey),
  222. labelOp,
  223. ),
  224. ops.And(
  225. ops.Not(ops.Contains(afilter.FieldLabel, aliasKey)),
  226. ops.And(
  227. ops.Contains(afilter.FieldAnnotation, aliasKey),
  228. annotationOp,
  229. ),
  230. ),
  231. )
  232. var node ast.FilterNode
  233. // This handles the special case of unallocated aliased value. There's
  234. // two forms of this; first is where the label/annotation exists, but
  235. // has an empty string value. That's actually handled by the extant case,
  236. // because the API passes through that empty string. The other is when
  237. // the aliased label/annotation doesn't exist for an allocation. That's
  238. // what this modification to the tree handles. This matters when you're
  239. // trying to drill into/identify workloads "not allocated" within that
  240. // specific aliased field.
  241. if filterValue == "" || filterValue == UnallocatedSuffix {
  242. node = ops.Or(
  243. extantCaseNode,
  244. ops.And(
  245. ops.Not(ops.Contains(afilter.FieldLabel, aliasKey)),
  246. ops.Not(ops.Contains(afilter.FieldAnnotation, aliasKey)),
  247. ),
  248. )
  249. } else {
  250. node = extantCaseNode
  251. }
  252. return node, nil
  253. }