cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.74.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

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