cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.63.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/parser.go

648lines · modepreview

package parser

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"regexp"
	"strconv"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/prometheus/common/model"

	"github.com/cloudflare/pint/internal/comments"
)

const (
	recordKey        = "record"
	exprKey          = "expr"
	labelsKey        = "labels"
	alertKey         = "alert"
	forKey           = "for"
	keepFiringForKey = "keep_firing_for"
	annotationsKey   = "annotations"
)

var ErrRuleCommentOnFile = errors.New("this comment is only valid when attached to a rule")

func NewParser(isStrict bool) Parser {
	return Parser{
		isStrict: isStrict,
	}
}

type Parser struct {
	isStrict bool
}

func (p Parser) Parse(content []byte) (rules []Rule, err error) {
	if len(content) == 0 {
		return nil, nil
	}

	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("unable to parse YAML file: %s", r)
		}
	}()

	dec := yaml.NewDecoder(bytes.NewReader(content))
	var index int
	for {
		var doc yaml.Node
		decodeErr := dec.Decode(&doc)
		if errors.Is(decodeErr, io.EOF) {
			break
		}
		if decodeErr != nil {
			return nil, tryDecodingYamlError(decodeErr)
		}
		index++
		if p.isStrict {
			r, err := parseGroups(content, &doc)
			if err.Err != nil {
				return rules, err
			}
			rules = append(rules, r...)
		} else {
			rules = append(rules, parseNode(content, &doc, 0)...)
		}
		if index > 1 && p.isStrict {
			rules = append(rules, Rule{
				Lines: LineRange{First: doc.Line, Last: doc.Line},
				Error: ParseError{
					Err: errors.New("multi-document YAML files are not allowed"),
					Details: `This is a multi-document YAML file. Prometheus will only parse the first document and silently ignore the rest.
To allow for multi-document YAML files set parser->relaxed option in pint config file.`,
					Line: doc.Line,
				},
			})
		}
	}

	return rules, err
}

func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule) {
	ret, isEmpty := parseRule(content, node, offset)
	if !isEmpty {
		rules = append(rules, ret)
		return rules
	}

	var rule Rule
	for _, root := range node.Content {
		// nolint: exhaustive
		switch root.Kind {
		case yaml.SequenceNode:
			for _, n := range root.Content {
				rules = append(rules, parseNode(content, n, offset)...)
			}
		case yaml.MappingNode:
			rule, isEmpty = parseRule(content, root, offset)
			if !isEmpty {
				rules = append(rules, rule)
			} else {
				for _, n := range root.Content {
					rules = append(rules, parseNode(content, n, offset)...)
				}
			}
		case yaml.ScalarNode:
			if root.Value != string(content) {
				c := []byte(root.Value)
				var n yaml.Node
				if err := yaml.Unmarshal(c, &n); err == nil {
					rules = append(rules, parseNode(c, &n, offset+root.Line)...)
				}
			}
		}
	}
	return rules
}

