cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.73.7

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

714lines · 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 // FIXME Use IsValidLegacyMetricName() & LabelName.IsValidLegacy() instead
41 // https://pkg.go.dev/github.com/prometheus/common@v0.63.0/model#NameValidationScheme
42 model.NameValidationScheme = names // nolint: staticcheck
43 return Parser{
44 isStrict: isStrict,
45 schema: schema,
46 }
47}
48
49type Parser struct {
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 = parseGroups(&doc, p.schema, 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 := 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 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 && !model.IsValidMetricName(model.LabelValue(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 !model.LabelName(lab.Key.Value).IsValid() || 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 !model.LabelName(ann.Key.Value).IsValid() {
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 unpackNodes(node *yaml.Node) []*yaml.Node {
502 nodes := make([]*yaml.Node, 0, len(node.Content))
503 var isMerge bool
504 for _, part := range node.Content {
505 if part.ShortTag() == mergeTag && part.Value == "<<" {
506 isMerge = true
507 }
508
509 if part.Alias != nil {
510 if isMerge {
511 nodes = append(nodes, resolveMapAlias(part, node).Content...)
512 } else {
513 nodes = append(nodes, resolveMapAlias(part, part))
514 }
515 isMerge = false
516 continue
517 }
518 if isMerge {
519 continue
520 }
521 nodes = append(nodes, part)
522 }
523 return nodes
524}
525
526func nodeKeys(node *yaml.Node) (keys []string) {
527 if node.Kind != yaml.MappingNode {
528 return keys
529 }
530 for i, n := range node.Content {
531 if i%2 == 0 && n.Value != "" {
532 keys = append(keys, n.Value)
533 }
534 }
535 return keys
536}
537
538func hasKey(node *yaml.Node, key string) bool {
539 for _, k := range nodeKeys(node) {
540 if k == key {
541 return true
542 }
543 }
544 return false
545}
546
547func hasValue(node *YamlNode) bool {
548 if node == nil {
549 return false
550 }
551 return node.Value != ""
552}
553
554func ensureRequiredKeys(lines diags.LineRange, key string, keyVal *YamlNode, expr *PromQLExpr) (Rule, bool) {
555 if keyVal == nil {
556 return Rule{Lines: lines}, true
557 }
558 if !hasValue(keyVal) {
559 return Rule{
560 Lines: lines,
561 Error: ParseError{
562 Line: keyVal.Pos.Lines().Last,
563 Err: fmt.Errorf("%s value cannot be empty", key),
564 },
565 }, false
566 }
567 if expr == nil {
568 return Rule{
569 Lines: lines,
570 Error: ParseError{
571 Line: keyVal.Pos.Lines().Last,
572 Err: fmt.Errorf("missing %s key", exprKey),
573 },
574 }, false
575 }
576 if !hasValue(expr.Value) {
577 return Rule{
578 Lines: lines,
579 Error: ParseError{
580 Line: expr.Value.Pos.Lines().Last,
581 Err: fmt.Errorf("%s value cannot be empty", exprKey),
582 },
583 }, false
584 }
585 return Rule{Lines: lines}, true
586}
587
588func resolveMapAlias(part, parent *yaml.Node) *yaml.Node {
589 node := *part
590 node.Content = nil
591 var ok bool
592 for i, alias := range part.Alias.Content {
593 if i%2 == 0 {
594 ok = !hasKey(parent, alias.Value)
595 }
596 if ok {
597 node.Content = append(node.Content, alias)
598 }
599 if i%2 == 1 {
600 ok = false
601 }
602 }
603 return &node
604}
605
606func duplicatedKeyError(lines diags.LineRange, line int, key string) (Rule, bool) {
607 rule := Rule{
608 Lines: lines,
609 Error: ParseError{
610 Line: line,
611 Err: fmt.Errorf("duplicated %s key", key),
612 },
613 }
614 return rule, false
615}
616
617func invalidValueError(lines diags.LineRange, line int, key, expectedTag, gotTag string) (Rule, bool) {
618 rule := Rule{
619 Lines: lines,
620 Error: ParseError{
621 Line: line,
622 Err: fmt.Errorf("%s value must be a %s, got %s instead", key, expectedTag, gotTag),
623 },
624 }
625 return rule, false
626}
627
628func isTag(tag, expected string) bool {
629 if tag == nullTag {
630 return true
631 }
632 return tag == expected
633}
634
635type yamlMap struct {
636 key *yaml.Node
637 val *yaml.Node
638}
639
640func mappingNodes(node *yaml.Node) []yamlMap {
641 m := make([]yamlMap, 0, len(node.Content)/2)
642 var key *yaml.Node
643 for _, child := range node.Content {
644 if key != nil {
645 m = append(m, yamlMap{key: key, val: child})
646 key = nil
647 } else {
648 key = child
649 }
650 }
651 return m
652}
653
654func rangeFromYamlMaps(m []yamlMap) (lr diags.LineRange) {
655 for _, entry := range m {
656 if lr.First == 0 {
657 lr.First = entry.key.Line
658 lr.Last = entry.val.Line
659 }
660 lr.First = min(lr.First, entry.key.Line, entry.val.Line)
661 lr.Last = max(lr.Last, entry.key.Line, entry.val.Line)
662 }
663 return lr
664}
665
666var (
667 yamlErrRe = regexp.MustCompile("^yaml: line (.+): (.+)")
668 yamlUnmarshalErrRe = regexp.MustCompile("^yaml: unmarshal errors:\n line (.+): (.+)")
669)
670
671func tryDecodingYamlError(err error) ParseError {
672 for _, re := range []*regexp.Regexp{yamlErrRe, yamlUnmarshalErrRe} {
673 parts := re.FindStringSubmatch(err.Error())
674 if len(parts) > 2 {
675 if line, err2 := strconv.Atoi(parts[1]); line > 0 && err2 == nil {
676 return ParseError{
677 Line: line,
678 Err: errors.New(parts[2]),
679 }
680 }
681 }
682 }
683 return ParseError{Line: 1, Err: err}
684}
685
686func countLeadingSpace(line string) (i int) {
687 for _, r := range line {
688 if r != ' ' {
689 return i
690 }
691 i++
692 }
693 return i
694}
695
696func validateStringMap(field string, nodes []yamlMap, offsetLine int, lines diags.LineRange) (bool, ParseError, diags.LineRange) {
697 names := map[string]struct{}{}
698 for _, entry := range nodes {
699 if !isTag(entry.val.ShortTag(), strTag) {
700 return false, ParseError{
701 Line: entry.val.Line + offsetLine,
702 Err: fmt.Errorf("%s %s value must be a %s, got %s instead", field, entry.key.Value, describeTag(strTag), describeTag(entry.val.ShortTag())),
703 }, lines
704 }
705 if _, ok := names[entry.key.Value]; ok {
706 return false, ParseError{
707 Line: entry.key.Line,
708 Err: fmt.Errorf("duplicated %s key %s", field, entry.key.Value),
709 }, rangeFromYamlMaps(nodes)
710 }
711 names[entry.key.Value] = struct{}{}
712 }
713 return true, ParseError{}, lines
714}
715