Skip to content

Commit

Permalink
More relaxed parsing of k8s manifests
Browse files Browse the repository at this point in the history
  • Loading branch information
prymitive committed May 24, 2022
1 parent 3c5bc88 commit 07e8489
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 39 deletions.
77 changes: 77 additions & 0 deletions cmd/pint/tests/0073_lint_k8s.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
pint.error --no-color lint rules
! stdout .
cmp stderr stderr.txt

-- stderr.txt --
level=info msg="Loading configuration file" path=.pint.hcl
level=info msg="File parsed" path=rules/1.yml rules=4
rules/1.yml:22-23: summary annotation is required (alerts/annotation)
- alert: Example_High_Restart_Rate
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )

rules/1.yml:22-23: priority label is required (rule/label)
- alert: Example_High_Restart_Rate
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )

rules/1.yml:24-25: summary annotation is required (alerts/annotation)
- alert: Invalid Query
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m]) / x

rules/1.yml:24-25: priority label is required (rule/label)
- alert: Invalid Query
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m]) / x

rules/1.yml:25: syntax error: no arguments for aggregate expression provided (promql/syntax)
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m]) / x

rules/1.yml:28: duplicated expr key (yaml/parse)
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )

level=info msg="Problems found" Bug=4 Fatal=2
level=fatal msg="Fatal error" error="problems found"
-- rules/1.yml --
---
kind: ConfigMap
apiVersion: v1
metadata:
name: example-app-alerts
labels:
app: example-app
data:
alerts: |
groups:
- name: example-app-alerts
rules:
- alert: Example_Is_Down
expr: kube_deployment_status_replicas_available{namespace="example-app"} < 1
for: 5m
labels:
priority: "2"
environment: production
annotations:
summary: "No replicas for Example have been running for 5 minutes"

- alert: Example_High_Restart_Rate
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )
- alert: Invalid Query
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m]) / x
- alert: Duplicated Expr
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )
expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )

-- .pint.hcl --
parser {
relaxed = [".*"]
}
rule {
match { kind = "alerting" }
label "priority" {
severity = "bug"
value = "(1|2|3|4|5)"
required = true
}
annotation "summary" {
severity = "bug"
required = true
}
}
30 changes: 30 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## v0.19.0

### Added

- Parsing files in relaxed mode will now try to find rules inside multi-line strings #252.
This allows direct linting of k8s manifests like the one below:

```yaml
---
kind: ConfigMap
apiVersion: v1
metadata:
name: example-app-alerts
labels:
app: example-app
data:
alerts: |
groups:
- name: example-app-alerts
rules:
- alert: Example_Is_Down
expr: kube_deployment_status_replicas_available{namespace="example-app"} < 1
for: 5m
labels:
priority: "2"
environment: production
annotations:
summary: "No replicas for Example have been running for 5 minutes"
```
## v0.18.1
### Fixed
Expand Down
34 changes: 17 additions & 17 deletions internal/parser/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ func appendLine(lines []int, newLines ...int) []int {
return lines
}

