cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.81.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

707lines · modecode

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