cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.71.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

660lines · modecode

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