Skip to content

Commit 9ab8fdb

Browse files
committed
Better way to escape Helm (also unescape afterwards)
Signed-off-by: Jurrie Overgoor <[email protected]>
1 parent fcd7c95 commit 9ab8fdb

File tree

11 files changed

+338
-147
lines changed

11 files changed

+338
-147
lines changed

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

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

16+
import java.io.BufferedReader;
17+
import java.io.ByteArrayInputStream;
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.InputStreamReader;
22+
import java.net.URL;
23+
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.stream.Collectors;
27+
1628
import com.fasterxml.jackson.core.type.TypeReference;
1729
import com.fasterxml.jackson.databind.ObjectMapper;
1830
import com.fasterxml.jackson.databind.ObjectReader;
@@ -24,15 +36,6 @@
2436
import io.fabric8.kubernetes.api.model.KubernetesResource;
2537
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
2638

27-
import java.io.ByteArrayInputStream;
28-
import java.io.File;
29-
import java.io.IOException;
30-
import java.io.InputStream;
31-
import java.net.URL;
32-
import java.nio.charset.StandardCharsets;
33-
import java.nio.file.Files;
34-
import java.nio.file.Path;
35-
3639
public class Serialization {
3740

3841
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
@@ -97,20 +100,22 @@ public static <T> T unmarshal(URL url, TypeReference<T> type) throws IOException
97100
}
98101
}
99102

100-
public static <T> T unmarshal(InputStream is, Class<T> clazz) {
101-
return KUBERNETES_SERIALIZATION.unmarshal(is, clazz);
103+
public static <T> T unmarshal(final InputStream is, final Class<T> clazz) {
104+
return unmarshal(inputStreamToString(is), clazz);
102105
}
103106

104-
public static <T> T unmarshal(InputStream is, TypeReference<T> type) {
105-
return KUBERNETES_SERIALIZATION.unmarshal(is, type);
107+
public static <T> T unmarshal(final InputStream is, final TypeReference<T> type) {
108+
return unmarshal(inputStreamToString(is), type);
106109
}
107110

108111
public static <T> T unmarshal(String string, Class<T> clazz) {
109-
return KUBERNETES_SERIALIZATION.unmarshal(string, clazz);
112+
final byte[] yaml = TemplateUtil.escapeYamlTemplate(string).getBytes(StandardCharsets.UTF_8);
113+
return KUBERNETES_SERIALIZATION.unmarshal(new ByteArrayInputStream(yaml), clazz);
110114
}
111115

112-
public static <T> T unmarshal(String string, TypeReference<T> type) {
113-
return unmarshal(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)), type);
116+
public static <T> T unmarshal(final String string, final TypeReference<T> type) {
117+
final byte[] yaml = TemplateUtil.escapeYamlTemplate(string).getBytes(StandardCharsets.UTF_8);
118+
return KUBERNETES_SERIALIZATION.unmarshal(new ByteArrayInputStream(yaml), type);
114119
}
115120

116121
public static <T> T merge(T original, T overrides) throws IOException {
@@ -131,14 +136,24 @@ public static ObjectWriter jsonWriter() {
131136
}
132137

133138
public static String asYaml(Object object) {
134-
return KUBERNETES_SERIALIZATION.asYaml(object);
139+
final String yamlString = KUBERNETES_SERIALIZATION.asYaml(object);
140+
return TemplateUtil.unescapeYamlTemplate(yamlString);
135141
}
136142

137143
public static void saveJson(File resultFile, Object value) throws IOException {
138144
JSON_MAPPER.writeValue(resultFile, value);
139145
}
140146

141147
public static void saveYaml(File resultFile, Object value) throws IOException {
142-
YAML_MAPPER.writeValue(resultFile, value);
148+
String yamlString = YAML_MAPPER.writeValueAsString(value);
149+
yamlString = TemplateUtil.unescapeYamlTemplate(yamlString);
150+
Files.write(resultFile.toPath(), yamlString.getBytes(StandardCharsets.UTF_8));
151+
}
152+
153+
private static String inputStreamToString(final InputStream is) {
154+
return new BufferedReader(
155+
new InputStreamReader(is, StandardCharsets.UTF_8))
156+
.lines()
157+
.collect(Collectors.joining("\n"));
143158
}
144159
}

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

Lines changed: 137 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,49 +13,152 @@
1313
*/
1414
package org.eclipse.jkube.kit.common.util;
1515

16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
18+
1619
public class TemplateUtil {
20+
private static final String HELM_DIRECTIVE_REGEX = "\\{\\{.*\\}\\}";
1721

1822
private TemplateUtil() {
1923
}
2024

2125
/**
22-
* Ported from https://github.com/fabric8io/fabric8-maven-plugin/commit/d6bdaa37e06863677bc01cefa31f7d23c6d5f0f9
26+
* This function will replace all Helm directives with a valid Yaml line containing the base64 encoded Helm directive.
27+
*
28+
* Helm lines that are by themselves will be converted like so:
29+
* <br/>
30+
* Input:
31+
*
32+
* <pre>
33+
* {{- $myDate := .Value.date }}
34+
* {{ include "something" . }}
35+
* someKey: {{ a bit of Helm }}
36+
* someOtherKey: {{ another bit of Helm }}
37+
* </pre>
38+
*
39+
* Output:
40+
*
41+
* <pre>
42+
* escapedHelm0: BASE64STRINGOFCHARACTERS=
43+
* escapedHelm1: ANOTHERBASE64STRING=
44+
* someKey: escapedHelmValueBASE64STRING==
45+
* someOtherKey: escapedHelmValueBASE64STRING
46+
* </pre>
47+
*
48+
* The <strong>escapedHelm</strong> and <strong>escapedHelmValue</strong> flags are needed for unescaping.
49+
*
50+
* @param yaml the input Yaml with Helm directives to be escaped
51+
* @return the same Yaml, only with Helm directives converted to valid Yaml
52+
* @see #unescapeYamlTemplate(String)
53+
*/
54+
public static String escapeYamlTemplate(final String yaml) {
55+
return escapeYamlTemplateLines(escapeYamlTemplateValues(yaml));
56+
}
57+
58+
/**
59+
* This function will replace all escaped Helm directives by {@link #escapeYamlTemplate(String)} back to actual Helm.
60+
* <br/>
61+
* This function promises to be the opposite of {@link #escapeYamlTemplate(String)}.
62+
*
63+
* @param template the input Yaml that was returned by a call to {@link #escapeYamlTemplate(String)}
64+
* @return the Yaml that was originally provided to {@link #escapeYamlTemplate(String)}
65+
* @see #escapeYamlTemplate(String)
66+
*/
67+
public static String unescapeYamlTemplate(final String template) {
68+
return unescapeYamlTemplateLines(unescapeYamlTemplateValues(template));
69+
}
70+
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>
2378
*
24-
* @param template String to escape
25-
* @return the escaped Yaml template
79+
* @see #unescapeYamlTemplateLines(String)
2680
*/
27-
public static String escapeYamlTemplate(String template) {
28-
StringBuilder answer = new StringBuilder();
29-
int count = 0;
30-
char last = 0;
31-
for (int i = 0, size = template.length(); i < size; i++) {
32-
char ch = template.charAt(i);
33-
if (ch == '{' || ch == '}') {
34-
if (count == 0) {
35-
last = ch;
36-
count = 1;
37-
} else {
38-
if (ch == last) {
39-
answer.append(ch == '{' ? "{{\"{{\"}}" : "{{\"}}\"}}");
40-
} else {
41-
answer.append(last);
42-
answer.append(ch);
43-
}
44-
count = 0;
45-
last = 0;
46-
}
47-
} else {
48-
if (count > 0) {
49-
answer.append(last);
50-
}
51-
answer.append(ch);
52-
count = 0;
53-
last = 0;
54-
}
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++;
5591
}
56-
if (count > 0) {
57-
answer.append(last);
92+
return template;
93+
}
94+
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+
}
118+
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);
137+
}
138+
return template;
139+
}
140+
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);
58161
}
59-
return answer.toString();
162+
return template;
60163
}
61164
}

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

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,80 @@
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;
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.eclipse.jkube.kit.common.util.TemplateUtil.escapeYamlTemplate;
18+
import static org.eclipse.jkube.kit.common.util.TemplateUtil.unescapeYamlTemplate;
1819

