allocationmatcher.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. package kubecost
  2. import (
  3. "fmt"
  4. afilter "github.com/opencost/opencost/pkg/filter21/allocation"
  5. "github.com/opencost/opencost/pkg/filter21/ast"
  6. "github.com/opencost/opencost/pkg/filter21/matcher"
  7. "github.com/opencost/opencost/pkg/filter21/ops"
  8. "github.com/opencost/opencost/pkg/filter21/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. return ops.Or(
  218. ops.And(
  219. ops.Contains(afilter.FieldLabel, aliasKey),
  220. labelOp,
  221. ),
  222. ops.And(
  223. ops.Not(ops.Contains(afilter.FieldLabel, aliasKey)),
  224. ops.And(
  225. ops.Contains(afilter.FieldAnnotation, aliasKey),
  226. annotationOp,
  227. ),
  228. ),
  229. ), nil
  230. }