cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6f43db459f067aff2d2b2b36eb8773a8a8d5de43

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

701lines · modecode

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