func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool) {
	if node.Kind != yaml.MappingNode {
		return rule, true
	}

	var recordPart *YamlNode
	var exprPart *PromQLExpr
	var labelsPart *YamlMap

	var alertPart *YamlNode
	var forPart *YamlNode
	var keepFiringForPart *YamlNode
	var annotationsPart *YamlMap

	var recordNode *yaml.Node
	var alertNode *yaml.Node
	var exprNode *yaml.Node
	var forNode *yaml.Node
	var keepFiringForNode *yaml.Node
	var labelsNode *yaml.Node
	var annotationsNode *yaml.Node
	labelsNodes := []yamlMap{}
	annotationsNodes := []yamlMap{}

	var key *yaml.Node
	unknownKeys := []*yaml.Node{}

	var lines LineRange

	var ruleComments []comments.Comment

	for i, part := range unpackNodes(node) {
		if lines.First == 0 || part.Line+offset < lines.First {
			lines.First = part.Line + offset
		}
		lines.Last = max(lines.Last, part.Line+offset)

		if i == 0 && node.HeadComment != "" && part.HeadComment == "" {
			part.HeadComment = node.HeadComment
		}
		if i == 0 && node.LineComment != "" && part.LineComment == "" {
			part.LineComment = node.LineComment
		}
		if i == len(node.Content)-1 && node.FootComment != "" && part.HeadComment == "" {
			part.FootComment = node.FootComment
		}
		for _, s := range mergeComments(part) {
			for _, c := range comments.Parse(part.Line, s) {
				if comments.IsRuleComment(c.Type) {
					ruleComments = append(ruleComments, c)
				}
			}
		}

		if i%2 == 0 {
			key = part
		} else {
			switch key.Value {
			case recordKey:
				if recordPart != nil {
					return duplicatedKeyError(lines, part.Line+offset, recordKey)
				}
				recordNode = part
				recordPart = newYamlNodeWithKey(key, part, offset)
				lines.Last = max(lines.Last, recordPart.Lines.Last)
			case alertKey:
				if alertPart != nil {
					return duplicatedKeyError(lines, part.Line+offset, alertKey)
				}
				alertNode = part
				alertPart = newYamlNodeWithKey(key, part, offset)
				lines.Last = max(lines.Last, alertPart.Lines.Last)
			case exprKey:
				if exprPart != nil {
					return duplicatedKeyError(lines, part.Line+offset, exprKey)
				}
				exprNode = part
				exprPart = newPromQLExpr(key, part, offset)
				lines.Last = max(lines.Last, exprPart.Value.Lines.Last)
			case forKey:
				if forPart != nil {
					return duplicatedKeyError(lines, part.Line+offset, forKey)
				}
				forNode = part
				forPart = newYamlNodeWithKey(key, part, offset)
				lines.Last = max(lines.Last, forPart.Lines.Last)
			case keepFiringForKey:
				if keepFiringForPart != nil {
					return duplicatedKeyError(lines, part.Line+offset, keepFiringForKey)
				}
				keepFiringForNode = part
				keepFiringForPart = newYamlNodeWithKey(key, part, offset)
				lines.Last = max(lines.Last, keepFiringForPart.Lines.Last)
			case labelsKey:
				if labelsPart != nil {
					return duplicatedKeyError(lines, part.Line+offset, labelsKey)
				}
				labelsNode = part
				labelsNodes = mappingNodes(part)
				labelsPart = newYamlMap(key, part, offset)
				lines.Last = max(lines.Last, labelsPart.Lines.Last)
			case annotationsKey:
				if annotationsPart != nil {
					return duplicatedKeyError(lines, part.Line+offset, annotationsKey)
				}
				annotationsNode = part
				annotationsNodes = mappingNodes(part)
				annotationsPart = newYamlMap(key, part, offset)
				lines.Last = max(lines.Last, annotationsPart.Lines.Last)
			default:
				unknownKeys = append(unknownKeys, key)
			}
		}
	}

	if exprPart != nil && exprPart.Value.Lines.First != exprPart.Value.Lines.Last {
		contentLines := strings.Split(string(content), "\n")
		for {
			start := exprPart.Value.Lines.First
			end := exprPart.Value.Lines.Last
			if end > len(contentLines) {
				end--
			}
			input := strings.Join(contentLines[start:end], "")
			input = strings.ReplaceAll(input, " ", "")
			output := strings.ReplaceAll(exprPart.Value.Value, "\n", "")
			output = strings.ReplaceAll(output, " ", "")
			if end >= len(contentLines) {
				break
			}
			if input == output {
				break
			}
			exprPart.Value.Lines.Last = end + 1
		}
	}

	if recordPart != nil && alertPart != nil {
		rule = Rule{
			Lines: lines,
			Error: ParseError{
				Line: node.Line + offset,
				Err:  fmt.Errorf("got both %s and %s keys in a single rule", recordKey, alertKey),
			},
		}
		return rule, false
	}
	if exprPart != nil && alertPart == nil && recordPart == nil {
		rule = Rule{
			Lines: lines,
			Error: ParseError{
				Line: exprPart.Value.Lines.Last,
				Err:  fmt.Errorf("incomplete rule, no %s or %s key", alertKey, recordKey),
			},
		}
		return rule, false
	}
	if recordPart != nil && forPart != nil {
		rule = Rule{
			Lines: lines,
			Error: ParseError{
				Line: forPart.Lines.First,
				Err:  fmt.Errorf("invalid field '%s' in recording rule", forKey),
			},
		}
		return rule, false
	}
	if recordPart != nil && keepFiringForPart != nil {
		rule = Rule{
			Lines: lines,
			Error: ParseError{
				Line: keepFiringForPart.Lines.First,
				Err:  fmt.Errorf("invalid field '%s' in recording rule", keepFiringForKey),
			},
		}
		return rule, false
	}
	if recordPart != nil && annotationsPart != nil {
		rule = Rule{
			Lines: lines,
			Error: ParseError{
				Line: annotationsPart.Lines.First,
				Err:  fmt.Errorf("invalid field '%s' in recording rule", annotationsKey),
			},
		}
		return rule, false
	}
	for _, entry := range []struct {
		part *yaml.Node
		key  string
	}{
		{key: recordKey, part: recordNode},
		{key: alertKey, part: alertNode},
		{key: exprKey, part: exprNode},
		{key: forKey, part: forNode},
		{key: keepFiringForKey, part: keepFiringForNode},
	} {
		if entry.part != nil && !isTag(entry.part.ShortTag(), strTag) {
			return invalidValueError(lines, entry.part.Line+offset, entry.key, describeTag(strTag), describeTag(entry.part.ShortTag()))
		}
	}

	for _, entry := range []struct {
		part *yaml.Node
		key  string
	}{
		{key: labelsKey, part: labelsNode},
		{key: annotationsKey, part: annotationsNode},
	} {
		if entry.part != nil && !isTag(entry.part.ShortTag(), mapTag) {
			return invalidValueError(lines, entry.part.Line+offset, entry.key, describeTag(mapTag), describeTag(entry.part.ShortTag()))
		}
	}

	for _, elem := range []struct {
		key   string
		parts []yamlMap
	}{
		{key: labelsKey, parts: labelsNodes},
		{key: annotationsKey, parts: annotationsNodes},
	} {
		names := map[string]struct{}{}
		for _, entry := range elem.parts {
			if !isTag(entry.val.ShortTag(), strTag) {
				return invalidValueError(lines, entry.val.Line+offset, fmt.Sprintf("%s %s", elem.key, nodeValue(entry.key)), describeTag(strTag), describeTag(entry.val.ShortTag()))
			}
			if _, ok := names[entry.key.Value]; ok {
				return Rule{
					Lines: rangeFromYamlMaps(elem.parts),
					Error: ParseError{
						Line: entry.key.Line,
						Err:  fmt.Errorf("duplicated %s key %s", elem.key, entry.key.Value),
					},
				}, false
			}
			names[entry.key.Value] = struct{}{}
		}
	}

	if r, ok := ensureRequiredKeys(lines, recordKey, recordPart, exprPart); !ok {
		return r, false
	}
	if r, ok := ensureRequiredKeys(lines, alertKey, alertPart, exprPart); !ok {
		return r, false
	}
	if (recordPart != nil || alertPart != nil) && len(unknownKeys) > 0 {
		var keys []string
		for _, n := range unknownKeys {
			keys = append(keys, n.Value)
		}
		rule = Rule{
			Lines: lines,
			Error: ParseError{
				Line: unknownKeys[0].Line + offset,
				Err:  fmt.Errorf("invalid key(s) found: %s", strings.Join(keys, ", ")),
			},
		}
		return rule, false
	}

	if recordPart != nil && !model.IsValidMetricName(model.LabelValue(recordPart.Value)) {
		return Rule{
			Lines: lines,
			Error: ParseError{
				Line: recordPart.Lines.First,
				Err:  fmt.Errorf("invalid recording rule name: %s", recordPart.Value),
			},
		}, false
	}

	if (recordPart != nil || alertPart != nil) && labelsPart != nil {
		for _, lab := range labelsPart.Items {
			if !model.LabelName(lab.Key.Value).IsValid() || lab.Key.Value == model.MetricNameLabel {
				return Rule{
					Lines: lines,
					Error: ParseError{
						Line: lab.Key.Lines.First,
						Err:  fmt.Errorf("invalid label name: %s", lab.Key.Value),
					},
				}, false
			}
			if !model.LabelValue(lab.Value.Value).IsValid() {
				return Rule{
					Lines: lines,
					Error: ParseError{
						Line: lab.Key.Lines.First,
						Err:  fmt.Errorf("invalid label value: %s", lab.Value.Value),
					},
				}, false
			}
		}
	}

	if alertPart != nil && annotationsPart != nil {
		for _, ann := range annotationsPart.Items {
			if !model.LabelName(ann.Key.Value).IsValid() {
				return Rule{
					Lines: lines,
					Error: ParseError{
						Line: ann.Key.Lines.First,
						Err:  fmt.Errorf("invalid annotation name: %s", ann.Key.Value),
					},
				}, false
			}
		}
	}

	if recordPart != nil && exprPart != nil {
		rule = Rule{
			Lines: lines,
			RecordingRule: &RecordingRule{
				Record: *recordPart,
				Expr:   *exprPart,
				Labels: labelsPart,
			},
			Comments: ruleComments,
		}
		return rule, false
	}

	if alertPart != nil && exprPart != nil {
		rule = Rule{
			Lines: lines,
			AlertingRule: &AlertingRule{
				Alert:         *alertPart,
				Expr:          *exprPart,
				For:           forPart,
				KeepFiringFor: keepFiringForPart,
				Labels:        labelsPart,
				Annotations:   annotationsPart,
			},
			Comments: ruleComments,
		}
		return rule, false
	}

	return rule, true
}

