cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.73.5

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

712lines · modecode

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