cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.74.5

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

723lines · modecode

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