mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix go template parsing (#97145)
Co-authored-by: Sonia Aguilar <sonia.aguilar@grafana.com> Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
parent
59d4b91e4c
commit
5e5fa86b8b
@ -0,0 +1,241 @@
|
||||
import { parseTemplates } from './utils';
|
||||
|
||||
describe('parseTemplates', () => {
|
||||
it('should parse basic template', () => {
|
||||
const templates = parseTemplates('{{ define "test" }}test{{ end }}');
|
||||
expect(templates).toEqual([{ name: 'test', content: '{{ define "test" }}test{{ end }}' }]);
|
||||
});
|
||||
|
||||
it('should parse templates with multiple conditions', () => {
|
||||
const originalTemplate = `
|
||||
{{ define "slack.title" }}{{ .CommonLabels.alertname -}}
|
||||
| [ACTIVE:{{ .Alerts.Firing | len }}{{
|
||||
if gt (.Alerts.Resolved | len) 0 }}, RESOLVED:{{ .Alerts.Resolved | len }}{{ end }}] | CRI:{{.CommonLabels.criticality}} IMP:{{.CommonLabels.impact}} {{
|
||||
if eq .CommonLabels.impact "4"}}:warning:{{end}}{{
|
||||
if eq .CommonLabels.impact "5"}}:bangbang:{{end}}{{
|
||||
if match "6|7|8" .CommonLabels.criticality}}:fire:{{end}}
|
||||
{{ end -}}
|
||||
`.trim();
|
||||
|
||||
const [template] = parseTemplates(originalTemplate);
|
||||
|
||||
expect(template).toBeDefined();
|
||||
expect(template.name).toEqual('slack.title');
|
||||
expect(template.content).toBe(originalTemplate);
|
||||
});
|
||||
|
||||
it('should parse templates with unusual formatting', () => {
|
||||
const originalTemplate = `
|
||||
{{ define "slack.title.small" }}{{ .CommonLabels.alertname -}}
|
||||
{{
|
||||
if match "6|7|8" .CommonLabels.criticality}}:fire:{{end}}
|
||||
{{ end -}}`.trim();
|
||||
|
||||
const [template] = parseTemplates(originalTemplate);
|
||||
|
||||
expect(template).toBeDefined();
|
||||
expect(template.name).toEqual('slack.title.small');
|
||||
expect(template.content).toBe(originalTemplate);
|
||||
});
|
||||
|
||||
it('should parse templates with nested templates', () => {
|
||||
const originalTemplate = `
|
||||
{{ define "nested" }}
|
||||
Main Template Content
|
||||
{{ template "sub1" }}
|
||||
{{ template "sub2" }}
|
||||
{{ end }}
|
||||
{{ define "sub1" }}
|
||||
Sub Template 1 Content
|
||||
{{ end }}
|
||||
{{ define "sub2" }}
|
||||
Sub Template 2 Content
|
||||
{{ end }}`.trim();
|
||||
|
||||
const [template] = parseTemplates(originalTemplate);
|
||||
|
||||
expect(template).toBeDefined();
|
||||
|
||||
expect(template.name).toEqual('nested');
|
||||
expect(template.content).toBe(originalTemplate);
|
||||
});
|
||||
|
||||
it('should parse multiple unrelated templates as separate ones', () => {
|
||||
const originalTemplate = `
|
||||
{{ define "template1" }}template1{{ end }}
|
||||
{{ define "template2" }}
|
||||
{{ .CommonLabels.alertname -}}
|
||||
{{ end }}
|
||||
{{ define "template3" }}
|
||||
{{if eq .CommonLabels.impact "5"}}:bangbang:{{end}}
|
||||
{{ end }}
|
||||
`.trim();
|
||||
|
||||
const templates = parseTemplates(originalTemplate);
|
||||
|
||||
expect(templates).toHaveLength(3);
|
||||
|
||||
const [template1, template2, template3] = templates;
|
||||
|
||||
expect(template1.name).toEqual('template1');
|
||||
expect(template2.name).toEqual('template2');
|
||||
expect(template3.name).toEqual('template3');
|
||||
|
||||
expect(template1.content).toBe('{{ define "template1" }}template1{{ end }}');
|
||||
expect(template2.content).toBe(
|
||||
`
|
||||
{{ define "template2" }}
|
||||
{{ .CommonLabels.alertname -}}
|
||||
{{ end }}`.trim()
|
||||
);
|
||||
expect(template3.content).toBe(
|
||||
`
|
||||
{{ define "template3" }}
|
||||
{{if eq .CommonLabels.impact "5"}}:bangbang:{{end}}
|
||||
{{ end }}`.trim()
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse mixed nested and non-nested templates', () => {
|
||||
const originalTemplate = `
|
||||
{{ define "parent" }}
|
||||
{{ template "nested" }}
|
||||
{{ end }}
|
||||
{{ define "top-level" }}
|
||||
Top Level Template Content
|
||||
{{ end }}
|
||||
{{ define "nested" }}
|
||||
Nested Template Content
|
||||
{{ end }}`.trim();
|
||||
|
||||
const templates = parseTemplates(originalTemplate);
|
||||
|
||||
expect(templates).toHaveLength(2);
|
||||
|
||||
const [parent, topLevel] = templates;
|
||||
|
||||
expect(parent.name).toEqual('parent');
|
||||
expect(topLevel.name).toEqual('top-level');
|
||||
|
||||
expect(parent.content).toBe(
|
||||
`
|
||||
{{ define "parent" }}
|
||||
{{ template "nested" }}
|
||||
{{ end }}
|
||||
{{ define "nested" }}
|
||||
Nested Template Content
|
||||
{{ end }}`.trim()
|
||||
);
|
||||
expect(topLevel.content).toBe(
|
||||
`
|
||||
{{ define "top-level" }}
|
||||
Top Level Template Content
|
||||
{{ end }}`.trim()
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle templates with block definitions', () => {
|
||||
const template = `
|
||||
{{ define "blocks" }}
|
||||
{{ block "header" . }}
|
||||
default header
|
||||
{{ end }}
|
||||
{{ block "body" . }}
|
||||
default body
|
||||
{{ end }}
|
||||
{{ end }}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
|
||||
it('should handle templates with pipeline operations', () => {
|
||||
const template = `
|
||||
{{ define "pipeline" }}
|
||||
{{ .Value | printf "%.2f" | quote }}
|
||||
{{ .Items | join "," | upper | trim }}
|
||||
{{ end }}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
|
||||
it('should handle templates with variable declarations and scope', () => {
|
||||
const template = `
|
||||
{{ define "variables" }}
|
||||
{{- $var1 := "value" -}}
|
||||
{{- with .Items -}}
|
||||
{{- $var2 := . -}}
|
||||
{{- range . -}}
|
||||
{{- $var3 := . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{ end }}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
|
||||
it('should handle templates with whitespace control modifiers', () => {
|
||||
const template = `
|
||||
{{- define "whitespace" -}}
|
||||
{{- if .Value -}}
|
||||
has-value
|
||||
{{- else -}}
|
||||
no-value
|
||||
{{- end -}}
|
||||
{{- end -}}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
|
||||
it('should handle templates with complex nested actions', () => {
|
||||
const template = `
|
||||
{{ define "complex" }}
|
||||
{{- range $i, $v := .Items -}}
|
||||
{{- if not $v.Hidden -}}
|
||||
{{- with $v -}}
|
||||
{{- template "item" . -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- /* skip hidden items */ -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{ end }}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
|
||||
it('should handle templates with Go template comments', () => {
|
||||
const template = `
|
||||
{{ define "comments" }}
|
||||
{{/* single-line comment */}}
|
||||
{{ printf "%q" "text" }}
|
||||
{{- /*
|
||||
multi-line
|
||||
comment
|
||||
*/ -}}
|
||||
{{ end }}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
|
||||
it('should handle templates with function calls and method chaining', () => {
|
||||
const template = `
|
||||
{{ define "functions" }}
|
||||
{{ call .Func .Arg1 .Arg2 }}
|
||||
{{ .Value.Method1.Method2 "arg" }}
|
||||
{{ index .Items 1 2 "key" }}
|
||||
{{ end }}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
|
||||
it('should handle templates with complex boolean logic', () => {
|
||||
const template = `
|
||||
{{ define "logic" }}
|
||||
{{- if and (not .Hidden) (or (gt .Value 100) (lt .Value 0)) (has .Flags "important") -}}
|
||||
special-case
|
||||
{{- end -}}
|
||||
{{ end }}`.trim();
|
||||
const [parsed] = parseTemplates(template);
|
||||
expect(parsed.content).toBe(template);
|
||||
});
|
||||
});
|
@ -20,13 +20,13 @@ import { Template } from './TemplateSelector';
|
||||
export function parseTemplates(templatesString: string): Template[] {
|
||||
const templates: Record<string, Template> = {};
|
||||
const stack: Array<{ type: string; startIndex: number; name?: string }> = [];
|
||||
const regex = /{{(-?\s*)(define|end|if|range|else|with|template)(\s*.*?)?(-?\s*)}}/gs;
|
||||
const regex = /{{-?\s*(define|end|if|range|else|with|template|block)\b(.*?)-?}}/gs;
|
||||
|
||||
let match;
|
||||
let currentIndex = 0;
|
||||
|
||||
while ((match = regex.exec(templatesString)) !== null) {
|
||||
const [, , keyword, middleContent] = match;
|
||||
const [, keyword, middleContent] = match;
|
||||
currentIndex = match.index;
|
||||
|
||||
if (keyword === 'define') {
|
||||
@ -36,7 +36,14 @@ export function parseTemplates(templatesString: string): Template[] {
|
||||
}
|
||||
} else if (keyword === 'end') {
|
||||
let top = stack.pop();
|
||||
while (top && top.type !== 'define' && top.type !== 'if' && top.type !== 'range' && top.type !== 'with') {
|
||||
while (
|
||||
top &&
|
||||
top.type !== 'define' &&
|
||||
top.type !== 'if' &&
|
||||
top.type !== 'range' &&
|
||||
top.type !== 'with' &&
|
||||
top.type !== 'block'
|
||||
) {
|
||||
top = stack.pop();
|
||||
}
|
||||
if (top) {
|
||||
@ -48,7 +55,13 @@ export function parseTemplates(templatesString: string): Template[] {
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (keyword === 'if' || keyword === 'range' || keyword === 'else' || keyword === 'with') {
|
||||
} else if (
|
||||
keyword === 'if' ||
|
||||
keyword === 'range' ||
|
||||
keyword === 'else' ||
|
||||
keyword === 'with' ||
|
||||
keyword === 'block'
|
||||
) {
|
||||
stack.push({ type: keyword, startIndex: currentIndex });
|
||||
}
|
||||
}
|
||||
@ -78,7 +91,7 @@ export function getTemplateName(useTemplateText: string) {
|
||||
}
|
||||
|
||||
/* This function checks if the a field value contains only one template usage
|
||||
for example:
|
||||
for example:
|
||||
"{{ template "templateName" . }}"" returns true
|
||||
but "{{ template "templateName" . }} some text {{ template "templateName" . }}"" returns false
|
||||
and "{{ template "templateName" . }} some text" some text returns false
|
||||
|
Loading…
Reference in New Issue
Block a user