1920
import java.util.stream.Stream;
2021

21-
import static org.assertj.core.api.Assertions.assertThat;
22-
import static org.eclipse.jkube.kit.common.util.TemplateUtil.escapeYamlTemplate;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.MethodSource;
2324

2425
class TemplateUtilTest {
2526

26-
public static Stream<Object[]> data() {
27-
return Stream.of(new Object[][]{
28-
{"abcd", "abcd"},
29-
{"abc{de}f}", "abc{de}f}"},
30-
{"abc{{de}f", "abc{{\"{{\"}}de}f"},
31-
{"abc{{de}f}}", "abc{{\"{{\"}}de}f{{\"}}\"}}"}
32-
});
33-
}
34-
35-
@ParameterizedTest(name = "{0} is {1}")
36-
@MethodSource("data")
37-
void escapeYamlTemplateTest(String input, String expected) {
38-
assertThat(escapeYamlTemplate(input)).isEqualTo(expected);
39-
}
27+
public static Stream<Object[]> data() {
28+
return Stream.of(new Object[][] {
29+
// No Helm directive
30+
{ "abcd", "abcd" },
31+
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" },
39+
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}}") },
53+
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" },
64+
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") },
72+
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") },
80+
});
81+
}
82+
83+
@ParameterizedTest(name = "{0} → {1}")
84+
@MethodSource("data")
85+
void escapeYamlTemplateTest(final String input, final String expected) {
86+
final String escapedYaml = escapeYamlTemplate(input);
87+
assertThat(escapedYaml).isEqualTo(expected);
88+
89+
final String unescapedYaml = unescapeYamlTemplate(escapedYaml);
90+
assertThat(input).isEqualTo(unescapedYaml);
91+
}
4092
}

0 commit comments

Comments
 (0)