Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Helm in Yaml fragments #2689

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@
import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;

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;

public class Serialization {

Expand Down Expand Up @@ -65,19 +69,28 @@ public static <T> T unmarshal(File file) throws IOException {
}

public static <T> T unmarshal(File file, Class<T> clazz) throws IOException {
try (InputStream fis = Files.newInputStream(file.toPath())) {
return unmarshal(file.toPath(), clazz);
}

public static <T> T unmarshal(File file, TypeReference<T> type) throws IOException {
return unmarshal(file.toPath(), type);
}

public static <T> T unmarshal(Path file, Class<T> clazz) throws IOException {
try (InputStream fis = Files.newInputStream(file)) {
return unmarshal(fis, clazz);
}
}

public static <T> T unmarshal(File file, TypeReference<T> type) throws IOException {
try (InputStream fis = Files.newInputStream(file.toPath())) {
public static <T> T unmarshal(Path file, TypeReference<T> type) throws IOException {
try (InputStream fis = Files.newInputStream(file)) {
return unmarshal(fis, type);
}
}
public static <T> T unmarshal(URL url, Class<T> type) throws IOException {

public static <T> T unmarshal(URL url, Class<T> clazz) throws IOException {
try (InputStream is = url.openStream()){
return unmarshal(is, type);
return unmarshal(is, clazz);
}
}

Expand All @@ -87,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> type) {
return KUBERNETES_SERIALIZATION.unmarshal(string, type);
public static <T> T unmarshal(String string, Class<T> 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 @@ -121,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,180 @@
*/
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 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() {
}

/**
* Ported from https://github.com/fabric8io/fabric8-maven-plugin/commit/d6bdaa37e06863677bc01cefa31f7d23c6d5f0f9
* 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/>
* Input:
*
* <pre>
* {{- $myDate := .Value.date }}
* {{ include "something" . }}
* myKey: "Regular line"
* someKey: {{ a bit of Helm }}
* </pre>
*
* Output:
*
* <pre>
* 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.
*
* @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) {
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
checkHelmStartAndEndTags(line, 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 if (line.contains(YAML_KEY_VALUE_SEPARATOR)) {
// Line is a "key: value" line. Strip the key, parse again, readd the key.
final int startOfRestOfLine = line.indexOf(YAML_KEY_VALUE_SEPARATOR) + YAML_KEY_VALUE_SEPARATOR.length();
final String key = line.substring(0, startOfRestOfLine);
final String value = line.substring(startOfRestOfLine);

if (value.trim().startsWith(HELM_START_TAG)) {
checkHelmStartAndEndTags(value, lineNumber);

final String base64EncodedLine = Base64Util.encodeToString(value);
return key + YAML_HELM_ESCAPE_KEY + base64EncodedLine;
} else {
// Value was a regular value
return line;
}
} else {
// Line was a regular line
return line;
}
}

private static void checkHelmStartAndEndTags(final String line, final long lineNumber) {
final int startCount = StringUtils.countMatches(line, HELM_START_TAG);
final int endCount = StringUtils.countMatches(line, HELM_END_TAG);
if (startCount != endCount) {
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.",
startCount, startCount == 1 ? "" : "s", HELM_START_TAG,
endCount, endCount == 1 ? "" : "s", HELM_END_TAG,
lineNumber);
}
}

/**
* 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 String to escape
* @return the escaped Yaml template
* @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 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;
}
public static String unescapeYamlTemplate(final String yaml) {
return iterateOverLines(yaml, (line, lineEnding, lineNumber) -> unescapeYamlLine(line) + lineEnding);
}

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 if (line.contains(YAML_KEY_VALUE_SEPARATOR)) {
// Line is a "key: value" line. Strip the key, parse again, readd the key.
final int startOfRestOfLine = line.indexOf(YAML_KEY_VALUE_SEPARATOR) + YAML_KEY_VALUE_SEPARATOR.length();
final String key = line.substring(0, startOfRestOfLine);
final String value = line.substring(startOfRestOfLine);
if (value.trim().startsWith(YAML_HELM_ESCAPE_KEY)) {
final String base64DecodedLine = Base64Util.decodeToString(value.substring(YAML_HELM_ESCAPE_KEY.length()));
return key + base64DecodedLine;
} else {
if (count > 0) {
answer.append(last);
}
answer.append(ch);
count = 0;
last = 0;
// Value was a regular value
return line;
}
} else {
// Line was a regular line
return line;
}
if (count > 0) {
answer.append(last);
}

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);

final String escapedYamlLine = iterator.onLine(line, lineEnding, lineNumber);
lineNumber++;

result.append(escapedYamlLine);

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

@FunctionalInterface
private interface IterateOverLinesCallback {
String onLine(String input, String lineEnding, long lineNumber);
}
}
Loading