Skip to content

Commit

Permalink
fea(helm): add support for generating helm test resources via fragmen…
Browse files Browse the repository at this point in the history
…ts (3346)

feat (jkube-kit/helm) : Add support for generating helm test resources via fragments

+ Add support for generating helm test resources. User would place a test
  fragment in src/main/jkube directory and it would be copied to
  templates/tests during helm chart generation.
+ Only fragments ending with `.test.helm.yaml` suffix would be considered
  for helm test resources.

Signed-off-by: Rohan Kumar <[email protected]>
---
review: helm test resource processing

Signed-off-by: Marc Nuri <[email protected]>

Co-authored-by: Marc Nuri <[email protected]>
  • Loading branch information
rohanKanojia and manusa authored Sep 23, 2024
1 parent d2afc38 commit 6ddd463
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 109 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Usage:
### 1.18-SNAPSHOT
* Fix #1125: Support WebFlux SpringBoot projects when it comes to generate probes for actuators
* Fix #2844: `oc:build` on openshift use `pods/log` to retrieve logs from build
* Fix #2375: Add support for generating helm test resources via fragments
* Fix #3354: Build fails with `imageStream` for `buildRecreate` value

### 1.17.0 (2024-08-13)
Expand Down
1 change: 1 addition & 0 deletions jkube-kit/doc/src/main/asciidoc/inc/helm/_jkube_helm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ For the `values.yaml` file you can provide a `values.helm.yaml` fragment in the
These fragments will be merged with the opinionated and configured defaults.
The values provided in the fragments will override any of the generated default values taking precedence over them.
For providing https://helm.sh/docs/topics/chart_tests/[Helm Chart Test] files, you can provide a fragment ending with `.test.helm.yaml` suffix in `src/main/jkube` directory.
// TODO: Remove if Once header depth is aligned in both plugins
ifeval::["{plugin-type}" == "maven"]
=== Installing the generated Helm chart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Consumer;
Expand All @@ -41,6 +42,7 @@
import com.marcnuri.helm.LintResult;
import com.marcnuri.helm.Release;
import com.marcnuri.helm.UninstallCommand;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import org.eclipse.jkube.kit.common.JKubeConfiguration;
import org.eclipse.jkube.kit.common.JKubeException;
import org.eclipse.jkube.kit.common.KitLogger;
Expand Down Expand Up @@ -77,13 +79,17 @@

