cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.60.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/parser/parser.go

587lines · modecode

1package parser
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io"
8 "strings"
9
10 "gopkg.in/yaml.v3"
11
12 "github.com/prometheus/common/model"
13
14 "github.com/cloudflare/pint/internal/comments"
15)
16
17const (
18 recordKey = "record"
19 exprKey = "expr"
20 labelsKey = "labels"
21 alertKey = "alert"
22 forKey = "for"
23 keepFiringForKey = "keep_firing_for"
24 annotationsKey = "annotations"
25)
26
27var ErrRuleCommentOnFile = errors.New("this comment is only valid when attached to a rule")
28
29func NewParser() Parser {
30 return Parser{}
31}
32
33type Parser struct{}
34
35func (p Parser) Parse(content []byte) (rules []Rule, err error) {
36 if len(content) == 0 {
37 return nil, nil
38 }
39
40 defer func() {
41 if r := recover(); r != nil {
42 err = fmt.Errorf("unable to parse YAML file: %s", r)
43 }
44 }()
45
46 var documents []yaml.Node
47 dec := yaml.NewDecoder(bytes.NewReader(content))
48 for {
49 var doc yaml.Node
50 decodeErr := dec.Decode(&doc)
51 if errors.Is(decodeErr, io.EOF) {
52 break
53 }
54 if decodeErr != nil {
55 return nil, decodeErr
56 }
57 documents = append(documents, doc)
58 }
59
60 for _, doc := range documents {
61 rules = append(rules, parseNode(content, &doc, 0)...)
62 }
63 return rules, err
64}
65
66func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule) {
67 ret, isEmpty := parseRule(content, node, offset)
68 if !isEmpty {
69 rules = append(rules, ret)
70 return rules
71 }
72
73 var rule Rule
74 for _, root := range node.Content {
75 // nolint: exhaustive
76 switch root.Kind {
77 case yaml.SequenceNode:
78 for _, n := range root.Content {
79 rules = append(rules, parseNode(content, n, offset)...)
80 }
81 case yaml.MappingNode:
82 rule, isEmpty = parseRule(content, root, offset)
83 if !isEmpty {
84 rules = append(rules, rule)
85 } else {
86 for _, n := range root.Content {
87 rules = append(rules, parseNode(content, n, offset)...)
88 }
89 }
90 case yaml.ScalarNode:
91 if root.Value != string(content) {
92 c := []byte(root.Value)
93 var n yaml.Node
94 if err := yaml.Unmarshal(c, &n); err == nil {
95 rules = append(rules, parseNode(c, &n, offset+root.Line)...)
96 }
97 }
98 }
99 }
100 return rules
101}
102
103func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool) {
104 if node.Kind != yaml.MappingNode {
105 return rule, true
106 }
107
108 var recordPart *YamlNode
109 var exprPart *PromQLExpr
110 var labelsPart *YamlMap
111
112 var alertPart *YamlNode
113 var forPart *YamlNode
114 var keepFiringForPart *YamlNode
115 var annotationsPart *YamlMap
116
117 var recordNode *yaml.Node
118 var alertNode *yaml.Node
119 var exprNode *yaml.Node
120 var forNode *yaml.Node
121 var keepFiringForNode *yaml.Node
122 var labelsNode *yaml.Node
123 var annotationsNode *yaml.Node
124 labelsNodes := map[*yaml.Node]*yaml.Node{}
125 annotationsNodes := map[*yaml.Node]*yaml.Node{}
126
127 var key *yaml.Node
128 unknownKeys := []*yaml.Node{}
129
130 var lines LineRange
131
132 var ruleComments []comments.Comment
133
134 for i, part := range unpackNodes(node) {
135 if lines.First == 0 || part.Line+offset < lines.First {
136 lines.First = part.Line + offset
137 }
138 lines.Last = max(lines.Last, part.Line+offset)
139
140 if i == 0 && node.HeadComment != "" && part.HeadComment == "" {
141 part.HeadComment = node.HeadComment
142 }
143 if i == 0 && node.LineComment != "" && part.LineComment == "" {
144 part.LineComment = node.LineComment
145 }
146 if i == len(node.Content)-1 && node.FootComment != "" && part.HeadComment == "" {
147 part.FootComment = node.FootComment
148 }
149 for _, s := range mergeComments(part) {
150 for _, c := range comments.Parse(part.Line, s) {
151 if comments.IsRuleComment(c.Type) {
152 ruleComments = append(ruleComments, c)
153 }
154 }
155 }
156
157 if i%2 == 0 {
158 key = part
159 } else {
160 switch key.Value {
161 case recordKey:
162 if recordPart != nil {
163 return duplicatedKeyError(lines, part.Line+offset, recordKey)
164 }
165 recordNode = part
166 recordPart = newYamlNodeWithKey(key, part, offset)
167 lines.Last = max(lines.Last, recordPart.Lines.Last)
168 case alertKey:
169 if alertPart != nil {
170 return duplicatedKeyError(lines, part.Line+offset, alertKey)
171 }
172 alertNode = part
173 alertPart = newYamlNodeWithKey(key, part, offset)
174 lines.Last = max(lines.Last, alertPart.Lines.Last)
175 case exprKey:
176 if exprPart != nil {
177 return duplicatedKeyError(lines, part.Line+offset, exprKey)
178 }
179 exprNode = part
180 exprPart = newPromQLExpr(key, part, offset)
181 lines.Last = max(lines.Last, exprPart.Value.Lines.Last)
182 case forKey:
183 if forPart != nil {
184 return duplicatedKeyError(lines, part.Line+offset, forKey)
185 }
186 forNode = part
187 forPart = newYamlNodeWithKey(key, part, offset)
188 lines.Last = max(lines.Last, forPart.Lines.Last)
189 case keepFiringForKey:
190 if keepFiringForPart != nil {
191 return duplicatedKeyError(lines, part.Line+offset, keepFiringForKey)
192 }
193 keepFiringForNode = part
194 keepFiringForPart = newYamlNodeWithKey(key, part, offset)
195 lines.Last = max(lines.Last, keepFiringForPart.Lines.Last)
196 case labelsKey:
197 if labelsPart != nil {
198 return duplicatedKeyError(lines, part.Line+offset, labelsKey)
199 }
200 labelsNode = part
201 labelsNodes = mappingNodes(part)
202 labelsPart = newYamlMap(key, part, offset)
203 lines.Last = max(lines.Last, labelsPart.Lines.Last)
204 case annotationsKey:
205 if annotationsPart != nil {
206 return duplicatedKeyError(lines, part.Line+offset, annotationsKey)
207 }
208 annotationsNode = part
209 annotationsNodes = mappingNodes(part)
210 annotationsPart = newYamlMap(key, part, offset)
211 lines.Last = max(lines.Last, annotationsPart.Lines.Last)
212 default:
213 unknownKeys = append(unknownKeys, key)
214 }
215 }
216 }
217
218 if exprPart != nil && exprPart.Value.Lines.First != exprPart.Value.Lines.Last {
219 contentLines := strings.Split(string(content), "\n")
220 for {
221 start := exprPart.Value.Lines.First
222 end := exprPart.Value.Lines.Last
223 if end > len(contentLines) {
224 end--
225 }
226 input := strings.Join(contentLines[start:end], "")
227 input = strings.ReplaceAll(input, " ", "")
228 output := strings.ReplaceAll(exprPart.Value.Value, "\n", "")
229 output = strings.ReplaceAll(output, " ", "")
230 if end >= len(contentLines) {
231 break
232 }
233 if input == output {
234 break
235 }
236 exprPart.Value.Lines.Last = end + 1
237 }
238 }
239
240 if recordPart != nil && alertPart != nil {
241 rule = Rule{
242 Lines: lines,
243 Error: ParseError{
244 Line: node.Line + offset,
245 Err: fmt.Errorf("got both %s and %s keys in a single rule", recordKey, alertKey),
246 },
247 }
248 return rule, false
249 }
250 if exprPart != nil && alertPart == nil && recordPart == nil {
251 rule = Rule{
252 Lines: lines,
253 Error: ParseError{
254 Line: exprPart.Value.Lines.Last,
255 Err: fmt.Errorf("incomplete rule, no %s or %s key", alertKey, recordKey),
256 },
257 }
258 return rule, false
259 }
260 if recordPart != nil && forPart != nil {
261 rule = Rule{
262 Lines: lines,
263 Error: ParseError{
264 Line: forPart.Lines.First,
265 Err: fmt.Errorf("invalid field '%s' in recording rule", forKey),
266 },
267 }
268 return rule, false
269 }
270 if recordPart != nil && keepFiringForPart != nil {
271 rule = Rule{
272 Lines: lines,
273 Error: ParseError{
274 Line: keepFiringForPart.Lines.First,
275 Err: fmt.Errorf("invalid field '%s' in recording rule", keepFiringForKey),
276 },
277 }
278 return rule, false
279 }
280 if recordPart != nil && annotationsPart != nil {
281 rule = Rule{
282 Lines: lines,
283 Error: ParseError{
284 Line: annotationsPart.Lines.First,
285 Err: fmt.Errorf("invalid field '%s' in recording rule", annotationsKey),
286 },
287 }
288 return rule, false
289 }
290 for key, part := range map[string]*yaml.Node{
291 recordKey: recordNode,
292 alertKey: alertNode,
293 exprKey: exprNode,
294 forKey: forNode,
295 keepFiringForKey: keepFiringForNode,
296 } {
297 if part != nil && !isTag(part.ShortTag(), "!!str") {
298 return invalidValueError(lines, part.Line+offset, key, "string", describeTag(part.ShortTag()))
299 }
300 }
301
302 for key, part := range map[string]*yaml.Node{
303 labelsKey: labelsNode,
304 annotationsKey: annotationsNode,
305 } {
306 if part != nil && !isTag(part.ShortTag(), "!!map") {
307 return invalidValueError(lines, part.Line+offset, key, "mapping", describeTag(part.ShortTag()))
308 }
309 }
310
311 for section, parts := range map[string]map[*yaml.Node]*yaml.Node{
312 labelsKey: labelsNodes,
313 annotationsKey: annotationsNodes,
314 } {
315 for key, value := range parts {
316 if !isTag(value.ShortTag(), "!!str") {
317 return invalidValueError(lines, value.Line+offset, fmt.Sprintf("%s %s", section, nodeValue(key)), "string", describeTag(value.ShortTag()))
318 }
319 }
320 }
321
322 if r, ok := ensureRequiredKeys(lines, recordKey, recordPart, exprPart); !ok {
323 return r, false
324 }
325 if r, ok := ensureRequiredKeys(lines, alertKey, alertPart, exprPart); !ok {
326 return r, false
327 }
328 if (recordPart != nil || alertPart != nil) && len(unknownKeys) > 0 {
329 var keys []string
330 for _, n := range unknownKeys {
331 keys = append(keys, n.Value)
332 }
333 rule = Rule{
334 Lines: lines,
335 Error: ParseError{
336 Line: unknownKeys[0].Line + offset,
337 Err: fmt.Errorf("invalid key(s) found: %s", strings.Join(keys, ", ")),
338 },
339 }
340 return rule, false
341 }
342
343 if recordPart != nil && !model.IsValidMetricName(model.LabelValue(recordPart.Value)) {
344 return Rule{
345 Lines: lines,
346 Error: ParseError{
347 Line: recordPart.Lines.First,
348 Err: fmt.Errorf("invalid recording rule name: %s", recordPart.Value),
349 },
350 }, false
351 }
352
353 if (recordPart != nil || alertPart != nil) && labelsPart != nil {
354 for _, lab := range labelsPart.Items {
355 if !model.LabelName(lab.Key.Value).IsValid() || lab.Key.Value == model.MetricNameLabel {
356 return Rule{
357 Lines: lines,
358 Error: ParseError{
359 Line: lab.Key.Lines.First,
360 Err: fmt.Errorf("invalid label name: %s", lab.Key.Value),
361 },
362 }, false
363 }
364 if !model.LabelValue(lab.Value.Value).IsValid() {
365 return Rule{
366 Lines: lines,
367 Error: ParseError{
368 Line: lab.Key.Lines.First,
369 Err: fmt.Errorf("invalid label value: %s", lab.Value.Value),
370 },
371 }, false
372 }
373 }
374 }
375
376 if alertPart != nil && annotationsPart != nil {
377 for _, ann := range annotationsPart.Items {
378 if !model.LabelName(ann.Key.Value).IsValid() {
379 return Rule{
380 Lines: lines,
381 Error: ParseError{
382 Line: ann.Key.Lines.First,
383 Err: fmt.Errorf("invalid annotation name: %s", ann.Key.Value),
384 },
385 }, false
386 }
387 }
388 }
389
390 if recordPart != nil && exprPart != nil {
391 rule = Rule{
392 Lines: lines,
393 RecordingRule: &RecordingRule{
394 Record: *recordPart,
395 Expr: *exprPart,
396 Labels: labelsPart,
397 },
398 Comments: ruleComments,
399 }
400 return rule, false
401 }
402
403 if alertPart != nil && exprPart != nil {
404 rule = Rule{
405 Lines: lines,
406 AlertingRule: &AlertingRule{
407 Alert: *alertPart,
408 Expr: *exprPart,
409 For: forPart,
410 KeepFiringFor: keepFiringForPart,
411 Labels: labelsPart,
412 Annotations: annotationsPart,
413 },
414 Comments: ruleComments,
415 }
416 return rule, false
417 }
418
419 return rule, true
420}
421
422func unpackNodes(node *yaml.Node) []*yaml.Node {
423 nodes := make([]*yaml.Node, 0, len(node.Content))
424 var isMerge bool
425 for _, part := range node.Content {
426 if part.ShortTag() == "!!merge" && part.Value == "<<" {
427 isMerge = true
428 }
429
430 if part.Alias != nil {
431 if isMerge {
432 nodes = append(nodes, resolveMapAlias(part, node).Content...)
433 } else {
434 nodes = append(nodes, resolveMapAlias(part, part))
435 }
436 isMerge = false
437 continue
438 }
439 if isMerge {
440 continue
441 }
442 nodes = append(nodes, part)
443 }
444 return nodes
445}
446
447func nodeKeys(node *yaml.Node) (keys []string) {
448 if node.Kind != yaml.MappingNode {
449 return keys
450 }
451 for i, n := range node.Content {
452 if i%2 == 0 && n.Value != "" {
453 keys = append(keys, n.Value)
454 }
455 }
456 return keys
457}
458
459func hasKey(node *yaml.Node, key string) bool {
460 for _, k := range nodeKeys(node) {
461 if k == key {
462 return true
463 }
464 }
465 return false
466}
467
468func hasValue(node *YamlNode) bool {
469 if node == nil {
470 return false
471 }
472 return node.Value != ""
473}
474
475func ensureRequiredKeys(lines LineRange, key string, keyVal *YamlNode, expr *PromQLExpr) (Rule, bool) {
476 if keyVal == nil {
477 return Rule{Lines: lines}, true
478 }
479 if !hasValue(keyVal) {
480 return Rule{
481 Lines: lines,
482 Error: ParseError{
483 Line: keyVal.Lines.Last,
484 Err: fmt.Errorf("%s value cannot be empty", key),
485 },
486 }, false
487 }
488 if expr == nil {
489 return Rule{
490 Lines: lines,
491 Error: ParseError{
492 Line: keyVal.Lines.Last,
493 Err: fmt.Errorf("missing %s key", exprKey),
494 },
495 }, false
496 }
497 if !hasValue(expr.Value) {
498 return Rule{
499 Lines: lines,
500 Error: ParseError{
501 Line: expr.Value.Lines.Last,
502 Err: fmt.Errorf("%s value cannot be empty", exprKey),
503 },
504 }, false
505 }
506 return Rule{Lines: lines}, true
507}
508
509func resolveMapAlias(part, parent *yaml.Node) *yaml.Node {
510 node := *part
511 node.Content = nil
512 var ok bool
513 for i, alias := range part.Alias.Content {
514 if i%2 == 0 {
515 ok = !hasKey(parent, alias.Value)
516 }
517 if ok {
518 node.Content = append(node.Content, alias)
519 }
520 if i%2 == 1 {
521 ok = false
522 }
523 }
524 return &node
525}
526
527func duplicatedKeyError(lines LineRange, line int, key string) (Rule, bool) {
528 rule := Rule{
529 Lines: lines,
530 Error: ParseError{
531 Line: line,
532 Err: fmt.Errorf("duplicated %s key", key),
533 },
534 }
535 return rule, false
536}
537
538func invalidValueError(lines LineRange, line int, key, expectedKind, gotKind string) (Rule, bool) {
539 rule := Rule{
540 Lines: lines,
541 Error: ParseError{
542 Line: line,
543 Err: fmt.Errorf("%s value must be a YAML %s, got %s instead", key, expectedKind, gotKind),
544 },
545 }
546 return rule, false
547}
548
549func isTag(tag, expected string) bool {
550 if tag == "!!null" {
551 return true
552 }
553 return tag == expected
554}
555
556func describeTag(tag string) string {
557 switch tag {
558 case "":
559 return "unknown type"
560 case "!!str":
561 return "string"
562 case "!!int":
563 return "integer"
564 case "!!seq":
565 return "list"
566 case "!!map":
567 return "mapping"
568 case "!!binary":
569 return "binary data"
570 default:
571 return strings.TrimLeft(tag, "!")
572 }
573}
574
575func mappingNodes(node *yaml.Node) map[*yaml.Node]*yaml.Node {
576 m := make(map[*yaml.Node]*yaml.Node, len(node.Content))
577 var key *yaml.Node
578 for _, child := range node.Content {
579 if key != nil {
580 m[key] = child
581 key = nil
582 } else {
583 key = child
584 }
585 }
586 return m
587}
588