cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.79.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

691lines · 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 // We don't validate labe values because they would only fail on invalid UTF-8.
442 // But YAML parsing already validates UTF-8 for us and we would get an error
443 // if we tried to parse file with invalid content.
444 }
445 }
446
447 if alertPart != nil && annotationsPart != nil {
448 for _, ann := range annotationsPart.Items {
449 if !p.names.IsValidLabelName(ann.Key.Value) {
450 return Rule{
451 Lines: lines,
452 Error: ParseError{
453 Line: ann.Key.Pos.Lines().First,
454 Err: fmt.Errorf("invalid annotation name: %s", ann.Key.Value),
455 },
456 }, false
457 }
458 }
459 }
460
461 if recordPart != nil && exprPart != nil {
462 rule = Rule{
463 Lines: lines,
464 RecordingRule: &RecordingRule{
465 Record: *recordPart,
466 Expr: *exprPart,
467 Labels: labelsPart,
468 },
469 Comments: ruleComments,
470 }
471 return rule, false
472 }
473
474 if alertPart != nil && exprPart != nil {
475 rule = Rule{
476 Lines: lines,
477 AlertingRule: &AlertingRule{
478 Alert: *alertPart,
479 Expr: *exprPart,
480 For: forPart,
481 KeepFiringFor: keepFiringForPart,
482 Labels: labelsPart,
483 Annotations: annotationsPart,
484 },
485 Comments: ruleComments,
486 }
487 return rule, false
488 }
489
490 return rule, true
491}
492
493func unpackNodes(node *yaml.Node) []*yaml.Node {
494 nodes := make([]*yaml.Node, 0, len(node.Content))
495 var isMerge bool
496 for _, part := range node.Content {
497 if part.ShortTag() == mergeTag && part.Value == "<<" {
498 isMerge = true
499 }
500
501 if part.Alias != nil {
502 if isMerge {
503 nodes = append(nodes, resolveMapAlias(part, node).Content...)
504 } else {
505 nodes = append(nodes, resolveMapAlias(part, part))
506 }
507 isMerge = false
508 continue
509 }
510 if isMerge {
511 continue
512 }
513 nodes = append(nodes, part)
514 }
515 return nodes
516}
517
518func hasKey(node *yaml.Node, key string) bool {
519 if node.Kind != yaml.MappingNode {
520 return false
521 }
522 for i := 0; i < len(node.Content); i += 2 {
523 if node.Content[i].Value == key {
524 return true
525 }
526 }
527 return false
528}
529
530func hasValue(node *YamlNode) bool {
531 return node != nil && node.Value != ""
532}
533
534func ensureRequiredKeys(lines diags.LineRange, key string, keyVal *YamlNode, expr *PromQLExpr) (Rule, bool) {
535 if keyVal == nil {
536 return Rule{Lines: lines}, true
537 }
538 if !hasValue(keyVal) {
539 return Rule{
540 Lines: lines,
541 Error: ParseError{
542 Line: keyVal.Pos.Lines().Last,
543 Err: fmt.Errorf("%s value cannot be empty", key),
544 },
545 }, false
546 }
547 if expr == nil {
548 return Rule{
549 Lines: lines,
550 Error: ParseError{
551 Line: keyVal.Pos.Lines().Last,
552 Err: fmt.Errorf("missing %s key", exprKey),
553 },
554 }, false
555 }
556 if !hasValue(expr.Value) {
557 return Rule{
558 Lines: lines,
559 Error: ParseError{
560 Line: expr.Value.Pos.Lines().Last,
561 Err: fmt.Errorf("%s value cannot be empty", exprKey),
562 },
563 }, false
564 }
565 return Rule{Lines: lines}, true
566}
567
568func resolveMapAlias(part, parent *yaml.Node) *yaml.Node {
569 node := *part
570 node.Content = nil
571 var ok bool
572 for i, alias := range part.Alias.Content {
573 if i%2 == 0 {
574 ok = !hasKey(parent, alias.Value)
575 }
576 if ok {
577 node.Content = append(node.Content, alias)
578 }
579 if i%2 == 1 {
580 ok = false
581 }
582 }
583 return &node
584}
585
586func duplicatedKeyError(lines diags.LineRange, line int, key string) (Rule, bool) {
587 rule := Rule{
588 Lines: lines,
589 Error: ParseError{
590 Line: line,
591 Err: fmt.Errorf("duplicated %s key", key),
592 },
593 }
594 return rule, false
595}
596
597func invalidValueError(lines diags.LineRange, line int, key, expectedTag, gotTag string) (Rule, bool) {
598 rule := Rule{
599 Lines: lines,
600 Error: ParseError{
601 Line: line,
602 Err: fmt.Errorf("%s value must be a %s, got %s instead", key, expectedTag, gotTag),
603 },
604 }
605 return rule, false
606}
607
608func isTag(tag, expected string) bool {
609 if tag == nullTag {
610 return true
611 }
612 return tag == expected
613}
614
615type yamlMap struct {
616 key *yaml.Node
617 val *yaml.Node
618}
619
620func mappingNodes(node *yaml.Node) []yamlMap {
621 if node.Kind != yaml.MappingNode {
622 return nil
623 }
624 m := make([]yamlMap, 0, len(node.Content)/2)
625 for i := 0; i < len(node.Content); i += 2 {
626 m = append(m, yamlMap{key: node.Content[i], val: node.Content[i+1]})
627 }
628 return m
629}
630
631func rangeFromYamlMaps(m []yamlMap) (lr diags.LineRange) {
632 for _, entry := range m {
633 if lr.First == 0 {
634 lr.First = entry.key.Line
635 lr.Last = entry.val.Line
636 }
637 lr.First = min(lr.First, entry.key.Line, entry.val.Line)
638 lr.Last = max(lr.Last, entry.key.Line, entry.val.Line)
639 }
640 return lr
641}
642
643var (
644 yamlErrRe = regexp.MustCompile("^yaml: line (.+): (.+)")
645 yamlUnmarshalErrRe = regexp.MustCompile("^yaml: unmarshal errors:\n line (.+): (.+)")
646)
647
648func tryDecodingYamlError(err error) ParseError {
649 for _, re := range []*regexp.Regexp{yamlErrRe, yamlUnmarshalErrRe} {
650 parts := re.FindStringSubmatch(err.Error())
651 if len(parts) > 2 {
652 if line, err2 := strconv.Atoi(parts[1]); line > 0 && err2 == nil {
653 return ParseError{
654 Line: line,
655 Err: errors.New(parts[2]),
656 }
657 }
658 }
659 }
660 return ParseError{Line: 1, Err: err}
661}
662
663func countLeadingSpace(line string) (i int) {
664 for _, r := range line {
665 if r != ' ' {
666 return i
667 }
668 i++
669 }
670 return i
671}
672
673func validateStringMap(field string, nodes []yamlMap, offsetLine int, lines diags.LineRange) (bool, ParseError, diags.LineRange) {
674 names := map[string]struct{}{}
675 for _, entry := range nodes {
676 if !isTag(entry.val.ShortTag(), strTag) {
677 return false, ParseError{
678 Line: entry.val.Line + offsetLine,
679 Err: fmt.Errorf("%s %s value must be a %s, got %s instead", field, entry.key.Value, describeTag(strTag), describeTag(entry.val.ShortTag())),
680 }, lines
681 }
682 if _, ok := names[entry.key.Value]; ok {
683 return false, ParseError{
684 Line: entry.key.Line,
685 Err: fmt.Errorf("duplicated %s key %s", field, entry.key.Value),
686 }, rangeFromYamlMaps(nodes)
687 }
688 names[entry.key.Value] = struct{}{}
689 }
690 return true, ParseError{}, lines
691}
692