public class HelmService {

private static final String YML_EXTENSION = ".yml";
private static final String YAML_EXTENSION = ".yaml";
public static final String CHART_FILENAME = "Chart" + YAML_EXTENSION;
private static final String VALUES_FILENAME = "values" + YAML_EXTENSION;

private static final String CHART_FRAGMENT_REGEX = "^chart\\.helm\\.(?<ext>yaml|yml|json)$";
public static final Pattern CHART_FRAGMENT_PATTERN = Pattern.compile(CHART_FRAGMENT_REGEX, Pattern.CASE_INSENSITIVE);

private static final String CHART_TEST_FRAGMENT_REGEX = "^.+\\.test\\.helm\\.(?<ext>yaml|yml|json)$";
private static final Pattern CHART_TEST_FRAGMENT_PATTERN = Pattern.compile(CHART_TEST_FRAGMENT_REGEX, Pattern.CASE_INSENSITIVE);

private static final String VALUES_FRAGMENT_REGEX = "^values\\.helm\\.(?<ext>yaml|yml|json)$";
public static final Pattern VALUES_FRAGMENT_PATTERN = Pattern.compile(VALUES_FRAGMENT_REGEX, Pattern.CASE_INSENSITIVE);
private static final String SYSTEM_LINE_SEPARATOR_REGEX = "\r?\n";
Expand Down Expand Up @@ -123,6 +129,8 @@ public void generateHelmCharts(HelmConfig helmConfig) throws IOException {
createChartYaml(helmConfig, outputDir);
logger.debug("Copying additional files");
copyAdditionalFiles(helmConfig, outputDir);
logger.debug("Copying test files");
processTestFiles(templatesDir);
logger.debug("Gathering parameters for placeholders");
final List<HelmParameter> parameters = collectParameters(helmConfig);
logger.debug("Generating values.yaml");
Expand All @@ -146,6 +154,7 @@ public void generateHelmCharts(HelmConfig helmConfig) throws IOException {
}
}


/**
* Uploads the charts defined in the provided {@link HelmConfig} to the applicable configured repository.
*
Expand Down Expand Up @@ -189,9 +198,9 @@ public void dependencyUpdate(HelmConfig helmConfig) {
if (helmConfig.isDependencySkipRefresh()) {
dependencyUpdateCommand.skipRefresh();
}
Arrays.stream(dependencyUpdateCommand.call()
Arrays.stream(dependencyUpdateCommand.call()
.split(SYSTEM_LINE_SEPARATOR_REGEX))
.forEach(l -> logger.info("[[W]]%s", l));
.forEach(l -> logger.info("[[W]]%s", l));
}
}

Expand Down Expand Up @@ -324,20 +333,31 @@ private static File prepareOutputDir(HelmConfig helmConfig, HelmConfig.HelmType
return outputDir;
}


public static boolean containsYamlFiles(File directory) {
return !listYamls(directory).isEmpty();
}

private void processTestFiles(File templatesDir) throws IOException {
final File templatesTestsDir = new File(templatesDir, "tests");
final Set<File> helmTestFragments = findHelmFragmentsInResourceDir(CHART_TEST_FRAGMENT_PATTERN, resourceServiceConfig)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
for (File testFragment : helmTestFragments) {
final GenericKubernetesResource testTemplate = readFragment(testFragment, GenericKubernetesResource.class);
final String fileName = FileUtil.stripPostfix(FileUtil.stripPostfix(FileUtil.stripPostfix(testFragment.getName(), YML_EXTENSION), YAML_EXTENSION), ".test.helm") + YAML_EXTENSION;
ResourceUtil.save(new File(templatesTestsDir, fileName), testTemplate, ResourceFileType.yaml);
}
}

private static void processSourceFiles(File sourceDir, File templatesDir) throws IOException {
for (File file : listYamls(sourceDir)) {
final KubernetesResource dto = Serialization.unmarshal(file);
if (dto instanceof Template) {
splitAndSaveTemplate((Template) dto, templatesDir);
} else {
final String fileName = FileUtil.stripPostfix(FileUtil.stripPostfix(file.getName(), ".yml"), YAML_EXTENSION) + YAML_EXTENSION;
final String fileName = FileUtil.stripPostfix(FileUtil.stripPostfix(file.getName(), YML_EXTENSION), YAML_EXTENSION) + YAML_EXTENSION;
File targetFile = new File(templatesDir, fileName);
// lets escape any {{ or }} characters to avoid creating invalid templates
// let's escape any {{ or }} characters to avoid creating invalid templates
String text = FileUtils.readFileToString(file, Charset.defaultCharset());
text = escapeYamlTemplate(text);
FileUtils.write(targetFile, text, Charset.defaultCharset());
Expand All @@ -356,7 +376,7 @@ private static void splitAndSaveTemplate(Template template, File templatesDir) t

private void createChartYaml(HelmConfig helmConfig, File outputDir) throws IOException {
final Chart chartFromHelmConfig = chartFromHelmConfig(helmConfig);
final Chart chartFromFragment = readFragment(CHART_FRAGMENT_PATTERN, Chart.class);
final Chart chartFromFragment = readFragment(resolveHelmFragment(CHART_FRAGMENT_PATTERN, resourceServiceConfig), Chart.class);
final Chart mergedChart = Serialization.merge(chartFromHelmConfig, chartFromFragment);
ResourceUtil.save(new File(outputDir, CHART_FILENAME), mergedChart, ResourceFileType.yaml);
}
Expand All @@ -378,8 +398,7 @@ private static Chart chartFromHelmConfig(HelmConfig helmConfig) {
.build();
}

private <T> T readFragment(Pattern filePattern, Class<T> type) {
final File helmChartFragment = resolveHelmFragment(filePattern, resourceServiceConfig);
private <T> T readFragment(File helmChartFragment, Class<T> type) {
if (helmChartFragment != null) {
try {
return Serialization.unmarshal(
Expand All @@ -392,18 +411,22 @@ private <T> T readFragment(Pattern filePattern, Class<T> type) {
}

private static File resolveHelmFragment(Pattern filePattern, ResourceServiceConfig resourceServiceConfig) {
return findHelmFragmentsInResourceDir(filePattern, resourceServiceConfig).findAny().orElse(null);
}

private static Stream<File> findHelmFragmentsInResourceDir(Pattern filePattern, ResourceServiceConfig resourceServiceConfig) {
final List<File> fragmentDirs = resourceServiceConfig.getResourceDirs();
if (fragmentDirs != null) {
for (File fragmentDir : fragmentDirs) {
if (fragmentDir.exists() && fragmentDir.isDirectory()) {
final File[] fragments = fragmentDir.listFiles((dir, name) -> filePattern.matcher(name).matches());
if (fragments != null) {
return Stream.of(fragments).filter(File::exists).findAny().orElse(null);
return Stream.of(fragments).filter(File::exists);
}
}
}
}
return null;
return Stream.empty();
}

private static void copyAdditionalFiles(HelmConfig helmConfig, File outputDir) throws IOException {
Expand Down Expand Up @@ -451,7 +474,7 @@ private void createValuesYaml(List<HelmParameter> helmParameters, File outputDir
// Placeholders replaced by Go expressions don't need to be persisted in the values.yaml file
.filter(hp -> !hp.isGolangExpression())
.collect(Collectors.toMap(HelmParameter::getName, HelmParameter::getValue));
final Map<String, Object> valuesFromFragment = readFragment(VALUES_FRAGMENT_PATTERN, Map.class);
final Map<String, Object> valuesFromFragment = readFragment(resolveHelmFragment(VALUES_FRAGMENT_PATTERN, resourceServiceConfig), Map.class);
final Map<String, Object> mergedValues = Serialization.merge(getNestedMap(valuesFromParameters), valuesFromFragment);
final Map<String, Object> sortedValues = sortValuesYaml(mergedValues);
ResourceUtil.save(new File(outputDir, VALUES_FILENAME), sortedValues, ResourceFileType.yaml);
Expand Down
Loading

0 comments on commit 6ddd463

Please sign in to comment.