cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.77.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

700lines · 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 if (recordPart != nil || alertPart != nil) && exprPart != nil && labelsNode != nil && !isTag(labelsNode.ShortTag(), mapTag) {
378 return invalidValueError(lines, labelsNode.Line+offsetLine, labelsKey, describeTag(mapTag), describeTag(labelsNode.ShortTag()))
379 }
380
381 if alertPart != nil && exprPart != nil && annotationsNode != nil && !isTag(annotationsNode.ShortTag(), mapTag) {
382 return invalidValueError(lines, annotationsNode.Line+offsetLine, annotationsKey, describeTag(mapTag), describeTag(annotationsNode.ShortTag()))
383 }
384
385 if ok, perr, plines := validateStringMap(labelsKey, labelsNodes, offsetLine, lines); !ok {
386 return Rule{
387 Lines: plines,
388 Error: perr,
389 }, false
390 }
391
392 if ok, perr, plines := validateStringMap(annotationsKey, annotationsNodes, offsetLine, lines); !ok {
393 return Rule{
394 Lines: plines,
395 Error: perr,
396 }, false
397 }
398
399 if r, ok := ensureRequiredKeys(lines, recordKey, recordPart, exprPart); !ok {
400 return r, false
401 }
402 if r, ok := ensureRequiredKeys(lines, alertKey, alertPart, exprPart); !ok {
403 return r, false
404 }
405 if (recordPart != nil || alertPart != nil) && len(unknownKeys) > 0 {
406 var keys []string
407 for _, n := range unknownKeys {
408 keys = append(keys, n.Value)
409 }
410 rule = Rule{
411 Lines: lines,
412 Error: ParseError{
413 Line: unknownKeys[0].Line + offsetLine,
414 Err: fmt.Errorf("invalid key(s) found: %s", strings.Join(keys, ", ")),
415 },
416 }
417 return rule, false
418 }
419
420 if recordPart != nil && !p.names.IsValidMetricName(recordPart.Value) {
421 return Rule{
422 Lines: lines,
423 Error: ParseError{
424 Line: recordPart.Pos.Lines().First,
425 Err: fmt.Errorf("invalid recording rule name: %s", recordPart.Value),
426 },
427 }, false
428 }
429
430 if (recordPart != nil || alertPart != nil) && labelsPart != nil {
431 for _, lab := range labelsPart.Items {
432 if !p.names.IsValidLabelName(lab.Key.Value) || lab.Key.Value == model.MetricNameLabel {
433 return Rule{
434 Lines: lines,
435 Error: ParseError{
436 Line: lab.Key.Pos.Lines().First,
437 Err: fmt.Errorf("invalid label name: %s", lab.Key.Value),
438 },
439 }, false
440 }
441 if !model.LabelValue(lab.Value.Value).IsValid() {
442 return Rule{
443 Lines: lines,
444 Error: ParseError{
445 Line: lab.Key.Pos.Lines().First,
446 Err: fmt.Errorf("invalid label value: %s", lab.Value.Value),
447 },
448 }, false
449 }
450 }
451 }
452
453 if alertPart != nil && annotationsPart != nil {
454 for _, ann := range annotationsPart.Items {
455 if !p.names.IsValidLabelName(ann.Key.Value) {
456 return Rule{
457 Lines: lines,
458 Error: ParseError{
459 Line: ann.Key.Pos.Lines().First,
460 Err: fmt.Errorf("invalid annotation name: %s", ann.Key.Value),
461 },
462 }, false
463 }
464 }
465 }
466
467 if recordPart != nil && exprPart != nil {
468 rule = Rule{
469 Lines: lines,
470 RecordingRule: &RecordingRule{
471 Record: *recordPart,
472 Expr: *exprPart,
473 Labels: labelsPart,
474 },
475 Comments: ruleComments,
476 }
477 return rule, false
478 }
479
480 if alertPart != nil && exprPart != nil {
481 rule = Rule{
482 Lines: lines,
483 AlertingRule: &AlertingRule{
484 Alert: *alertPart,
485 Expr: *exprPart,
486 For: forPart,
487 KeepFiringFor: keepFiringForPart,
488 Labels: labelsPart,
489 Annotations: annotationsPart,
490 },
491 Comments: ruleComments,
492 }
493 return rule, false
494 }
495
496 return rule, true
497}
498
499func unpackNodes(node *yaml.Node) []*yaml.Node {
500 nodes := make([]*yaml.Node, 0, len(node.Content))
501 var isMerge bool
502 for _, part := range node.Content {
503 if part.ShortTag() == mergeTag && part.Value == "<<" {
504 isMerge = true
505 }
506
507 if part.Alias != nil {
508 if isMerge {
509 nodes = append(nodes, resolveMapAlias(part, node).Content...)
510 } else {
511 nodes = append(nodes, resolveMapAlias(part, part))
512 }
513 isMerge = false
514 continue
515 }
516 if isMerge {
517 continue
518 }
519 nodes = append(nodes, part)
520 }
521 return nodes
522}
523
524func hasKey(node *yaml.Node, key string) bool {
525 if node.Kind != yaml.MappingNode {
526 return false
527 }
528 for i := 0; i < len(node.Content); i += 2 {
529 if node.Content[i].Value == key {
530 return true
531 }
532 }
533 return false
534}
535
536func hasValue(node *YamlNode) bool {
537 if node == nil {
538 return false
539 }
540 return node.Value != ""
541}
542
543func ensureRequiredKeys(lines diags.LineRange, key string, keyVal *YamlNode, expr *PromQLExpr) (Rule, bool) {
544 if keyVal == nil {
545 return Rule{Lines: lines}, true
546 }
547 if !hasValue(keyVal) {
548 return Rule{
549 Lines: lines,
550 Error: ParseError{
551 Line: keyVal.Pos.Lines().Last,
552 Err: fmt.Errorf("%s value cannot be empty", key),
553 },
554 }, false
555 }
556 if expr == nil {
557 return Rule{
558 Lines: lines,
559 Error: ParseError{
560 Line: keyVal.Pos.Lines().Last,
561 Err: fmt.Errorf("missing %s key", exprKey),
562 },
563 }, false
564 }
565 if !hasValue(expr.Value) {
566 return Rule{
567 Lines: lines,
568 Error: ParseError{
569 Line: expr.Value.Pos.Lines().Last,
570 Err: fmt.Errorf("%s value cannot be empty", exprKey),
571 },
572 }, false
573 }
574 return Rule{Lines: lines}, true
575}
576
577func resolveMapAlias(part, parent *yaml.Node) *yaml.Node {
578 node := *part
579 node.Content = nil
580 var ok bool
581 for i, alias := range part.Alias.Content {
582 if i%2 == 0 {
583 ok = !hasKey(parent, alias.Value)
584 }
585 if ok {
586 node.Content = append(node.Content, alias)
587 }
588 if i%2 == 1 {
589 ok = false
590 }
591 }
592 return &node
593}
594
595func duplicatedKeyError(lines diags.LineRange, line int, key string) (Rule, bool) {
596 rule := Rule{
597 Lines: lines,
598 Error: ParseError{
599 Line: line,
600 Err: fmt.Errorf("duplicated %s key", key),
601 },
602 }
603 return rule, false
604}
605
606func invalidValueError(lines diags.LineRange, line int, key, expectedTag, gotTag string) (Rule, bool) {
607 rule := Rule{
608 Lines: lines,
609 Error: ParseError{
610 Line: line,
611 Err: fmt.Errorf("%s value must be a %s, got %s instead", key, expectedTag, gotTag),
612 },
613 }
614 return rule, false
615}
616
617func isTag(tag, expected string) bool {
618 if tag == nullTag {
619 return true
620 }
621 return tag == expected
622}
623
624type yamlMap struct {
625 key *yaml.Node
626 val *yaml.Node
627}
628
629func mappingNodes(node *yaml.Node) []yamlMap {
630 if node.Kind != yaml.MappingNode {
631 return nil
632 }
633 m := make([]yamlMap, 0, len(node.Content)/2)
634 for i := 0; i < len(node.Content); i += 2 {
635 m = append(m, yamlMap{key: node.Content[i], val: node.Content[i+1]})
636 }
637 return m
638}
639
640func rangeFromYamlMaps(m []yamlMap) (lr diags.LineRange) {
641 for _, entry := range m {
642 if lr.First == 0 {
643 lr.First = entry.key.Line
644 lr.Last = entry.val.Line
645 }
646 lr.First = min(lr.First, entry.key.Line, entry.val.Line)
647 lr.Last = max(lr.Last, entry.key.Line, entry.val.Line)
648 }
649 return lr
650}
651
652var (
653 yamlErrRe = regexp.MustCompile("^yaml: line (.+): (.+)")
654 yamlUnmarshalErrRe = regexp.MustCompile("^yaml: unmarshal errors:\n line (.+): (.+)")
655)
656
657func tryDecodingYamlError(err error) ParseError {
658 for _, re := range []*regexp.Regexp{yamlErrRe, yamlUnmarshalErrRe} {
659 parts := re.FindStringSubmatch(err.Error())
660 if len(parts) > 2 {
661 if line, err2 := strconv.Atoi(parts[1]); line > 0 && err2 == nil {
662 return ParseError{
663 Line: line,
664 Err: errors.New(parts[2]),
665 }
666 }
667 }
668 }
669 return ParseError{Line: 1, Err: err}
670}
671
672func countLeadingSpace(line string) (i int) {
673 for _, r := range line {
674 if r != ' ' {
675 return i
676 }
677 i++
678 }
679 return i
680}
681
682func validateStringMap(field string, nodes []yamlMap, offsetLine int, lines diags.LineRange) (bool, ParseError, diags.LineRange) {
683 names := map[string]struct{}{}
684 for _, entry := range nodes {
685 if !isTag(entry.val.ShortTag(), strTag) {
686 return false, ParseError{
687 Line: entry.val.Line + offsetLine,
688 Err: fmt.Errorf("%s %s value must be a %s, got %s instead", field, entry.key.Value, describeTag(strTag), describeTag(entry.val.ShortTag())),
689 }, lines
690 }
691 if _, ok := names[entry.key.Value]; ok {
692 return false, ParseError{
693 Line: entry.key.Line,
694 Err: fmt.Errorf("duplicated %s key %s", field, entry.key.Value),
695 }, rangeFromYamlMaps(nodes)
696 }
697 names[entry.key.Value] = struct{}{}
698 }
699 return true, ParseError{}, lines
700}
701