Skip to content

Commit

Permalink
Better way to escape Helm (also unescape afterwards)
Browse files Browse the repository at this point in the history
Signed-off-by: Jurrie Overgoor <[email protected]>
  • Loading branch information
Jurrie committed Feb 16, 2024
1 parent fcd7c95 commit 9ab8fdb
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
*/
package org.eclipse.jkube.kit.common.util;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
Expand All @@ -24,15 +36,6 @@
import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Serialization {

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

public static <T> T unmarshal(InputStream is, Class<T> clazz) {
return KUBERNETES_SERIALIZATION.unmarshal(is, clazz);
public static <T> T unmarshal(final InputStream is, final Class<T> clazz) {
return unmarshal(inputStreamToString(is), clazz);
}

public static <T> T unmarshal(InputStream is, TypeReference<T> type) {
return KUBERNETES_SERIALIZATION.unmarshal(is, type);
public static <T> T unmarshal(final InputStream is, final TypeReference<T> type) {
return unmarshal(inputStreamToString(is), type);
}

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

public static <T> T unmarshal(String string, TypeReference<T> type) {
return unmarshal(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)), type);
public static <T> T unmarshal(final String string, final TypeReference<T> type) {
final byte[] yaml = TemplateUtil.escapeYamlTemplate(string).getBytes(StandardCharsets.UTF_8);
return KUBERNETES_SERIALIZATION.unmarshal(new ByteArrayInputStream(yaml), type);
}

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

public static String asYaml(Object object) {
return KUBERNETES_SERIALIZATION.asYaml(object);
final String yamlString = KUBERNETES_SERIALIZATION.asYaml(object);
return TemplateUtil.unescapeYamlTemplate(yamlString);
}

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

public static void saveYaml(File resultFile, Object value) throws IOException {
YAML_MAPPER.writeValue(resultFile, value);
String yamlString = YAML_MAPPER.writeValueAsString(value);
yamlString = TemplateUtil.unescapeYamlTemplate(yamlString);
Files.write(resultFile.toPath(), yamlString.getBytes(StandardCharsets.UTF_8));
}

private static String inputStreamToString(final InputStream is) {
return new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,152 @@
*/
package org.eclipse.jkube.kit.common.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TemplateUtil {
private static final String HELM_DIRECTIVE_REGEX = "\\{\\{.*\\}\\}";

private TemplateUtil() {
}

/**
* Ported from https://github.com/fabric8io/fabric8-maven-plugin/commit/d6bdaa37e06863677bc01cefa31f7d23c6d5f0f9
* This function will replace all Helm directives with a valid Yaml line containing the base64 encoded Helm directive.
*
* Helm lines that are by themselves will be converted like so:
* <br/>
* Input:
*
* <pre>
* {{- $myDate := .Value.date }}
* {{ include "something" . }}
* someKey: {{ a bit of Helm }}
* someOtherKey: {{ another bit of Helm }}
* </pre>
*
* Output:
*
* <pre>
* escapedHelm0: BASE64STRINGOFCHARACTERS=
* escapedHelm1: ANOTHERBASE64STRING=
* someKey: escapedHelmValueBASE64STRING==
* someOtherKey: escapedHelmValueBASE64STRING
* </pre>
*
* The <strong>escapedHelm</strong> and <strong>escapedHelmValue</strong> flags are needed for unescaping.
*
* @param yaml the input Yaml with Helm directives to be escaped
* @return the same Yaml, only with Helm directives converted to valid Yaml
* @see #unescapeYamlTemplate(String)
*/
public static String escapeYamlTemplate(final String yaml) {
return escapeYamlTemplateLines(escapeYamlTemplateValues(yaml));
}

/**
* This function will replace all escaped Helm directives by {@link #escapeYamlTemplate(String)} back to actual Helm.
* <br/>
* This function promises to be the opposite of {@link #escapeYamlTemplate(String)}.
*
* @param template the input Yaml that was returned by a call to {@link #escapeYamlTemplate(String)}
* @return the Yaml that was originally provided to {@link #escapeYamlTemplate(String)}
* @see #escapeYamlTemplate(String)
*/
public static String unescapeYamlTemplate(final String template) {
return unescapeYamlTemplateLines(unescapeYamlTemplateValues(template));
}

/**
* This function is responsible for escaping the Helm directives that are stand-alone.
* For example:
*
* <pre>
* {{ include "something" . }}
* </pre>
*
* @param template String to escape
* @return the escaped Yaml template
* @see #unescapeYamlTemplateLines(String)
*/
public static String escapeYamlTemplate(String template) {
StringBuilder answer = new StringBuilder();
int count = 0;
char last = 0;
for (int i = 0, size = template.length(); i < size; i++) {
char ch = template.charAt(i);
if (ch == '{' || ch == '}') {
if (count == 0) {
last = ch;
count = 1;
} else {
if (ch == last) {
answer.append(ch == '{' ? "{{\"{{\"}}" : "{{\"}}\"}}");
} else {
answer.append(last);
answer.append(ch);
}
count = 0;
last = 0;
}
} else {
if (count > 0) {
answer.append(last);
}
answer.append(ch);
count = 0;
last = 0;
}
private static String escapeYamlTemplateLines(String template) {
long escapedHelmIndex = 0;
final Pattern compile = Pattern.compile("^( *-? *)(" + HELM_DIRECTIVE_REGEX + ".*)$", Pattern.MULTILINE);
Matcher matcher = compile.matcher(template);
while (matcher.find()) {
final String indentation = matcher.group(1);
final String base64Line = Base64Util.encodeToString(matcher.group(2));
template = matcher.replaceFirst(indentation + "escapedHelm" + escapedHelmIndex + ": " + base64Line);
matcher = compile.matcher(template);
escapedHelmIndex++;
}
if (count > 0) {
answer.append(last);
return template;
}

/**
* This function is responsible for reinstating the stand-alone Helm directives.
* For example:
*
* <pre>
* BASE64STRINGOFCHARACTERS=
* </pre>
*
* It is the opposite of {@link #escapeYamlTemplateLines(String)}.
*
* @see #escapeYamlTemplateLines(String)
*/
private static String unescapeYamlTemplateLines(String template) {
final Pattern compile = Pattern.compile("^( *-? *)escapedHelm[\\d]+: \"?(.*?)\"?$", Pattern.MULTILINE);
Matcher matcher = compile.matcher(template);
while (matcher.find()) {
final String indentation = matcher.group(1);
final String helmLine = Base64Util.decodeToString(matcher.group(2));
template = matcher.replaceFirst(indentation + helmLine.replace("$", "\\$"));
matcher = compile.matcher(template);
}
return template;
}

/**
* This function is responsible for escaping the Helm directives that are Yaml values.
* For example:
*
* <pre>
* someKey: {{ a bit of Helm }}
* </pre>
*
* @see #unescapeYamlTemplateValues(String)
*/
private static String escapeYamlTemplateValues(String template) {
final Pattern compile = Pattern.compile("^( *[^ ]+ *): *(" + HELM_DIRECTIVE_REGEX + ".*)$", Pattern.MULTILINE);
Matcher matcher = compile.matcher(template);
while (matcher.find()) {
final String indentation = matcher.group(1);
final String base64Value = Base64Util.encodeToString(matcher.group(2));
template = matcher.replaceFirst(indentation + ": escapedHelmValue" + base64Value);
matcher = compile.matcher(template);
}
return template;
}

/**
* This function is responsible for reinstating the Helm directives that were Yaml values.
* For example:
*
* <pre>
* someKey: escapedHelmValueBASE64STRING==
* </pre>
*
* It is the opposite of {@link #escapeYamlTemplateValues(String)}.
*
* @see #escapeYamlTemplateValues(String)
*/
private static String unescapeYamlTemplateValues(String template) {
final Pattern compile = Pattern.compile("^( *[^ ]+ *): *\"?escapedHelmValue(.*?)\"?$", Pattern.MULTILINE);
Matcher matcher = compile.matcher(template);
while (matcher.find()) {
final String indentation = matcher.group(1);
final String helmValue = Base64Util.decodeToString(matcher.group(2));
template = matcher.replaceFirst(indentation + ": " + helmValue.replace("$", "\\$"));
matcher = compile.matcher(template);
}
return answer.toString();
return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,80 @@
*/
package org.eclipse.jkube.kit.common.util;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.jkube.kit.common.util.TemplateUtil.escapeYamlTemplate;
import static org.eclipse.jkube.kit.common.util.TemplateUtil.unescapeYamlTemplate;

import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.jkube.kit.common.util.TemplateUtil.escapeYamlTemplate;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class TemplateUtilTest {

public static Stream<Object[]> data() {
return Stream.of(new Object[][]{
{"abcd", "abcd"},
{"abc{de}f}", "abc{de}f}"},
{"abc{{de}f", "abc{{\"{{\"}}de}f"},
{"abc{{de}f}}", "abc{{\"{{\"}}de}f{{\"}}\"}}"}
});
}

@ParameterizedTest(name = "{0} is {1}")
@MethodSource("data")
void escapeYamlTemplateTest(String input, String expected) {
assertThat(escapeYamlTemplate(input)).isEqualTo(expected);
}
public static Stream<Object[]> data() {
return Stream.of(new Object[][] {
// No Helm directive
{ "abcd", "abcd" },

// When the Helm directive is not the first on the line
{ "abc{de}f}", "abc{de}f}" },
{ "abc{{de}f", "abc{{de}f" },
{ "abc{{$def}}", "abc{{$def}}" },
{ "abc{{de}}f", "abc{{de}}f" },
{ "abc{{de}f}}", "abc{{de}f}}" },
{ "abc{{def}}ghi{{jkl}}mno", "abc{{def}}ghi{{jkl}}mno" },

// When the Helm directive is the first on the line
{ "{de}f}", "{de}f}" },
{ "{{de}f", "{{de}f" },
{ "{{$def}}", "escapedHelm0: " + Base64Util.encodeToString("{{$def}}") },
{ "{{de}}f", "escapedHelm0: " + Base64Util.encodeToString("{{de}}f") },
{ "{{de}f}}", "escapedHelm0: " + Base64Util.encodeToString("{{de}f}}") },
{ "{{def}}ghi{{jkl}}mno", "escapedHelm0: " + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
{ "hello\n{{def}}\nworld", "hello\nescapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\nworld" },
{ "- hello\n- {{def}}\n- world", "- hello\n- escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n- world" },
{ "{{multiple}}\n{{helm}}\n{{lines}}",
"escapedHelm0: " + Base64Util.encodeToString("{{multiple}}") + "\n" +
"escapedHelm1: " + Base64Util.encodeToString("{{helm}}") + "\n" +
"escapedHelm2: " + Base64Util.encodeToString("{{lines}}") },

// When the Helm directive is the first on the line, but indented
{ " {de}f}", " {de}f}" },
{ " {{de}f", " {{de}f" },
{ " {{$def}}", " escapedHelm0: " + Base64Util.encodeToString("{{$def}}") },
{ " {{de}}f", " escapedHelm0: " + Base64Util.encodeToString("{{de}}f") },
{ " {{de}f}}", " escapedHelm0: " + Base64Util.encodeToString("{{de}f}}") },
{ " {{def}}ghi{{jkl}}mno", " escapedHelm0: " + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
{ "hello:\n {{def}}\n world", "hello:\n escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n world" },
{ "hello:\n - {{def}}\n - world",
"hello:\n - escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n - world" },

// When the Helm directive is a value
{ "key: {de}f}", "key: {de}f}" },
{ "key: {{de}f", "key: {{de}f" },
{ "key: {{$def}}", "key: escapedHelmValue" + Base64Util.encodeToString("{{$def}}") },
{ "key: {{de}}f", "key: escapedHelmValue" + Base64Util.encodeToString("{{de}}f") },
{ "key: {{de}f}}", "key: escapedHelmValue" + Base64Util.encodeToString("{{de}f}}") },
{ "key: {{def}}ghi{{jkl}}mno", "key: escapedHelmValue" + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },

// When the Helm directive is a value, but indented
{ " key: {de}f}", " key: {de}f}" },
{ " key: {{de}f", " key: {{de}f" },
{ " key: {{$def}}", " key: escapedHelmValue" + Base64Util.encodeToString("{{$def}}") },
{ " key: {{de}}f", " key: escapedHelmValue" + Base64Util.encodeToString("{{de}}f") },
{ " key: {{de}f}}", " key: escapedHelmValue" + Base64Util.encodeToString("{{de}f}}") },
{ " key: {{def}}ghi{{jkl}}mno", " key: escapedHelmValue" + Base64Util.encodeToString("{{def}}ghi{{jkl}}mno") },
});
}

@ParameterizedTest(name = "{0} → {1}")
@MethodSource("data")
void escapeYamlTemplateTest(final String input, final String expected) {
final String escapedYaml = escapeYamlTemplate(input);
assertThat(escapedYaml).isEqualTo(expected);

final String unescapedYaml = unescapeYamlTemplate(escapedYaml);
assertThat(input).isEqualTo(unescapedYaml);
}
}
Loading

0 comments on commit 9ab8fdb

Please sign in to comment.