allocationmatcher.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 identifier.Field == nil {
  43. return "", fmt.Errorf("cannot map field from identifier with nil field")
  44. }
  45. switch afilter.AllocationField(identifier.Field.Name) {
  46. case afilter.FieldNamespace:
  47. return a.Properties.Namespace, nil
  48. case afilter.FieldNode:
  49. return a.Properties.Node, nil
  50. case afilter.FieldClusterID:
  51. return a.Properties.Cluster, nil
  52. case afilter.FieldControllerName:
  53. return a.Properties.Controller, nil
  54. case afilter.FieldControllerKind:
  55. return a.Properties.ControllerKind, nil
  56. case afilter.FieldPod:
  57. return a.Properties.Pod, nil
  58. case afilter.FieldContainer:
  59. return a.Properties.Container, nil
  60. case afilter.FieldProvider:
  61. return a.Properties.ProviderID, nil
  62. case afilter.FieldLabel:
  63. return a.Properties.Labels[identifier.Key], nil
  64. case afilter.FieldAnnotation:
  65. return a.Properties.Annotations[identifier.Key], nil
  66. }
  67. return "", fmt.Errorf("Failed to find string identifier on Allocation: %s", identifier.Field.Name)
  68. }
  69. // Maps slice fields from an allocation to a []string value based on an identifier
  70. func allocationSliceFieldMap(a *Allocation, identifier ast.Identifier) ([]string, error) {
  71. switch afilter.AllocationField(identifier.Field.Name) {
  72. case afilter.FieldServices:
  73. return a.Properties.Services, nil
  74. }
  75. return nil, fmt.Errorf("Failed to find []string identifier on Allocation: %s", identifier.Field.Name)
  76. }
  77. // Maps map fields from an allocation to a map[string]string value based on an identifier
  78. func allocationMapFieldMap(a *Allocation, identifier ast.Identifier) (map[string]string, error) {
  79. switch afilter.AllocationField(identifier.Field.Name) {
  80. case afilter.FieldLabel:
  81. return a.Properties.Labels, nil
  82. case afilter.FieldAnnotation:
  83. return a.Properties.Annotations, nil
  84. }
  85. return nil, fmt.Errorf("Failed to find map[string]string identifier on Allocation: %s", identifier.Field.Name)
  86. }
  87. // allocatioAliasPass implements the transform.CompilerPass interface, providing
  88. // a pass which converts alias nodes to logically-equivalent label/annotation
  89. // filter nodes based on the label config.
  90. type allocationAliasPass struct {
  91. Config LabelConfig
  92. AliasNameToAliasKey map[afilter.AllocationAlias]string
  93. }
  94. // NewAliasPass creates a compiler pass that converts alias nodes to
  95. // logically-equivalent label/annotation nodes based on the label config.
  96. //
  97. // Due to the special alias logic that combines label and annotation behavior
  98. // when filtering on alias, an alias filter is logically equivalent to the
  99. // following expression:
  100. //
  101. // (or
  102. //
  103. // (and (contains labels <parseraliaskey>)
  104. // (<op> labels[<parseraliaskey>] <filtervalue>))
  105. // (and (not (contains labels <parseraliaskey>))
  106. // (and (contains annotations departmentkey)
  107. // (<op> annotations[<parseraliaskey>] <filtervalue>))))
  108. func NewAllocationAliasPass(config LabelConfig) transform.CompilerPass {
  109. aliasNameToAliasKey := map[afilter.AllocationAlias]string{
  110. afilter.AliasDepartment: config.DepartmentLabel,
  111. afilter.AliasEnvironment: config.EnvironmentLabel,
  112. afilter.AliasOwner: config.OwnerLabel,
  113. afilter.AliasProduct: config.ProductLabel,
  114. afilter.AliasTeam: config.TeamLabel,
  115. }
  116. return &allocationAliasPass{
  117. Config: config,
  118. AliasNameToAliasKey: aliasNameToAliasKey,
  119. }
  120. }
  121. // Exec implements the transform.CompilerPass interface for an alias pass.
  122. // See aliasPass struct documentation for an explanation.
  123. func (p *allocationAliasPass) Exec(filter ast.FilterNode) (ast.FilterNode, error) {
  124. if p.AliasNameToAliasKey == nil {
  125. return nil, fmt.Errorf("cannot perform alias conversion with nil mapping of alias name -> key")
  126. }
  127. var transformErr error
  128. leafTransformerFunc := func(node ast.FilterNode) ast.FilterNode {
  129. if transformErr != nil {
  130. return node
  131. }
  132. var field *ast.Field
  133. var filterValue string
  134. var filterOp ast.FilterOp
  135. switch concrete := node.(type) {
  136. // These ops are not alias ops, alias ops can only be base-level ops
  137. // like =, !=, etc. No modification required here.
  138. case *ast.AndOp, *ast.OrOp, *ast.NotOp, *ast.VoidOp, *ast.ContradictionOp:
  139. return node
  140. case *ast.EqualOp:
  141. field = concrete.Left.Field
  142. filterValue = concrete.Right
  143. filterOp = ast.FilterOpEquals
  144. case *ast.ContainsOp:
  145. field = concrete.Left.Field
  146. filterValue = concrete.Right
  147. filterOp = ast.FilterOpContains
  148. case *ast.ContainsPrefixOp:
  149. field = concrete.Left.Field
  150. filterValue = concrete.Right
  151. filterOp = ast.FilterOpContainsPrefix
  152. case *ast.ContainsSuffixOp:
  153. field = concrete.Left.Field
  154. filterValue = concrete.Right
  155. filterOp = ast.FilterOpContainsSuffix
  156. default:
  157. transformErr = fmt.Errorf("unknown op '%s' during alias pass", concrete.Op())
  158. return node
  159. }
  160. if field == nil {
  161. return node
  162. }
  163. if !field.IsAlias() {
  164. return node
  165. }
  166. filterFieldAlias := afilter.AllocationAlias(field.Name)
  167. parserAliasKey, ok := p.AliasNameToAliasKey[filterFieldAlias]
  168. if !ok {
  169. transformErr = fmt.Errorf("unknown alias field '%s'", filterFieldAlias)
  170. return node
  171. }
  172. newFilter, err := convertAliasFilterToLabelAnnotationFilter(parserAliasKey, filterValue, filterOp)
  173. if err != nil {
  174. transformErr = fmt.Errorf("performing alias conversion for node '%+v': %w", node, err)
  175. return node
  176. }
  177. return newFilter
  178. }
  179. newFilter := ast.TransformLeaves(filter, leafTransformerFunc)
  180. if transformErr != nil {
  181. return nil, fmt.Errorf("alias pass transform: %w", transformErr)
  182. }
  183. return newFilter, nil
  184. }
  185. // convertAliasFilterToLabelAnnotationFilter constructs a new filter node using
  186. // only operations on labels and annotations that is logically equivalent to an
  187. // alias node from relevant data extracted from the original alias node.
  188. func convertAliasFilterToLabelAnnotationFilter(aliasKey string, filterValue string, op ast.FilterOp) (ast.FilterNode, error) {
  189. labelKey := ops.WithKey(afilter.FieldLabel, aliasKey)
  190. annotationKey := ops.WithKey(afilter.FieldAnnotation, aliasKey)
  191. var labelOp ast.FilterNode
  192. var annotationOp ast.FilterNode
  193. // This should only need to implement conversion for base-level ops like
  194. // equals, contains, etc.
  195. switch op {
  196. case ast.FilterOpEquals:
  197. labelOp = ops.Eq(labelKey, filterValue)
  198. annotationOp = ops.Eq(annotationKey, filterValue)
  199. case ast.FilterOpContains:
  200. labelOp = ops.Contains(labelKey, filterValue)
  201. annotationOp = ops.Contains(annotationKey, filterValue)
  202. case ast.FilterOpContainsPrefix:
  203. labelOp = ops.ContainsPrefix(labelKey, filterValue)
  204. annotationOp = ops.ContainsPrefix(annotationKey, filterValue)
  205. case ast.FilterOpContainsSuffix:
  206. labelOp = ops.ContainsSuffix(labelKey, filterValue)
  207. annotationOp = ops.ContainsSuffix(annotationKey, filterValue)
  208. default:
  209. return nil, fmt.Errorf("unsupported op type '%s' for alias conversion", op)
  210. }
  211. return ops.Or(
  212. ops.And(
  213. ops.Contains(afilter.FieldLabel, aliasKey),
  214. labelOp,
  215. ),
  216. ops.And(
  217. ops.Not(ops.Contains(afilter.FieldLabel, aliasKey)),
  218. ops.And(
  219. ops.Contains(afilter.FieldAnnotation, aliasKey),
  220. annotationOp,
  221. ),
  222. ),
  223. ), nil
  224. }