cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.80.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

698lines · 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(groups,
161 p.parseNode(
162 &n,
163 node,
164 group,
165 offsetLine+node.Line,
166 offsetColumn+countLeadingSpace(contentLines[node.Line]),
167 strings.Split(node.Value, "\n"),
168 )...,
169 )
170 return groups
171 }
172 }
173 }
174
175 for _, child := range unpackNodes(node) {
176 groups = append(groups, p.parseNode(child, node, group, offsetLine, offsetColumn, contentLines)...)
177 }
178
179 return groups
180}
181
182func tryParseGroup(node *yaml.Node, offsetLine, offsetColumn int, contentLines []string) (g Group, rules yamlMap, _ bool) {
183 for _, e := range mappingNodes(node) {
184 switch val := nodeValue(e.key); val {
185 case "name":
186 g.Name = YamlNode{
187 Value: nodeValue(e.val),
188 Pos: diags.NewPositionRange(contentLines, e.val, 1),
189 }
190 case "interval":
191 g.Interval = newYamlDuration(e.val, offsetLine, offsetColumn, contentLines, 1)
192 case "limit":
193 g.Limit = newYamlInt(e.val, offsetLine, offsetColumn, contentLines, 1)
194 case "query_offset":
195 g.QueryOffset = newYamlDuration(e.val, offsetLine, offsetColumn, contentLines, 1)
196 case "labels":
197 g.Labels = newYamlMap(e.key, e.val, offsetLine, offsetColumn, contentLines)
198 case "rules":
199 if e.val.Kind == yaml.SequenceNode {
200 rules = e
201 }
202 }
203 }
204 return g, rules, g.Name.Value != "" && rules.key != nil
205}
206
207func (p Parser) parseRule(node *yaml.Node, offsetLine, offsetColumn int, contentLines []string) (rule Rule, _ bool) {
208 var recordPart *YamlNode
209 var exprPart *PromQLExpr
210 var labelsPart *YamlMap
211
212 var alertPart *YamlNode
213 var forPart *YamlDuration
214 var keepFiringForPart *YamlDuration
215 var annotationsPart *YamlMap
216
217 var recordNode *yaml.Node
218 var alertNode *yaml.Node
219 var exprNode *yaml.Node
220 var forNode *yaml.Node
221 var keepFiringForNode *yaml.Node
222 var labelsNode *yaml.Node
223 var annotationsNode *yaml.Node
224
225 labelsNodes := []yamlMap{}
226 annotationsNodes := []yamlMap{}
227
228 var key *yaml.Node
229 unknownKeys := []*yaml.Node{}
230
231 var lines diags.LineRange
232
233 var ruleComments []comments.Comment
234
235 for i, part := range unpackNodes(node) {
236 if lines.First == 0 || part.Line+offsetLine < lines.First {
237 lines.First = part.Line + offsetLine
238 }
239 lines.Last = max(lines.Last, part.Line+offsetLine)
240
241 if i == 0 && node.HeadComment != "" && part.HeadComment == "" {
242 part.HeadComment = node.HeadComment
243 }
244 if i == 0 && node.LineComment != "" && part.LineComment == "" {
245 part.LineComment = node.LineComment
246 }
247 if i == len(node.Content)-1 && node.FootComment != "" && part.HeadComment == "" {
248 part.FootComment = node.FootComment
249 }
250 for _, s := range mergeComments(part) {
251 for _, c := range comments.Parse(part.Line, s) {
252 if comments.IsRuleComment(c.Type) {
253 ruleComments = append(ruleComments, c)
254 }
255 }
256 }
257
258 if i%2 == 0 {
259 key = part
260 } else {
261 switch key.Value {
262 case recordKey:
263 if recordPart != nil {
264 return duplicatedKeyError(lines, part.Line+offsetLine, recordKey)
265 }
266 recordNode = part
267 recordPart = newYamlNode(part, offsetLine, offsetColumn, contentLines, key.Column+2)
268 lines.Last = max(lines.Last, recordPart.Pos.Lines().Last)
269 case alertKey:
270 if alertPart != nil {
271 return duplicatedKeyError(lines, part.Line+offsetLine, alertKey)
272 }
273 alertNode = part
274 alertPart = newYamlNode(part, offsetLine, offsetColumn, contentLines, key.Column+2)
275 lines.Last = max(lines.Last, alertPart.Pos.Lines().Last)
276 case exprKey:
277 if exprPart != nil {
278 return duplicatedKeyError(lines, part.Line+offsetLine, exprKey)
279 }
280 exprNode = part
281 exprPart = newPromQLExpr(part, offsetLine, offsetColumn, contentLines, key.Column+2)
282 lines.Last = max(lines.Last, exprPart.Value.Pos.Lines().Last)
283 case forKey:
284 if forPart != nil {
285 return duplicatedKeyError(lines, part.Line+offsetLine, forKey)
286 }
287 forNode = part
288 forPart = newYamlDuration(part, offsetLine, offsetColumn, contentLines, key.Column+2)
289 lines.Last = max(lines.Last, forPart.Pos.Lines().Last)
290 case keepFiringForKey:
291 if keepFiringForPart != nil {
292 return duplicatedKeyError(lines, part.Line+offsetLine, keepFiringForKey)
293 }
294 keepFiringForNode = part
295 keepFiringForPart = newYamlDuration(part, offsetLine, offsetColumn, contentLines, key.Column+2)
296 lines.Last = max(lines.Last, keepFiringForPart.Pos.Lines().Last)
297 case labelsKey:
298 if labelsPart != nil {
299 return duplicatedKeyError(lines, part.Line+offsetLine, labelsKey)
300 }
301 labelsNode = part
302 labelsNodes = mappingNodes(part)
303 labelsPart = newYamlMap(key, part, offsetLine, offsetColumn, contentLines)
304 lines.Last = max(lines.Last, labelsPart.Lines().Last)
305 case annotationsKey:
306 if annotationsPart != nil {
307 return duplicatedKeyError(lines, part.Line+offsetLine, annotationsKey)
308 }
309 annotationsNode = part
310 annotationsNodes = mappingNodes(part)
311 annotationsPart = newYamlMap(key, part, offsetLine, offsetColumn, contentLines)
312 lines.Last = max(lines.Last, annotationsPart.Lines().Last)
313 default:
314 unknownKeys = append(unknownKeys, key)
315 }
316 }
317 }
318
319 if recordPart != nil && alertPart != nil {
320 rule = Rule{
321 Lines: lines,
322 Error: ParseError{
323 Line: node.Line + offsetLine,
324 Err: fmt.Errorf("got both %s and %s keys in a single rule", recordKey, alertKey),
325 },
326 }
327 return rule, false
328 }
329 if exprPart != nil && alertPart == nil && recordPart == nil {
330 rule = Rule{
331 Lines: lines,
332 Error: ParseError{
333 Line: exprPart.Value.Pos.Lines().Last,
334 Err: fmt.Errorf("incomplete rule, no %s or %s key", alertKey, recordKey),
335 },
336 }
337 return rule, false
338 }
339 if recordPart != nil && forNode != nil {
340 rule = Rule{
341 Lines: lines,
342 Error: ParseError{
343 Line: forNode.Line + offsetLine,
344 Err: fmt.Errorf("invalid field '%s' in recording rule", forKey),
345 },
346 }
347 return rule, false
348 }
349 if recordPart != nil && keepFiringForNode != nil {
350 rule = Rule{
351 Lines: lines,
352 Error: ParseError{
353 Line: keepFiringForNode.Line + offsetLine,
354 Err: fmt.Errorf("invalid field '%s' in recording rule", keepFiringForKey),
355 },
356 }
357 return rule, false
358 }
359 if recordPart != nil && annotationsPart != nil {
360 rule = Rule{
361 Lines: lines,
362 Error: ParseError{
363 Line: annotationsPart.Lines().First,
364 Err: fmt.Errorf("invalid field '%s' in recording rule", annotationsKey),
365 },
366 }
367 return rule, false
368 }
369 for _, entry := range []struct {
370 part *yaml.Node
371 key string
372 }{
373 {key: recordKey, part: recordNode},
374 {key: alertKey, part: alertNode},
375 {key: exprKey, part: exprNode},
376 {key: forKey, part: forNode},
377 {key: keepFiringForKey, part: keepFiringForNode},
378 } {
379 if entry.part != nil && !isTag(entry.part.ShortTag(), strTag) {
380 return invalidValueError(lines, entry.part.Line+offsetLine, entry.key, describeTag(strTag), describeTag(entry.part.ShortTag()))
381 }
382 }
383
384 if (recordPart != nil || alertPart != nil) && exprPart != nil && labelsNode != nil && !isTag(labelsNode.ShortTag(), mapTag) {
385 return invalidValueError(lines, labelsNode.Line+offsetLine, labelsKey, describeTag(mapTag), describeTag(labelsNode.ShortTag()))
386 }
387
388 if alertPart != nil && exprPart != nil && annotationsNode != nil && !isTag(annotationsNode.ShortTag(), mapTag) {
389 return invalidValueError(lines, annotationsNode.Line+offsetLine, annotationsKey, describeTag(mapTag), describeTag(annotationsNode.ShortTag()))
390 }
391
392 if ok, perr, plines := validateStringMap(labelsKey, labelsNodes, offsetLine, lines); !ok {
393 return Rule{
394 Lines: plines,
395 Error: perr,
396 }, false
397 }
398
399 if ok, perr, plines := validateStringMap(annotationsKey, annotationsNodes, offsetLine, lines); !ok {
400 return Rule{
401 Lines: plines,
402 Error: perr,
403 }, false
404 }
405
406 if r, ok := ensureRequiredKeys(lines, recordKey, recordPart, exprPart); !ok {
407 return r, false
408 }
409 if r, ok := ensureRequiredKeys(lines, alertKey, alertPart, exprPart); !ok {
410 return r, false
411 }
412 if (recordPart != nil || alertPart != nil) && len(unknownKeys) > 0 {
413 var keys []string
414 for _, n := range unknownKeys {
415 keys = append(keys, n.Value)
416 }
417 rule = Rule{
418 Lines: lines,
419 Error: ParseError{
420 Line: unknownKeys[0].Line + offsetLine,
421 Err: fmt.Errorf("invalid key(s) found: %s", strings.Join(keys, ", ")),
422 },
423 }
424 return rule, false
425 }
426
427 if recordPart != nil && !p.opts.Names.IsValidMetricName(recordPart.Value) {
428 return Rule{
429 Lines: lines,
430 Error: ParseError{
431 Line: recordPart.Pos.Lines().First,
432 Err: fmt.Errorf("invalid recording rule name: %s", recordPart.Value),
433 },
434 }, false
435 }
436
437 if (recordPart != nil || alertPart != nil) && labelsPart != nil {
438 for _, lab := range labelsPart.Items {
439 if !p.opts.Names.IsValidLabelName(lab.Key.Value) || lab.Key.Value == model.MetricNameLabel {
440 return Rule{
441 Lines: lines,
442 Error: ParseError{
443 Line: lab.Key.Pos.Lines().First,
444 Err: fmt.Errorf("invalid label name: %s", lab.Key.Value),
445 },
446 }, false
447 }
448 // We don't validate labe values because they would only fail on invalid UTF-8.
449 // But YAML parsing already validates UTF-8 for us and we would get an error
450 // if we tried to parse file with invalid content.
451 }
452 }
453
454 if alertPart != nil && annotationsPart != nil {
455 for _, ann := range annotationsPart.Items {
456 if !p.opts.Names.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 unpackNodes(node *yaml.Node) []*yaml.Node {
501 nodes := make([]*yaml.Node, 0, len(node.Content))
502 var isMerge bool
503 for _, part := range node.Content {
504 if part.ShortTag() == mergeTag && part.Value == "<<" {
505 isMerge = true
506 }
507
508 if part.Alias != nil {
509 if isMerge {
510 nodes = append(nodes, resolveMapAlias(part, node).Content...)
511 } else {
512 nodes = append(nodes, resolveMapAlias(part, part))
513 }
514 isMerge = false
515 continue
516 }
517 if isMerge {
518 continue
519 }
520 nodes = append(nodes, part)
521 }
522 return nodes
523}
524
525func hasKey(node *yaml.Node, key string) bool {
526 if node.Kind != yaml.MappingNode {
527 return false
528 }
529 for i := 0; i < len(node.Content); i += 2 {
530 if node.Content[i].Value == key {
531 return true
532 }
533 }
534 return false
535}
536
537func hasValue(node *YamlNode) bool {
538 return node != nil && node.Value != ""
539}
540
541func ensureRequiredKeys(lines diags.LineRange, key string, keyVal *YamlNode, expr *PromQLExpr) (Rule, bool) {
542 if keyVal == nil {
543 return Rule{Lines: lines}, true
544 }
545 if !hasValue(keyVal) {
546 return Rule{
547 Lines: lines,
548 Error: ParseError{
549 Line: keyVal.Pos.Lines().Last,
550 Err: fmt.Errorf("%s value cannot be empty", key),
551 },
552 }, false
553 }
554 if expr == nil {
555 return Rule{
556 Lines: lines,
557 Error: ParseError{
558 Line: keyVal.Pos.Lines().Last,
559 Err: fmt.Errorf("missing %s key", exprKey),
560 },
561 }, false
562 }
563 if !hasValue(expr.Value) {
564 return Rule{
565 Lines: lines,
566 Error: ParseError{
567 Line: expr.Value.Pos.Lines().Last,
568 Err: fmt.Errorf("%s value cannot be empty", exprKey),
569 },
570 }, false
571 }
572 return Rule{Lines: lines}, true
573}
574
575func resolveMapAlias(part, parent *yaml.Node) *yaml.Node {
576 node := *part
577 node.Content = nil
578 var ok bool
579 for i, alias := range part.Alias.Content {
580 if i%2 == 0 {
581 ok = !hasKey(parent, alias.Value)
582 }
583 if ok {
584 node.Content = append(node.Content, alias)
585 }
586 if i%2 == 1 {
587 ok = false
588 }
589 }
590 return &node
591}
592
593func duplicatedKeyError(lines diags.LineRange, line int, key string) (Rule, bool) {
594 rule := Rule{
595 Lines: lines,
596 Error: ParseError{
597 Line: line,
598 Err: fmt.Errorf("duplicated %s key", key),
599 },
600 }
601 return rule, false
602}
603
604func invalidValueError(lines diags.LineRange, line int, key, expectedTag, gotTag string) (Rule, bool) {
605 rule := Rule{
606 Lines: lines,
607 Error: ParseError{
608 Line: line,
609 Err: fmt.Errorf("%s value must be a %s, got %s instead", key, expectedTag, gotTag),
610 },
611 }
612 return rule, false
613}
614
615func isTag(tag, expected string) bool {
616 if tag == nullTag {
617 return true
618 }
619 return tag == expected
620}
621
622type yamlMap struct {
623 key *yaml.Node
624 val *yaml.Node
625}
626
627func mappingNodes(node *yaml.Node) []yamlMap {
628 if node.Kind != yaml.MappingNode {
629 return nil
630 }
631 m := make([]yamlMap, 0, len(node.Content)/2)
632 for i := 0; i < len(node.Content); i += 2 {
633 m = append(m, yamlMap{key: node.Content[i], val: node.Content[i+1]})
634 }
635 return m
636}
637
638func rangeFromYamlMaps(m []yamlMap) (lr diags.LineRange) {
639 for _, entry := range m {
640 if lr.First == 0 {
641 lr.First = entry.key.Line
642 lr.Last = entry.val.Line
643 }
644 lr.First = min(lr.First, entry.key.Line, entry.val.Line)
645 lr.Last = max(lr.Last, entry.key.Line, entry.val.Line)
646 }
647 return lr
648}
649
650var (
651 yamlErrRe = regexp.MustCompile("^yaml: line (.+): (.+)")
652 yamlUnmarshalErrRe = regexp.MustCompile("^yaml: unmarshal errors:\n line (.+): (.+)")
653)
654
655func tryDecodingYamlError(err error) ParseError {
656 for _, re := range []*regexp.Regexp{yamlErrRe, yamlUnmarshalErrRe} {
657 parts := re.FindStringSubmatch(err.Error())
658 if len(parts) > 2 {
659 if line, err2 := strconv.Atoi(parts[1]); line > 0 && err2 == nil {
660 return ParseError{
661 Line: line,
662 Err: errors.New(parts[2]),
663 }
664 }
665 }
666 }
667 return ParseError{Line: 1, Err: err}
668}
669
670func countLeadingSpace(line string) (i int) {
671 for _, r := range line {
672 if r != ' ' {
673 return i
674 }
675 i++
676 }
677 return i
678}
679
680func validateStringMap(field string, nodes []yamlMap, offsetLine int, lines diags.LineRange) (bool, ParseError, diags.LineRange) {
681 names := map[string]struct{}{}
682 for _, entry := range nodes {
683 if !isTag(entry.val.ShortTag(), strTag) {
684 return false, ParseError{
685 Line: entry.val.Line + offsetLine,
686 Err: fmt.Errorf("%s %s value must be a %s, got %s instead", field, entry.key.Value, describeTag(strTag), describeTag(entry.val.ShortTag())),
687 }, lines
688 }
689 if _, ok := names[entry.key.Value]; ok {
690 return false, ParseError{
691 Line: entry.key.Line,
692 Err: fmt.Errorf("duplicated %s key %s", field, entry.key.Value),
693 }, rangeFromYamlMaps(nodes)
694 }
695 names[entry.key.Value] = struct{}{}
696 }
697 return true, ParseError{}, lines
698}
699