From 3cc8a753dc5c1d034206bc3b9c926f85cac80e0e Mon Sep 17 00:00:00 2001
From: Lukasz Mierzwa <lukasz@cloudflare.com>
Date: Thu, 17 Jun 2021 13:51:21 +0100
Subject: [PATCH] Workaround new line parsing issues in go-yaml

See #20.
---
 cmd/pint/tests/0012_issue_20.txt |  64 +++++++++++++++++
 internal/parser/models.go        |   2 +-
 internal/parser/parser.go        |   4 +-
 internal/parser/parser_test.go   | 117 +++++++++++++++++++++++++++++++
 internal/reporter/console.go     |   9 ++-
 5 files changed, 192 insertions(+), 4 deletions(-)
 create mode 100644 cmd/pint/tests/0012_issue_20.txt

diff --git a/cmd/pint/tests/0012_issue_20.txt b/cmd/pint/tests/0012_issue_20.txt
new file mode 100644
index 00000000..69952beb
--- /dev/null
+++ b/cmd/pint/tests/0012_issue_20.txt
@@ -0,0 +1,64 @@
+pint.ok 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.yaml rules=1
+level=warn msg="Tried to read more lines than present in the source file, this is likely due to '\n' usage in some rules, see https://github.com/cloudflare/pint/issues/20 for details" path=rules/1.yaml
+rules/1.yaml:9-13: runbook_url annotation is required (alerts/annotation)
+        annotations:
+          summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})"
+          description: "Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}"
+
+-- rules/1.yaml --
+groups:
+  - name: "haproxy.api_server.rules"
+    rules:
+      - alert: HaproxyServerHealthcheckFailure
+        expr: increase(haproxy_server_check_failures_total[15m]) > 100
+        for: 5m
+        labels:
+          severity: 24x7
+        annotations:
+          summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})"
+          description: "Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}"
+-- .pint.hcl --
+rule {
+  match {
+    kind = "alerting"
+  }
+  # Each alert must have a 'severity' annotation that's either '24x7','10x5' or 'debug'.
+  label "severity" {
+    severity = "bug"
+    value    = "(24x7|10x5|debug)"
+    required = true
+  }
+  annotation "runbook_url" {
+    severity = "warning"
+    required = true
+  }
+}
+
+rule {
+  # Disallow spaces in label/annotation keys, they're only allowed in values.
+  reject ".* +.*" {
+    label_keys      = true
+    annotation_keys = true
+  }
+
+  # Disallow URLs in labels, they should go to annotations.
+  reject "https?://.+" {
+    label_keys   = true
+    label_values = true
+  }
+  # Check how many times each alert would fire in the last 1d.
+  alerts {
+    range   = "1d"
+    step    = "1m"
+    resolve = "5m"
+  }
+  # Check if '{{ $value }}'/'{{ .Value }}' is used in labels
+  # https://www.robustperception.io/dont-put-the-value-in-alert-labels
+  value {}
+}
diff --git a/internal/parser/models.go b/internal/parser/models.go
index d2127ff9..d4dc82ec 100644
--- a/internal/parser/models.go
+++ b/internal/parser/models.go
@@ -51,7 +51,7 @@ type FilePosition struct {
 	Lines []int
 }
 
