allocationmatcher.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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.FieldAccount:
  69. return "", fmt.Errorf("account property not implemented")
  70. case afilter.FieldLabel:
  71. return a.Properties.Labels[identifier.Key], nil
  72. case afilter.FieldAnnotation:
  73. return a.Properties.Annotations[identifier.Key], nil
  74. }
  75. return "", fmt.Errorf("Failed to find string identifier on Allocation: %s", identifier.Field.Name)
  76. }
  77. // Maps slice fields from an allocation to a []string value based on an identifier
  78. func allocationSliceFieldMap(a *Allocation, identifier ast.Identifier) ([]string, error) {
  79. switch afilter.AllocationField(identifier.Field.Name) {
  80. case afilter.FieldServices:
  81. return a.Properties.Services, nil
  82. }
  83. return nil, fmt.Errorf("Failed to find []string identifier on Allocation: %s", identifier.Field.Name)
  84. }
  85. // Maps map fields from an allocation to a map[string]string value based on an identifier
  86. func allocationMapFieldMap(a *Allocation, identifier ast.Identifier) (map[string]string, error) {
  87. switch afilter.AllocationField(identifier.Field.Name) {
  88. case afilter.FieldLabel:
  89. return a.Properties.Labels, nil
  90. case afilter.FieldAnnotation:
  91. return a.Properties.Annotations, nil
  92. }
  93. return nil, fmt.Errorf("Failed to find map[string]string identifier on Allocation: %s", identifier.Field.Name)
  94. }
  95. // allocatioAliasPass implements the transform.CompilerPass interface, providing
  96. // a pass which converts alias nodes to logically-equivalent label/annotation
  97. // filter nodes based on the label config.
  98. type allocationAliasPass struct {
  99. Config LabelConfig
  100. AliasNameToAliasKey map[afilter.AllocationAlias]string
  101. }
  102. // NewAliasPass creates a compiler pass that converts alias nodes to
  103. // logically-equivalent label/annotation nodes based on the label config.
  104. //
  105. // Due to the special alias logic that combines label and annotation behavior
  106. // when filtering on alias, an alias filter is logically equivalent to the
  107. // following expression:
  108. //
  109. // (or
  110. //
  111. // (and (contains labels <parseraliaskey>)
  112. // (<op> labels[<parseraliaskey>] <filtervalue>))
  113. // (and (not (contains labels <parseraliaskey>))
  114. // (and (contains annotations departmentkey)
  115. // (<op> annotations[<parseraliaskey>] <filtervalue>))))
  116. func NewAllocationAliasPass(config LabelConfig) transform.CompilerPass {
  117. aliasNameToAliasKey := map[afilter.AllocationAlias]string{
  118. afilter.AliasDepartment: config.DepartmentLabel,
  119. afilter.AliasEnvironment: config.EnvironmentLabel,
  120. afilter.AliasOwner: config.OwnerLabel,
  121. afilter.AliasProduct: config.ProductLabel,
  122. afilter.AliasTeam: config.TeamLabel,
  123. }
  124. return &allocationAliasPass{
  125. Config: config,
  126. AliasNameToAliasKey: aliasNameToAliasKey,
  127. }
  128. }
  129. // Exec implements the transform.CompilerPass interface for an alias pass.
  130. // See aliasPass struct documentation for an explanation.
  131. func (p *allocationAliasPass) Exec(filter ast.FilterNode) (ast.FilterNode, error) {
  132. if p.AliasNameToAliasKey == nil {
  133. return nil, fmt.Errorf("cannot perform alias conversion with nil mapping of alias name -> key")
  134. }
  135. var transformErr error
  136. leafTransformerFunc := func(node ast.FilterNode) ast.FilterNode {
  137. if transformErr != nil {
  138. return node
  139. }
  140. var field *ast.Field
  141. var filterValue string
  142. var filterOp ast.FilterOp
  143. switch concrete := node.(type) {
  144. // These ops are not alias ops, alias ops can only be base-level ops
  145. // like =, !=, etc. No modification required here.
  146. case *ast.AndOp, *ast.OrOp, *ast.NotOp, *ast.VoidOp, *ast.ContradictionOp:
  147. return node
  148. case *ast.EqualOp:
  149. field = concrete.Left.Field
  150. filterValue = concrete.Right
  151. filterOp = ast.FilterOpEquals
  152. case *ast.ContainsOp:
  153. field = concrete.Left.Field
  154. filterValue = concrete.Right
  155. filterOp = ast.FilterOpContains
  156. case *ast.ContainsPrefixOp:
  157. field = concrete.Left.Field
  158. filterValue = concrete.Right
  159. filterOp = ast.FilterOpContainsPrefix
  160. case *ast.ContainsSuffixOp:
  161. field = concrete.Left.Field
  162. filterValue = concrete.Right
  163. filterOp = ast.FilterOpContainsSuffix
  164. default:
  165. transformErr = fmt.Errorf("unknown op '%s' during alias pass", concrete.Op())
  166. return node
  167. }
  168. if field == nil {
  169. return node
  170. }
  171. if !field.IsAlias() {
  172. return node
  173. }
  174. filterFieldAlias := afilter.AllocationAlias(field.Name)
  175. parserAliasKey, ok := p.AliasNameToAliasKey[filterFieldAlias]
  176. if !ok {
  177. transformErr = fmt.Errorf("unknown alias field '%s'", filterFieldAlias)
  178. return node
  179. }
  180. newFilter, err := convertAliasFilterToLabelAnnotationFilter(parserAliasKey, filterValue, filterOp)
  181. if err != nil {
  182. transformErr = fmt.Errorf("performing alias conversion for node '%+v': %w", node, err)
  183. return node
  184. }
  185. return newFilter
  186. }
  187. newFilter := ast.TransformLeaves(filter, leafTransformerFunc)
  188. if transformErr != nil {
  189. return nil, fmt.Errorf("alias pass transform: %w", transformErr)
  190. }
  191. return newFilter, nil
  192. }
  193. // convertAliasFilterToLabelAnnotationFilter constructs a new filter node using
  194. // only operations on labels and annotations that is logically equivalent to an
  195. // alias node from relevant data extracted from the original alias node.
  196. func convertAliasFilterToLabelAnnotationFilter(aliasKey string, filterValue string, op ast.FilterOp) (ast.FilterNode, error) {
  197. labelKey := ops.WithKey(afilter.FieldLabel, aliasKey)
  198. annotationKey := ops.WithKey(afilter.FieldAnnotation, aliasKey)
  199. var labelOp ast.FilterNode
  200. var annotationOp ast.FilterNode
  201. // This should only need to implement conversion for base-level ops like
  202. // equals, contains, etc.
  203. switch op {
  204. case ast.FilterOpEquals:
  205. labelOp = ops.Eq(labelKey, filterValue)
  206. annotationOp = ops.Eq(annotationKey, filterValue)
  207. case ast.FilterOpContains:
  208. labelOp = ops.Contains(labelKey, filterValue)
  209. annotationOp = ops.Contains(annotationKey, filterValue)
  210. case ast.FilterOpContainsPrefix:
  211. labelOp = ops.ContainsPrefix(labelKey, filterValue)
  212. annotationOp = ops.ContainsPrefix(annotationKey, filterValue)
  213. case ast.FilterOpContainsSuffix:
  214. labelOp = ops.ContainsSuffix(labelKey, filterValue)
  215. annotationOp = ops.ContainsSuffix(annotationKey, filterValue)
  216. default:
  217. return nil, fmt.Errorf("unsupported op type '%s' for alias conversion", op)
  218. }
  219. // This handles the case where a label EXISTS/IS PRESENT for (is extant)
  220. // for an aliased field. That's the primary case.
  221. extantCaseNode := ops.Or(
  222. ops.And(
  223. ops.Contains(afilter.FieldLabel, aliasKey),
  224. labelOp,
  225. ),
  226. ops.And(
  227. ops.Not(ops.Contains(afilter.FieldLabel, aliasKey)),
  228. ops.And(
  229. ops.Contains(afilter.FieldAnnotation, aliasKey),
  230. annotationOp,
  231. ),
  232. ),
  233. )
  234. var node ast.FilterNode
  235. // This handles the special case of unallocated aliased value. There's
  236. // two forms of this; first is where the label/annotation exists, but
  237. // has an empty string value. That's actually handled by the extant case,
  238. // because the API passes through that empty string. The other is when
  239. // the aliased label/annotation doesn't exist for an allocation. That's
  240. // what this modification to the tree handles. This matters when you're
  241. // trying to drill into/identify workloads "not allocated" within that
  242. // specific aliased field.
  243. if filterValue == "" || filterValue == UnallocatedSuffix {
  244. node = ops.Or(
  245. extantCaseNode,
  246. ops.And(
  247. ops.Not(ops.Contains(afilter.FieldLabel, aliasKey)),
  248. ops.Not(ops.Contains(afilter.FieldAnnotation, aliasKey)),
  249. ),
  250. )
  251. } else {
  252. node = extantCaseNode
  253. }
  254. return node, nil
  255. }