cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.73.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

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