-func (fp FilePosition) FistLine() (line int) {
+func (fp FilePosition) FirstLine() (line int) {
 	for _, l := range fp.Lines {
 		if line == 0 || l < line {
 			line = l
diff --git a/internal/parser/parser.go b/internal/parser/parser.go
index 143aadfa..72092fb6 100644
--- a/internal/parser/parser.go
+++ b/internal/parser/parser.go
@@ -123,9 +123,9 @@ func parseRule(content []byte, node *yaml.Node) (rule Rule, isEmpty bool, err er
 		}
 	}
 
-	if exprPart != nil && exprPart.Key.Position.FistLine() != exprPart.Value.Position.FistLine() {
+	if exprPart != nil && exprPart.Key.Position.FirstLine() != exprPart.Value.Position.FirstLine() {
 		for {
-			start := exprPart.Value.Position.FistLine() - 1
+			start := exprPart.Value.Position.FirstLine() - 1
 			end := exprPart.Value.Position.LastLine()
 			input := strings.Join(strings.Split(string(content), "\n")[start:end], "")
 			input = strings.ReplaceAll(input, " ", "")
diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go
index 0597280a..9298a9f9 100644
--- a/internal/parser/parser_test.go
+++ b/internal/parser/parser_test.go
@@ -613,6 +613,123 @@ data:
 `),
 			output: nil,
 		},
+		/*
+					FIXME https://github.com/cloudflare/pint/issues/20
+					{
+						content: []byte(`groups:
+			  - name: "haproxy.api_server.rules"
+			    rules:
+			      - alert: HaproxyServerHealthcheckFailure
+			        expr: increase(haproxy_server_check_failures_total[15m]) > 100
+			        for: 5m
+			        labels:
+			          severity: 24x7
+			        annotations:
+			          summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})"
+			          description: "Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}"
+			`),
+						output: []parser.Rule{
+							{
+								AlertingRule: &parser.AlertingRule{
+									Alert: parser.YamlKeyValue{
+										Key: &parser.YamlNode{
+											Position: parser.FilePosition{Lines: []int{4}},
+											Value:    "alert",
+										},
+										Value: &parser.YamlNode{
+											Position: parser.FilePosition{Lines: []int{4}},
+											Value:    "HaproxyServerHealthcheckFailure",
+										},
+									},
+									Expr: parser.PromQLExpr{
+										Key: &parser.YamlNode{
+											Position: parser.FilePosition{Lines: []int{5}},
+											Value:    "expr",
+										},
+										Value: &parser.YamlNode{
+											Position: parser.FilePosition{Lines: []int{5}},
+											Value:    "increase(haproxy_server_check_failures_total[15m]) > 100",
+										},
+										Query: &parser.PromQLNode{
+											Expr: "increase(haproxy_server_check_failures_total[15m]) > 100",
+											Children: []*parser.PromQLNode{
+												{
+													Expr: "increase(haproxy_server_check_failures_total[15m])",
+													Children: []*parser.PromQLNode{
+														{
+															Expr: "haproxy_server_check_failures_total[15m]",
+															Children: []*parser.PromQLNode{
+																{
+																	Expr: "haproxy_server_check_failures_total",
+																},
+															},
+														},
+													},
+												},
+												{Expr: "100"},
+											},
+										},
+									},
+									For: &parser.YamlKeyValue{
+										Key: &parser.YamlNode{
+											Position: parser.FilePosition{Lines: []int{6}},
+											Value:    "for",
+										},
+										Value: &parser.YamlNode{Position: parser.FilePosition{Lines: []int{6}},
+											Value: "5m",
+										},
+									},
+									Labels: &parser.YamlMap{
+										Key: &parser.YamlNode{
+											Position: parser.FilePosition{Lines: []int{7}},
+											Value:    "labels",
+										},
+										Items: []*parser.YamlKeyValue{
+											{
+												Key: &parser.YamlNode{
+													Position: parser.FilePosition{Lines: []int{8}},
+													Value:    "severity",
+												},
+												Value: &parser.YamlNode{
+													Position: parser.FilePosition{Lines: []int{8}},
+													Value:    "24x7",
+												},
+											},
+										},
+									},
+									Annotations: &parser.YamlMap{
+										Key: &parser.YamlNode{
+											Position: parser.FilePosition{Lines: []int{9}},
+											Value:    "annotations",
+										},
+										Items: []*parser.YamlKeyValue{
+											{
+												Key: &parser.YamlNode{
+													Position: parser.FilePosition{Lines: []int{10}},
+													Value:    "summary",
+												},
+												Value: &parser.YamlNode{
+													Position: parser.FilePosition{Lines: []int{10}},
+													Value:    "HAProxy server healthcheck failure (instance {{ $labels.instance }})",
+												},
+											},
+											{
+												Key: &parser.YamlNode{
+													Position: parser.FilePosition{Lines: []int{11}},
+													Value:    "description",
+												},
+												Value: &parser.YamlNode{
+													Position: parser.FilePosition{Lines: []int{11}},
+													Value:    `Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}`,
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+		*/
 	}
 
 	alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
diff --git a/internal/reporter/console.go b/internal/reporter/console.go
index dbd67b3b..c8b16402 100644
--- a/internal/reporter/console.go
+++ b/internal/reporter/console.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/cloudflare/pint/internal/checks"
 	"github.com/cloudflare/pint/internal/output"
+	"github.com/rs/zerolog/log"
 )
 
 func NewConsoleReporter(output io.Writer) ConsoleReporter {
@@ -68,7 +69,13 @@ func (cr ConsoleReporter) Submit(summary Summary) error {
 			msg = append(msg, output.MakeGray(report.Problem.Text))
 		}
 		msg = append(msg, output.MakeMagneta(" (%s)\n", report.Problem.Reporter))
-		for _, c := range strings.Split(content, "\n")[firstLine-1 : lastLine] {
+
+		lines := strings.Split(content, "\n")
+		if lastLine > len(lines)-1 {
+			lastLine = len(lines) - 1
+			log.Warn().Str("path", report.Path).Msgf("Tried to read more lines than present in the source file, this is likely due to '\n' usage in some rules, see https://github.com/cloudflare/pint/issues/20 for details")
+		}
+		for _, c := range lines[firstLine-1 : lastLine] {
 			msg = append(msg, output.MakeWhite("%s\n", c))
 		}
 		perFile[report.Path] = append(perFile[report.Path], strings.Join(msg, ""))