func nodeLines(node *yaml.Node) (lines []int) {
func nodeLines(node *yaml.Node, offset int) (lines []int) {
lineCount := len(strings.Split(strings.TrimSuffix(node.Value, "\n"), "\n"))

var firstLine int
switch node.Style {
case yaml.LiteralStyle, yaml.FoldedStyle:
firstLine = node.Line + 1
firstLine = node.Line + 1 + offset
default:
firstLine = node.Line
firstLine = node.Line + offset
}

for i := 0; i < lineCount; i++ {
Expand Down Expand Up @@ -88,26 +88,26 @@ type YamlNode struct {
Comments []string
}

func newYamlNode(node *yaml.Node) *YamlNode {
func newYamlNode(node *yaml.Node, offset int) *YamlNode {
return &YamlNode{
Position: NewFilePosition(nodeLines(node)),
Position: NewFilePosition(nodeLines(node, offset)),
Value: node.Value,
Comments: mergeComments(node),
}
}

func newYamlNodeWithParent(parent, node *yaml.Node) *YamlNode {
func newYamlNodeWithParent(parent, node *yaml.Node, offset int) *YamlNode {
return &YamlNode{
Position: NewFilePosition(nodeLines(node)),
Position: NewFilePosition(nodeLines(node, offset)),
Value: node.Value,
Comments: mergeComments(node),
}
}

func newYamlKeyValue(key, val *yaml.Node) *YamlKeyValue {
func newYamlKeyValue(key, val *yaml.Node, offset int) *YamlKeyValue {
return &YamlKeyValue{
Key: newYamlNode(key),
Value: newYamlNodeWithParent(key, val),
Key: newYamlNode(key, offset),
Value: newYamlNodeWithParent(key, val, offset),
}
}

Expand Down Expand Up @@ -144,17 +144,17 @@ func (ym YamlMap) GetValue(key string) *YamlNode {
return nil
}

func newYamlMap(key, value *yaml.Node) *YamlMap {
func newYamlMap(key, value *yaml.Node, offset int) *YamlMap {
ym := YamlMap{
Key: newYamlNode(key),
Key: newYamlNode(key, offset),
}

var ckey *yaml.Node
for _, child := range value.Content {
if ckey != nil {
kv := YamlKeyValue{
Key: newYamlNode(ckey),
Value: newYamlNode(child),
Key: newYamlNode(ckey, offset),
Value: newYamlNode(child, offset),
}
ym.Items = append(ym.Items, &kv)
ckey = nil
Expand Down Expand Up @@ -202,10 +202,10 @@ func (pqle PromQLExpr) Lines() (lines []int) {
return
}

func newPromQLExpr(key, val *yaml.Node) *PromQLExpr {
func newPromQLExpr(key, val *yaml.Node, offset int) *PromQLExpr {
expr := PromQLExpr{
Key: newYamlNode(key),
Value: newYamlNodeWithParent(key, val),
Key: newYamlNode(key, offset),
Value: newYamlNodeWithParent(key, val, offset),
}

qlNode, err := DecodeExpr(val.Value)
Expand Down
55 changes: 34 additions & 21 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ func (p Parser) Parse(content []byte) (rules []Rule, err error) {
return nil, err
}

return parseNode(content, &node)
return parseNode(content, &node, 0)
}

func parseNode(content []byte, node *yaml.Node) (rules []Rule, err error) {
ret, isEmpty, err := parseRule(content, node)
func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule, err error) {
ret, isEmpty, err := parseRule(content, node, offset)
if err != nil {
return nil, err
}
Expand All @@ -56,22 +56,35 @@ func parseNode(content []byte, node *yaml.Node) (rules []Rule, err error) {
switch root.Kind {
case yaml.SequenceNode:
for _, n := range root.Content {
ret, err := parseNode(content, n)
ret, err := parseNode(content, n, offset)
if err != nil {
return nil, err
}
rules = append(rules, ret...)
}
case yaml.MappingNode:
rule, isEmpty, err := parseRule(content, root)
rule, isEmpty, err := parseRule(content, root, offset)
if err != nil {
return nil, err
}
if !isEmpty {
rules = append(rules, rule)
} else {
for _, n := range root.Content {
ret, err := parseNode(content, n)
ret, err := parseNode(content, n, offset)
if err != nil {
return nil, err
}
rules = append(rules, ret...)
}
}
case yaml.ScalarNode:
if root.Value != string(content) {
c := []byte(root.Value)
var n yaml.Node
err = yaml.Unmarshal(c, &n)
if err == nil {
ret, err := parseNode(c, &n, offset+root.Line)
if err != nil {
return nil, err
}
Expand All @@ -83,7 +96,7 @@ func parseNode(content []byte, node *yaml.Node) (rules []Rule, err error) {
return rules, nil
}

func parseRule(content []byte, node *yaml.Node) (rule Rule, isEmpty bool, err error) {
func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, isEmpty bool, err error) {
isEmpty = true

if node.Kind != yaml.MappingNode {
Expand Down Expand Up @@ -113,34 +126,34 @@ func parseRule(content []byte, node *yaml.Node) (rule Rule, isEmpty bool, err er
switch key.Value {
case recordKey:
if recordPart != nil {
return duplicatedKeyError(part.Line, recordKey, nil)
return duplicatedKeyError(part.Line+offset, recordKey, nil)
}
recordPart = newYamlKeyValue(key, part)
recordPart = newYamlKeyValue(key, part, offset)
case alertKey:
if alertPart != nil {
return duplicatedKeyError(part.Line, alertKey, nil)
return duplicatedKeyError(part.Line+offset, alertKey, nil)
}
alertPart = newYamlKeyValue(key, part)
alertPart = newYamlKeyValue(key, part, offset)
case exprKey:
if exprPart != nil {
return duplicatedKeyError(part.Line, exprKey, nil)
return duplicatedKeyError(part.Line+offset, exprKey, nil)
}
exprPart = newPromQLExpr(key, part)
exprPart = newPromQLExpr(key, part, offset)
case forKey:
if forPart != nil {
return duplicatedKeyError(part.Line, forKey, nil)
return duplicatedKeyError(part.Line+offset, forKey, nil)
}
forPart = newYamlKeyValue(key, part)
forPart = newYamlKeyValue(key, part, offset)
case labelsKey:
if labelsPart != nil {
return duplicatedKeyError(part.Line, labelsKey, nil)
return duplicatedKeyError(part.Line+offset, labelsKey, nil)
}
labelsPart = newYamlMap(key, part)
labelsPart = newYamlMap(key, part, offset)
case annotationsKey:
if annotationsPart != nil {
return duplicatedKeyError(part.Line, annotationsKey, nil)
return duplicatedKeyError(part.Line+offset, annotationsKey, nil)
}
annotationsPart = newYamlMap(key, part)
annotationsPart = newYamlMap(key, part, offset)
default:
unknownKeys = append(unknownKeys, key)
}
Expand Down Expand Up @@ -172,7 +185,7 @@ func parseRule(content []byte, node *yaml.Node) (rule Rule, isEmpty bool, err er
isEmpty = false
rule = Rule{
Error: ParseError{
Line: node.Line,
Line: node.Line + offset,
Err: fmt.Errorf("got both %s and %s keys in a single rule", recordKey, alertKey),
},
}
Expand Down Expand Up @@ -216,7 +229,7 @@ func parseRule(content []byte, node *yaml.Node) (rule Rule, isEmpty bool, err er
}
rule = Rule{
Error: ParseError{
Line: unknownKeys[0].Line,
Line: unknownKeys[0].Line + offset,
Err: fmt.Errorf("invalid key(s) found: %s", strings.Join(keys, ", ")),
},
}
Expand Down
Loading

0 comments on commit 07e8489

Please sign in to comment.