Skip to content

Commit 88d310e

Browse files
committed
Rewrote the escaping logic - hopefully better readable
Signed-off-by: Jurrie Overgoor <[email protected]>
1 parent 0bfaf28 commit 88d310e

File tree

4 files changed

+151
-151
lines changed

4 files changed

+151
-151
lines changed

jkube-kit/common/src/main/java/org/eclipse/jkube/kit/common/util/TemplateUtil.java

Lines changed: 92 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,32 @@
1313
*/
1414
package org.eclipse.jkube.kit.common.util;
1515

16+
import java.lang.invoke.MethodHandles;
17+
import java.util.concurrent.atomic.AtomicLong;
1618
import java.util.regex.Matcher;
1719
import java.util.regex.Pattern;
1820

21+
import org.apache.commons.lang3.StringUtils;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
1925
public class TemplateUtil {
20-
private static final String HELM_DIRECTIVE_REGEX = "\\{\\{.*\\}\\}";
26+
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
27+
28+
private static final String HELM_END_TAG = "}}";
29+
private static final String HELM_START_TAG = "{{";
30+
31+
private static final String YAML_HELM_ESCAPE_KEY = "escapedHelm";
32+
private static final String YAML_LIST_TAG = "-";
33+
private static final String YAML_KEY_VALUE_SEPARATOR = ": ";
2134

2235
private TemplateUtil() {
2336
}
2437

2538
/**
26-
* This function will replace all Helm directives with a valid Yaml line containing the base64 encoded Helm directive.
39+
* This function will replace all single line Helm directives with a valid Yaml line containing the base64 encoded Helm
40+
* directive.
41+
* Helm directives that are values (so not a full line) will be left alone.
2742
*
2843
* Helm lines that are by themselves will be converted like so:
2944
* <br/>
@@ -32,17 +47,17 @@ private TemplateUtil() {
3247
* <pre>
3348
* {{- $myDate := .Value.date }}
3449
* {{ include "something" . }}
50+
* myKey: "Regular line"
3551
* someKey: {{ a bit of Helm }}
36-
* someOtherKey: {{ another bit of Helm }}
3752
* </pre>
3853
*
3954
* Output:
4055
*
4156
* <pre>
42-
* escapedHelm0: BASE64STRINGOFCHARACTERS=
43-
* escapedHelm1: ANOTHERBASE64STRING=
44-
* someKey: escapedHelmValueBASE64STRING==
45-
* someOtherKey: escapedHelmValueBASE64STRING
57+
* escapedHelm0: e3stICA6PSAuVmFsdWUuZGF0ZSB9fQ==
58+
* escapedHelm1: e3sgaW5jbHVkZSBzb21ldGhpbmcgLiB9fQ==
59+
* myKey: "Regular line"
60+
* someKey: {{ a bit of Helm }}
4661
* </pre>
4762
*
4863
* The <strong>escapedHelm</strong> and <strong>escapedHelmValue</strong> flags are needed for unescaping.
@@ -52,7 +67,39 @@ private TemplateUtil() {
5267
* @see #unescapeYamlTemplate(String)
5368
*/
5469
public static String escapeYamlTemplate(final String yaml) {
55-
return escapeYamlTemplateLines(escapeYamlTemplateValues(yaml));
70+
final AtomicLong helmEscapeIndex = new AtomicLong();
71+
return iterateOverLines(yaml,
72+
(line, lineEnding, lineNumber) -> escapeYamlLine(line, lineNumber, helmEscapeIndex) + lineEnding);
73+
}
74+
75+
private static String escapeYamlLine(final String line, final long lineNumber, final AtomicLong helmEscapeIndex) {
76+
if (line.trim().startsWith(HELM_START_TAG)) {
77+
// Line starts with optional indenting and then '{{', so replace whole line
78+
final int helmStartCount = StringUtils.countMatches(line, HELM_START_TAG);
79+
final int helmEndCount = StringUtils.countMatches(line, HELM_END_TAG);
80+
if (helmStartCount != helmEndCount) {
81+
LOG.warn("Found {} Helm start tag{} ('{}') but {} end tag{} ('{}') on line {}. "
82+
+ "Expected this to be equal! Note that multi-line Helm directives are not supported.",
83+
helmStartCount, helmStartCount == 1 ? "" : "s", HELM_START_TAG,
84+
helmEndCount, helmEndCount == 1 ? "" : "s", HELM_END_TAG,
85+
lineNumber);
86+
}
87+
88+
final int startOfHelm = line.indexOf(HELM_START_TAG);
89+
final String indentation = line.substring(0, startOfHelm);
90+
final String base64EncodedLine = Base64Util.encodeToString(line.substring(startOfHelm));
91+
final long currentHelmEscapeIndex = helmEscapeIndex.getAndIncrement();
92+
return indentation + YAML_HELM_ESCAPE_KEY + currentHelmEscapeIndex + YAML_KEY_VALUE_SEPARATOR + base64EncodedLine;
93+
} else if (line.trim().startsWith(YAML_LIST_TAG)) {
94+
// Line starts with optional indenting and then '-'. Strip the '-', parse again, readd the '-'.
95+
final int startOfRestOfLine = line.indexOf(YAML_LIST_TAG) + YAML_LIST_TAG.length();
96+
final String prefix = line.substring(0, startOfRestOfLine);
97+
final String restOfLine = line.substring(startOfRestOfLine);
98+
return prefix + escapeYamlLine(restOfLine, lineNumber, helmEscapeIndex);
99+
} else {
100+
// Line was a regular line
101+
return line;
102+
}
56103
}
57104

58105
/**
@@ -64,101 +111,51 @@ public static String escapeYamlTemplate(final String yaml) {
64111
* @return the Yaml that was originally provided to {@link #escapeYamlTemplate(String)}
65112
* @see #escapeYamlTemplate(String)
66113
*/
67-
public static String unescapeYamlTemplate(final String template) {
68-
return unescapeYamlTemplateLines(unescapeYamlTemplateValues(template));
114+
public static String unescapeYamlTemplate(final String yaml) {
115+
return iterateOverLines(yaml, (line, lineEnding, lineNumber) -> unescapeYamlLine(line) + lineEnding);
69116
}
70117

71-
/**
72-
* This function is responsible for escaping the Helm directives that are stand-alone.
73-
* For example:
74-
*
75-
* <pre>
76-
* {{ include "something" . }}
77-
* </pre>
78-
*
79-
* @see #unescapeYamlTemplateLines(String)
80-
*/
81-
private static String escapeYamlTemplateLines(String template) {
82-
long escapedHelmIndex = 0;
83-
final Pattern compile = Pattern.compile("^( *-? *)(" + HELM_DIRECTIVE_REGEX + ".*)$", Pattern.MULTILINE);
84-
Matcher matcher = compile.matcher(template);
85-
while (matcher.find()) {
86-
final String indentation = matcher.group(1);
87-
final String base64Line = Base64Util.encodeToString(matcher.group(2));
88-
template = matcher.replaceFirst(indentation + "escapedHelm" + escapedHelmIndex + ": " + base64Line);
89-
matcher = compile.matcher(template);
90-
escapedHelmIndex++;
118+
private static String unescapeYamlLine(final String line) {
119+
if (line.trim().startsWith(YAML_HELM_ESCAPE_KEY)) {
120+
// Line starts with optional indenting and then 'escapedHelm', so replace whole line
121+
final int endOfIndentation = line.indexOf(YAML_HELM_ESCAPE_KEY);
122+
final int startOfBase64Helm = line.indexOf(YAML_KEY_VALUE_SEPARATOR) + YAML_KEY_VALUE_SEPARATOR.length();
123+
final String indentation = line.substring(0, endOfIndentation);
124+
final String base64DecodedLine = Base64Util.decodeToString(line.substring(startOfBase64Helm));
125+
return indentation + base64DecodedLine;
126+
} else if (line.trim().startsWith(YAML_LIST_TAG)) {
127+
// Line starts with optional indenting and then '-'. Strip the '-', parse again, readd the '-'.
128+
final int startOfRestOfLine = line.indexOf(YAML_LIST_TAG) + YAML_LIST_TAG.length();
129+
final String prefix = line.substring(0, startOfRestOfLine);
130+
final String restOfLine = line.substring(startOfRestOfLine);
131+
return prefix + unescapeYamlLine(restOfLine);
132+
} else {
133+
// Line was a regular line
134+
return line;
91135
}
92-
return template;
93136
}
94137

95-
/**
96-
* This function is responsible for reinstating the stand-alone Helm directives.
97-
* For example:
98-
*
99-
* <pre>
100-
* BASE64STRINGOFCHARACTERS=
101-
* </pre>
102-
*
103-
* It is the opposite of {@link #escapeYamlTemplateLines(String)}.
104-
*
105-
* @see #escapeYamlTemplateLines(String)
106-
*/
107-
private static String unescapeYamlTemplateLines(String template) {
108-
final Pattern compile = Pattern.compile("^( *-? *)escapedHelm[\\d]+: \"?(.*?)\"?$", Pattern.MULTILINE);
109-
Matcher matcher = compile.matcher(template);
110-
while (matcher.find()) {
111-
final String indentation = matcher.group(1);
112-
final String helmLine = Base64Util.decodeToString(matcher.group(2));
113-
template = matcher.replaceFirst(indentation + helmLine.replace("$", "\\$"));
114-
matcher = compile.matcher(template);
115-
}
116-
return template;
117-
}
138+
private static String iterateOverLines(final String yaml, final IterateOverLinesCallback iterator) {
139+
final Matcher matcher = Pattern.compile("(.*)(\\R|$)").matcher(yaml);
140+
final StringBuilder result = new StringBuilder();
141+
int index = 0;
142+
long lineNumber = 0;
143+
while (matcher.find(index) && matcher.start() != matcher.end()) {
144+
final String line = matcher.group(1);
145+
final String lineEnding = matcher.group(2);
118146

119-
/**
120-
* This function is responsible for escaping the Helm directives that are Yaml values.
121-
* For example:
122-
*
123-
* <pre>
124-
* someKey: {{ a bit of Helm }}
125-
* </pre>
126-
*
127-
* @see #unescapeYamlTemplateValues(String)
128-
*/
129-
private static String escapeYamlTemplateValues(String template) {
130-
final Pattern compile = Pattern.compile("^( *[^ ]+ *): *(" + HELM_DIRECTIVE_REGEX + ".*)$", Pattern.MULTILINE);
131-
Matcher matcher = compile.matcher(template);
132-
while (matcher.find()) {
133-
final String indentation = matcher.group(1);
134-
final String base64Value = Base64Util.encodeToString(matcher.group(2));
135-
template = matcher.replaceFirst(indentation + ": escapedHelmValue" + base64Value);
136-
matcher = compile.matcher(template);
147+
final String escapedYamlLine = iterator.onLine(line, lineEnding, lineNumber);
148+
lineNumber++;
149+
150+
result.append(escapedYamlLine);
151+
152+
index = matcher.end();
137153
}
138-
return template;
154+
return result.toString();
139155
}
140156

141-
/**
142-
* This function is responsible for reinstating the Helm directives that were Yaml values.
143-
* For example:
144-
*
145-
* <pre>
146-
* someKey: escapedHelmValueBASE64STRING==
147-
* </pre>
148-
*
149-
* It is the opposite of {@link #escapeYamlTemplateValues(String)}.
150-
*
151-
* @see #escapeYamlTemplateValues(String)
152-
*/
153-
private static String unescapeYamlTemplateValues(String template) {
154-
final Pattern compile = Pattern.compile("^( *[^ ]+ *): *\"?escapedHelmValue(.*?)\"?$", Pattern.MULTILINE);
155-
Matcher matcher = compile.matcher(template);
156-
while (matcher.find()) {
157-
final String indentation = matcher.group(1);
158-
final String helmValue = Base64Util.decodeToString(matcher.group(2));
159-
template = matcher.replaceFirst(indentation + ": " + helmValue.replace("$", "\\$"));
160-
matcher = compile.matcher(template);
161-
}
162-
return template;
157+
@FunctionalInterface
158+
private interface IterateOverLinesCallback {
159+
String onLine(String input, String lineEnding, long lineNumber);
163160
}
164161
}

jkube-kit/common/src/test/java/org/eclipse/jkube/kit/common/util/TemplateUtilTest.java

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,70 +13,71 @@
1313
*/
1414
package org.eclipse.jkube.kit.common.util;
1515

16-
import org.junit.jupiter.params.ParameterizedTest;
17-
import org.junit.jupiter.params.provider.MethodSource;
18-
19-
import java.util.stream.Stream;
20-
2116
import static org.assertj.core.api.Assertions.assertThat;
2217
import static org.eclipse.jkube.kit.common.util.TemplateUtil.escapeYamlTemplate;
2318
import static org.eclipse.jkube.kit.common.util.TemplateUtil.unescapeYamlTemplate;
2419

20+
import java.util.stream.Stream;
21+
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.MethodSource;
24+
2525
class TemplateUtilTest {
2626

2727
public static Stream<Object[]> data() {
2828
return Stream.of(new Object[][] {
29-
// No Helm directive
30-
{ "abcd", "abcd" },
29+
// No Helm directive
30+
{ "abcd", "abcd" },
3131

32-
// When the Helm directive is not the first on the line
33-
{ "abc{de}f}", "abc{de}f}" },
34-
{ "abc{{de}f", "abc{{de}f" },
35-
{ "abc{{$def}}", "abc{{$def}}" },
36-
{ "abc{{de}}f", "abc{{de}}f" },
37-
{ "abc{{de}f}}", "abc{{de}f}}" },
38-
{ "abc{{def}}ghi{{jkl}}mno", "abc{{def}}ghi{{jkl}}mno" },
32+
// When the Helm directive is not the first on the line
33+
{ "abc{de}f}", "abc{de}f}" },
34+
{ "abc{{de}f", "abc{{de}f" },
35+
{ "abc{{$def}}", "abc{{$def}}" },
36+
{ "abc{{de}}f", "abc{{de}}f" },
37+
{ "abc{{de}f}}", "abc{{de}f}}" },
38+
{ "abc{{def}}ghi{{jkl}}mno", "abc{{def}}ghi{{jkl}}mno" },
3939

40-
// When the Helm directive is the first on the line
41-
{ "{de}f}", "{de}f}" },
42-
{ "{{de}f", "{{de}f" },
43-
{ "{{$def}}", "escapedHelm0: " + Base64Util.encodeToString("{{$def}}") },
44-
{ "{{de}}f", "escapedHelm0: " + Base64Util.encodeToString("{{de}}f") },
45-
{ "{{de}f}}", "escapedHelm0: " + Base64Util.encodeToString("{{de}f}}") },
46-
{ "{{def}}ghi{{jkl}}mno", "escapedHelm0: " + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
47-
{ "hello\n{{def}}\nworld", "hello\nescapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\nworld" },
48-
{ "- hello\n- {{def}}\n- world", "- hello\n- escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n- world" },
49-
{ "{{multiple}}\n{{helm}}\n{{lines}}",
50-
"escapedHelm0: " + Base64Util.encodeToString("{{multiple}}") + "\n" +
51-
"escapedHelm1: " + Base64Util.encodeToString("{{helm}}") + "\n" +
52-
"escapedHelm2: " + Base64Util.encodeToString("{{lines}}") },
40+
// When the Helm directive is the first on the line
41+
{ "{de}f}", "{de}f}" },
42+
{ "{{de}f", "escapedHelm0: " + Base64Util.encodeToString("{{de}f") },
43+
{ "{{$def}}", "escapedHelm0: " + Base64Util.encodeToString("{{$def}}") },
44+
{ "{{de}}f", "escapedHelm0: " + Base64Util.encodeToString("{{de}}f") },
45+
{ "{{de}f}}", "escapedHelm0: " + Base64Util.encodeToString("{{de}f}}") },
46+
{ "{{def}}ghi{{jkl}}mno", "escapedHelm0: " + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
47+
{ "abc{{def}}ghi{{jkl}}mno", "abc{{def}}ghi{{jkl}}mno" },
48+
{ "hello\n{{def}}\nworld", "hello\nescapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\nworld" },
49+
{ "{{multiple}}\n{{helm}}\n{{lines}}",
50+
"escapedHelm0: " + Base64Util.encodeToString("{{multiple}}") + "\n" +
51+
"escapedHelm1: " + Base64Util.encodeToString("{{helm}}") + "\n" +
52+
"escapedHelm2: " + Base64Util.encodeToString("{{lines}}") },
53+
{ "{{multiple\nhelm\nlines}}",
54+
"escapedHelm0: " + Base64Util.encodeToString("{{multiple") + "\nhelm\nlines}}" },
55+
{ "{{ include \"}}something{{}}}}{{\" . }}",
56+
"escapedHelm0: " + Base64Util.encodeToString("{{ include \"}}something{{}}}}{{\" . }}") },
5357

54-
// When the Helm directive is the first on the line, but indented
55-
{ " {de}f}", " {de}f}" },
56-
{ " {{de}f", " {{de}f" },
57-
{ " {{$def}}", " escapedHelm0: " + Base64Util.encodeToString("{{$def}}") },
58-
{ " {{de}}f", " escapedHelm0: " + Base64Util.encodeToString("{{de}}f") },
59-
{ " {{de}f}}", " escapedHelm0: " + Base64Util.encodeToString("{{de}f}}") },
60-
{ " {{def}}ghi{{jkl}}mno", " escapedHelm0: " + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
61-
{ "hello:\n {{def}}\n world", "hello:\n escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n world" },
62-
{ "hello:\n - {{def}}\n - world",
63-
"hello:\n - escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n - world" },
58+
// When the Helm directive is the first on the line, but indented
59+
{ " {de}f}", " {de}f}" },
60+
{ " {{de}f", " escapedHelm0: " + Base64Util.encodeToString("{{de}f") },
61+
{ " {{$def}}", " escapedHelm0: " + Base64Util.encodeToString("{{$def}}") },
62+
{ " {{de}}f", " escapedHelm0: " + Base64Util.encodeToString("{{de}}f") },
63+
{ " {{de}f}}", " escapedHelm0: " + Base64Util.encodeToString("{{de}f}}") },
64+
{ " {{def}}ghi{{jkl}}mno", " escapedHelm0: " + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
65+
{ "hello:\n {{def}}\n world", "hello:\n escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n world" },
6466

65-
// When the Helm directive is a value
66-
{ "key: {de}f}", "key: {de}f}" },
67-
{ "key: {{de}f", "key: {{de}f" },
68-
{ "key: {{$def}}", "key: escapedHelmValue" + Base64Util.encodeToString("{{$def}}") },
69-
{ "key: {{de}}f", "key: escapedHelmValue" + Base64Util.encodeToString("{{de}}f") },
70-
{ "key: {{de}f}}", "key: escapedHelmValue" + Base64Util.encodeToString("{{de}f}}") },
71-
{ "key: {{def}}ghi{{jkl}}mno", "key: escapedHelmValue" + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
67+
// When Helm is used in a list
68+
{ "- hello\n- {{def}}\n- world {{ helm }}",
69+
"- hello\n- escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n- world {{ helm }}" },
70+
{ "hello:\n - {{def}}\n - world\n - hello {{ helm }}",
71+
"hello:\n - escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n - world\n - hello {{ helm }}" },
7272

73-
// When the Helm directive is a value, but indented
74-
{ " key: {de}f}", " key: {de}f}" },
75-
{ " key: {{de}f", " key: {{de}f" },
76-
{ " key: {{$def}}", " key: escapedHelmValue" + Base64Util.encodeToString("{{$def}}") },
77-
{ " key: {{de}}f", " key: escapedHelmValue" + Base64Util.encodeToString("{{de}}f") },
78-
{ " key: {{de}f}}", " key: escapedHelmValue" + Base64Util.encodeToString("{{de}f}}") },
79-
{ " key: {{def}}ghi{{jkl}}mno", " key: escapedHelmValue" + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
73+
// When the Helm directive is a value we do not escape
74+
{ "key: {de}f}", "key: {de}f}" },
75+
{ "key: {{de}f", "key: {{de}f" },
76+
{ "key: {{$def}}", "key: {{$def}}" },
77+
{ "key: {{de}}f", "key: {{de}}f" },
78+
{ " key: {{$def}}", " key: {{$def}}" },
79+
{ "key: {{de}f}}", "key: {{de}f}}" },
80+
{ "key: {{def}}ghi{{jkl}}mno", "key: {{def}}ghi{{jkl}}mno" },
8081
});
8182
}
8283

@@ -87,6 +88,6 @@ void escapeYamlTemplateTest(final String input, final String expected) {
8788
assertThat(escapedYaml).isEqualTo(expected);
8889

8990
final String unescapedYaml = unescapeYamlTemplate(escapedYaml);
90-
assertThat(input).isEqualTo(unescapedYaml);
91+
assertThat(unescapedYaml).isEqualTo(input);
9192
}
9293
}

jkube-kit/helm/src/test/resources/it/expected/kubernetes/templates/kubernetes.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ items:
3333
helm-variable: {{ required "A valid .Values.GLOBAL_TEMPLATE_ENV_VAR entry required!" .Values.GLOBAL_TEMPLATE_ENV_VAR }}
3434
value-that-looks-like-helm-test: "{{ $The term 'helm' finds its roots in Old English }}"
3535
value-that-is-a-helm-expression: {{ .Values.abandonShip }}
36+
value-with-helm-expression-inside: So does {{ print "this" }} work?
3637
helm-golang-expression: {{ .Values.unused_value | upper | quote }}
3738
{{ end }}
3839
labels:
@@ -42,7 +43,7 @@ items:
4243
name: test
4344
spec:
4445
ports:
45-
- name: http
46+
- {{ print "name: http" }}
4647
port: 8080
4748
protocol: TCP
4849
targetPort: 8080
@@ -103,7 +104,7 @@ items:
103104
- containerPort: 8080
104105
name: http
105106
protocol: TCP
106-
- containerPort: 9779
107+
- containerPort: {{ .Values.prometheusPort }}
107108
name: prometheus
108109
protocol: TCP
109110
- containerPort: 8778

0 commit comments

Comments
 (0)