func unpackNodes(node *yaml.Node) []*yaml.Node {
	nodes := make([]*yaml.Node, 0, len(node.Content))
	var isMerge bool
	for _, part := range node.Content {
		if part.ShortTag() == mergeTag && part.Value == "<<" {
			isMerge = true
		}

		if part.Alias != nil {
			if isMerge {
				nodes = append(nodes, resolveMapAlias(part, node).Content...)
			} else {
				nodes = append(nodes, resolveMapAlias(part, part))
			}
			isMerge = false
			continue
		}
		if isMerge {
			continue
		}
		nodes = append(nodes, part)
	}
	return nodes
}

func nodeKeys(node *yaml.Node) (keys []string) {
	if node.Kind != yaml.MappingNode {
		return keys
	}
	for i, n := range node.Content {
		if i%2 == 0 && n.Value != "" {
			keys = append(keys, n.Value)
		}
	}
	return keys
}

func hasKey(node *yaml.Node, key string) bool {
	for _, k := range nodeKeys(node) {
		if k == key {
			return true
		}
	}
	return false
}

func hasValue(node *YamlNode) bool {
	if node == nil {
		return false
	}
	return node.Value != ""
}

func ensureRequiredKeys(lines LineRange, key string, keyVal *YamlNode, expr *PromQLExpr) (Rule, bool) {
	if keyVal == nil {
		return Rule{Lines: lines}, true
	}
	if !hasValue(keyVal) {
		return Rule{
			Lines: lines,
			Error: ParseError{
				Line: keyVal.Lines.Last,
				Err:  fmt.Errorf("%s value cannot be empty", key),
			},
		}, false
	}
	if expr == nil {
		return Rule{
			Lines: lines,
			Error: ParseError{
				Line: keyVal.Lines.Last,
				Err:  fmt.Errorf("missing %s key", exprKey),
			},
		}, false
	}
	if !hasValue(expr.Value) {
		return Rule{
			Lines: lines,
			Error: ParseError{
				Line: expr.Value.Lines.Last,
				Err:  fmt.Errorf("%s value cannot be empty", exprKey),
			},
		}, false
	}
	return Rule{Lines: lines}, true
}

