cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
29ed4c2ccc1fb946d416df7635c9bae86ffca5b5

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

658lines · modecode

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