Skip to content

Commit

Permalink
Rewrote the escaping logic - hopefully better readable
Browse files Browse the repository at this point in the history
Signed-off-by: Jurrie Overgoor <[email protected]>
  • Loading branch information
Jurrie committed Feb 23, 2024
1 parent 0bfaf28 commit 88d310e
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,32 @@
*/
package org.eclipse.jkube.kit.common.util;

import java.lang.invoke.MethodHandles;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TemplateUtil {
private static final String HELM_DIRECTIVE_REGEX = "\\{\\{.*\\}\\}";
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private static final String HELM_END_TAG = "}}";
private static final String HELM_START_TAG = "{{";

private static final String YAML_HELM_ESCAPE_KEY = "escapedHelm";
private static final String YAML_LIST_TAG = "-";
private static final String YAML_KEY_VALUE_SEPARATOR = ": ";

private TemplateUtil() {
}

/**
* This function will replace all Helm directives with a valid Yaml line containing the base64 encoded Helm directive.
* This function will replace all single line Helm directives with a valid Yaml line containing the base64 encoded Helm
* directive.
* Helm directives that are values (so not a full line) will be left alone.
*
* Helm lines that are by themselves will be converted like so:
* <br/>
Expand All @@ -32,17 +47,17 @@ private TemplateUtil() {
* <pre>
* {{- $myDate := .Value.date }}
* {{ include "something" . }}
* myKey: "Regular line"
* someKey: {{ a bit of Helm }}
* someOtherKey: {{ another bit of Helm }}
* </pre>
*
* Output:
*
* <pre>
* escapedHelm0: BASE64STRINGOFCHARACTERS=
* escapedHelm1: ANOTHERBASE64STRING=
* someKey: escapedHelmValueBASE64STRING==
* someOtherKey: escapedHelmValueBASE64STRING
* escapedHelm0: e3stICA6PSAuVmFsdWUuZGF0ZSB9fQ==
* escapedHelm1: e3sgaW5jbHVkZSBzb21ldGhpbmcgLiB9fQ==
* myKey: "Regular line"
* someKey: {{ a bit of Helm }}
* </pre>
*
* The <strong>escapedHelm</strong> and <strong>escapedHelmValue</strong> flags are needed for unescaping.
Expand All @@ -52,7 +67,39 @@ private TemplateUtil() {
* @see #unescapeYamlTemplate(String)
*/
public static String escapeYamlTemplate(final String yaml) {
return escapeYamlTemplateLines(escapeYamlTemplateValues(yaml));
final AtomicLong helmEscapeIndex = new AtomicLong();
return iterateOverLines(yaml,
(line, lineEnding, lineNumber) -> escapeYamlLine(line, lineNumber, helmEscapeIndex) + lineEnding);
}

private static String escapeYamlLine(final String line, final long lineNumber, final AtomicLong helmEscapeIndex) {
if (line.trim().startsWith(HELM_START_TAG)) {
// Line starts with optional indenting and then '{{', so replace whole line
final int helmStartCount = StringUtils.countMatches(line, HELM_START_TAG);
final int helmEndCount = StringUtils.countMatches(line, HELM_END_TAG);
if (helmStartCount != helmEndCount) {
LOG.warn("Found {} Helm start tag{} ('{}') but {} end tag{} ('{}') on line {}. "
+ "Expected this to be equal! Note that multi-line Helm directives are not supported.",
helmStartCount, helmStartCount == 1 ? "" : "s", HELM_START_TAG,
helmEndCount, helmEndCount == 1 ? "" : "s", HELM_END_TAG,
lineNumber);
}

final int startOfHelm = line.indexOf(HELM_START_TAG);
final String indentation = line.substring(0, startOfHelm);
final String base64EncodedLine = Base64Util.encodeToString(line.substring(startOfHelm));
final long currentHelmEscapeIndex = helmEscapeIndex.getAndIncrement();
return indentation + YAML_HELM_ESCAPE_KEY + currentHelmEscapeIndex + YAML_KEY_VALUE_SEPARATOR + base64EncodedLine;
} else if (line.trim().startsWith(YAML_LIST_TAG)) {
// Line starts with optional indenting and then '-'. Strip the '-', parse again, readd the '-'.
final int startOfRestOfLine = line.indexOf(YAML_LIST_TAG) + YAML_LIST_TAG.length();
final String prefix = line.substring(0, startOfRestOfLine);
final String restOfLine = line.substring(startOfRestOfLine);
return prefix + escapeYamlLine(restOfLine, lineNumber, helmEscapeIndex);
} else {
// Line was a regular line
return line;
}
}

/**
Expand All @@ -64,101 +111,51 @@ public static String escapeYamlTemplate(final String yaml) {
* @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));
public static String unescapeYamlTemplate(final String yaml) {
return iterateOverLines(yaml, (line, lineEnding, lineNumber) -> unescapeYamlLine(line) + lineEnding);
}

/**
* This function is responsible for escaping the Helm directives that are stand-alone.
* For example:
*
* <pre>
* {{ include "something" . }}
* </pre>
*
* @see #unescapeYamlTemplateLines(String)
*/
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++;
private static String unescapeYamlLine(final String line) {
if (line.trim().startsWith(YAML_HELM_ESCAPE_KEY)) {
// Line starts with optional indenting and then 'escapedHelm', so replace whole line
final int endOfIndentation = line.indexOf(YAML_HELM_ESCAPE_KEY);
final int startOfBase64Helm = line.indexOf(YAML_KEY_VALUE_SEPARATOR) + YAML_KEY_VALUE_SEPARATOR.length();
final String indentation = line.substring(0, endOfIndentation);
final String base64DecodedLine = Base64Util.decodeToString(line.substring(startOfBase64Helm));
return indentation + base64DecodedLine;
} else if (line.trim().startsWith(YAML_LIST_TAG)) {
// Line starts with optional indenting and then '-'. Strip the '-', parse again, readd the '-'.
final int startOfRestOfLine = line.indexOf(YAML_LIST_TAG) + YAML_LIST_TAG.length();
final String prefix = line.substring(0, startOfRestOfLine);
final String restOfLine = line.substring(startOfRestOfLine);
return prefix + unescapeYamlLine(restOfLine);
} else {
// Line was a regular line
return line;
}
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;
}
private static String iterateOverLines(final String yaml, final IterateOverLinesCallback iterator) {
final Matcher matcher = Pattern.compile("(.*)(\\R|$)").matcher(yaml);
final StringBuilder result = new StringBuilder();
int index = 0;
long lineNumber = 0;
while (matcher.find(index) && matcher.start() != matcher.end()) {
final String line = matcher.group(1);
final String lineEnding = matcher.group(2);

/**
* 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);
final String escapedYamlLine = iterator.onLine(line, lineEnding, lineNumber);
lineNumber++;

result.append(escapedYamlLine);

index = matcher.end();
}
return template;
return result.toString();
}

/**
* 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 template;
@FunctionalInterface
private interface IterateOverLinesCallback {
String onLine(String input, String lineEnding, long lineNumber);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,70 +13,71 @@
*/
package org.eclipse.jkube.kit.common.util;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

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 org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class TemplateUtilTest {

public static Stream<Object[]> data() {
return Stream.of(new Object[][] {
// No Helm directive
{ "abcd", "abcd" },
// 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 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
{ "{de}f}", "{de}f}" },
{ "{{de}f", "escapedHelm0: " + Base64Util.encodeToString("{{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") },
{ "abc{{def}}ghi{{jkl}}mno", "abc{{def}}ghi{{jkl}}mno" },
{ "hello\n{{def}}\nworld", "hello\nescapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\nworld" },
{ "{{multiple}}\n{{helm}}\n{{lines}}",
"escapedHelm0: " + Base64Util.encodeToString("{{multiple}}") + "\n" +
"escapedHelm1: " + Base64Util.encodeToString("{{helm}}") + "\n" +
"escapedHelm2: " + Base64Util.encodeToString("{{lines}}") },
{ "{{multiple\nhelm\nlines}}",
"escapedHelm0: " + Base64Util.encodeToString("{{multiple") + "\nhelm\nlines}}" },
{ "{{ include \"}}something{{}}}}{{\" . }}",
"escapedHelm0: " + Base64Util.encodeToString("{{ include \"}}something{{}}}}{{\" . }}") },

// 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 the first on the line, but indented
{ " {de}f}", " {de}f}" },
{ " {{de}f", " escapedHelm0: " + Base64Util.encodeToString("{{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" },

// 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 Helm is used in a list
{ "- hello\n- {{def}}\n- world {{ helm }}",
"- hello\n- escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n- world {{ helm }}" },
{ "hello:\n - {{def}}\n - world\n - hello {{ helm }}",
"hello:\n - escapedHelm0: " + Base64Util.encodeToString("{{def}}") + "\n - world\n - hello {{ helm }}" },

// 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") },
// When the Helm directive is a value we do not escape
{ "key: {de}f}", "key: {de}f}" },
{ "key: {{de}f", "key: {{de}f" },
{ "key: {{$def}}", "key: {{$def}}" },
{ "key: {{de}}f", "key: {{de}}f" },
{ " key: {{$def}}", " key: {{$def}}" },
{ "key: {{de}f}}", "key: {{de}f}}" },
{ "key: {{def}}ghi{{jkl}}mno", "key: {{def}}ghi{{jkl}}mno" },
});
}

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

final String unescapedYaml = unescapeYamlTemplate(escapedYaml);
assertThat(input).isEqualTo(unescapedYaml);
assertThat(unescapedYaml).isEqualTo(input);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ items:
helm-variable: {{ required "A valid .Values.GLOBAL_TEMPLATE_ENV_VAR entry required!" .Values.GLOBAL_TEMPLATE_ENV_VAR }}
value-that-looks-like-helm-test: "{{ $The term 'helm' finds its roots in Old English }}"
value-that-is-a-helm-expression: {{ .Values.abandonShip }}
value-with-helm-expression-inside: So does {{ print "this" }} work?
helm-golang-expression: {{ .Values.unused_value | upper | quote }}
{{ end }}
labels:
Expand All @@ -42,7 +43,7 @@ items:
name: test
spec:
ports:
- name: http
- {{ print "name: http" }}
port: 8080
protocol: TCP
targetPort: 8080
Expand Down Expand Up @@ -103,7 +104,7 @@ items:
- containerPort: 8080
name: http
protocol: TCP
- containerPort: 9779
- containerPort: {{ .Values.prometheusPort }}
name: prometheus
protocol: TCP
- containerPort: 8778
Expand Down
Loading

0 comments on commit 88d310e

Please sign in to comment.