func resolveMapAlias(part, parent *yaml.Node) *yaml.Node {
	node := *part
	node.Content = nil
	var ok bool
	for i, alias := range part.Alias.Content {
		if i%2 == 0 {
			ok = !hasKey(parent, alias.Value)
		}
		if ok {
			node.Content = append(node.Content, alias)
		}
		if i%2 == 1 {
			ok = false
		}
	}
	return &node
}

func duplicatedKeyError(lines LineRange, line int, key string) (Rule, bool) {
	rule := Rule{
		Lines: lines,
		Error: ParseError{
			Line: line,
			Err:  fmt.Errorf("duplicated %s key", key),
		},
	}
	return rule, false
}

func invalidValueError(lines LineRange, line int, key, expectedTag, gotTag string) (Rule, bool) {
	rule := Rule{
		Lines: lines,
		Error: ParseError{
			Line: line,
			Err:  fmt.Errorf("%s value must be a %s, got %s instead", key, expectedTag, gotTag),
		},
	}
	return rule, false
}

func isTag(tag, expected string) bool {
	if tag == nullTag {
		return true
	}
	return tag == expected
}

type yamlMap struct {
	key *yaml.Node
	val *yaml.Node
}

func mappingNodes(node *yaml.Node) []yamlMap {
	m := make([]yamlMap, 0, len(node.Content)/2)
	var key *yaml.Node
	for _, child := range node.Content {
		if key != nil {
			m = append(m, yamlMap{key: key, val: child})
			key = nil
		} else {
			key = child
		}
	}
	return m
}

func rangeFromYamlMaps(m []yamlMap) (lr LineRange) {
	for _, entry := range m {
		if lr.First == 0 {
			lr.First = entry.key.Line
			lr.Last = entry.val.Line
		}
		lr.First = min(lr.First, entry.key.Line, entry.val.Line)
		lr.Last = max(lr.Last, entry.key.Line, entry.val.Line)
	}
	return lr
}

var (
	yamlErrRe          = regexp.MustCompile("^yaml: line (.+): (.+)")
	yamlUnmarshalErrRe = regexp.MustCompile("^yaml: unmarshal errors:\n  line (.+): (.+)")
)

func tryDecodingYamlError(err error) ParseError {
	for _, re := range []*regexp.Regexp{yamlErrRe, yamlUnmarshalErrRe} {
		parts := re.FindStringSubmatch(err.Error())
		if len(parts) > 2 {
			if line, err2 := strconv.Atoi(parts[1]); line > 0 && err2 == nil {
				return ParseError{
					Line: line,
					Err:  errors.New(parts[2]),
				}
			}
		}
	}
	return ParseError{Line: 1, Err: err}
}