Skip to content

Commit

Permalink
feat: support for Helm values.yaml fragments
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <[email protected]>
  • Loading branch information
manusa committed Sep 28, 2023
1 parent 0662bcc commit 543c13b
Show file tree
Hide file tree
Showing 17 changed files with 212 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Usage:
```
### 1.15-SNAPSHOT
* Fix #2138: Support for Spring Boot Native Image
* Fix #2200: Support for Helm `values.yaml` fragments
* Fix #2356: Helm values.yaml parameter names preserve case
* Fix #2369: Helm chart apiVersion can be configured
* Fix #2386: Helm icon inferred from annotations in independent resource files (not aggregated kubernetes/openshift.yaml)
Expand Down
1 change: 1 addition & 0 deletions gradle-plugin/it/src/it/helm-dsl/expected/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--- {}
8 changes: 8 additions & 0 deletions gradle-plugin/it/src/it/helm-fragment-and-dsl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def extensionConfig = {
name = 'repository/helm-dsl:latest'
build {
from = 'repository/from:latest'
ports = [8080]
}
}
}
Expand All @@ -53,6 +54,13 @@ def extensionConfig = {
url = 'https://example.com/user1'
}]
icon = 'https://example.com/icon-is-overridden'
parameters = [{
name = 'replicaCount'
value = 1
}, {
name = 'securityContext'
value = '{{ .Values.deployment.securityContext.privileged }}'
}]
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
replicaCount: "1"
deployment:
securityContext:
privileged: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#
# Copyright (c) 2019 Red Hat, Inc.
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at:
#
# https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Red Hat, Inc. - initial API and implementation
#
spec:
replicas: ${replicaCount}
template:
spec:
containers:
- securityContext:
privileged: ${securityContext}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
deployment:
securityContext:
privileged: false
12 changes: 12 additions & 0 deletions gradle-plugin/it/src/it/helm-fragment/expected/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
replicaCount: 1

image:
repository: nginx

ingress:
enabled: false
className: ""
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
tls: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# Copyright (c) 2019 Red Hat, Inc.
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at:
#
# https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Red Hat, Inc. - initial API and implementation
#

replicaCount: 1

image:
repository: nginx

ingress:
enabled: false
className: ""
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
tls: []
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--- {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--- {}
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ class HelmIT {
"helm-properties",
"helm-zero-config"
})
void k8sResourceHelmFromFragment_whenRun_generatesK8sManifestsAndHelmChart(String projectName) throws Exception {
void k8sResourceHelmFromFragment_whenRun_generatesHelmChart(String projectName) throws Exception {
// When
final BuildResult result = gradleRunner.withITProject(projectName)
.withArguments("clean", "k8sResource", "k8sHelm").build();
// Then
ResourceVerify.verifyResourceDescriptors(gradleRunner.resolveDefaultKubernetesHelmMetadataFile(projectName),
gradleRunner.resolveFile("expected", "Chart.yaml"));
ResourceVerify.verifyResourceDescriptors(
gradleRunner.resolveFile("build", "jkube", "helm", projectName, "kubernetes", "Chart.yaml"),
gradleRunner.resolveFile("expected", "Chart.yaml"));
assertThat(result).extracting(BuildResult::getOutput).asString()
.contains("Using resource templates from")
.contains("Adding a default Deployment")
Expand All @@ -49,6 +50,24 @@ void k8sResourceHelmFromFragment_whenRun_generatesK8sManifestsAndHelmChart(Strin
.contains(String.format("Creating Helm Chart \"%s\" for Kubernetes", projectName));
}

@ParameterizedTest(name = "k8sResource k8sHelm with {0}")
@ValueSource(strings = {
"helm-dsl",
"helm-fragment",
"helm-fragment-and-dsl",
"helm-properties",
"helm-zero-config"
})
void k8sResourceHelmFromFragment_whenRun_generatesHelmValues(String projectName) throws Exception {
// When
final BuildResult result = gradleRunner.withITProject(projectName)
.withArguments("clean", "k8sResource", "k8sHelm").build();
// Then
ResourceVerify.verifyResourceDescriptors(
gradleRunner.resolveFile("build", "jkube", "helm", projectName, "kubernetes", "values.yaml"),
gradleRunner.resolveFile("expected", "values.yaml"));
}

@ParameterizedTest(name = "ocResource ocHelm with {0}")
@ValueSource(strings = {
"helm-dsl",
Expand All @@ -57,13 +76,14 @@ void k8sResourceHelmFromFragment_whenRun_generatesK8sManifestsAndHelmChart(Strin
"helm-properties",
"helm-zero-config"
})
void ocResourceHelmFromFragment_whenRun_generatesOpenShiftManifestsAndHelmChart(String projectName) throws Exception {
void ocResourceHelmFromFragment_whenRun_generatesHelmChart(String projectName) throws Exception {
// When
final BuildResult result = gradleRunner.withITProject(projectName)
.withArguments("clean", "ocResource", "ocHelm").build();
// Then
ResourceVerify.verifyResourceDescriptors(gradleRunner.resolveDefaultOpenShiftHelmMetadataFile(projectName),
gradleRunner.resolveFile("expected", "Chart.yaml"));
ResourceVerify.verifyResourceDescriptors(
gradleRunner.resolveFile("build", "jkube", "helm", projectName, "openshift", "Chart.yaml"),
gradleRunner.resolveFile("expected", "Chart.yaml"));
assertThat(result).extracting(BuildResult::getOutput).asString()
.contains("Using resource templates from")
.contains("Adding a default Deployment")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,6 @@ public File resolveFile(String... relativePaths) {
return path.toFile();
}

public File resolveDefaultKubernetesHelmMetadataFile(String projectName) {
return resolveFile("build", "jkube", "helm", projectName, "kubernetes", "Chart.yaml");
}

public File resolveDefaultOpenShiftHelmMetadataFile(String projectName) {
return resolveFile("build", "jkube", "helm", projectName, "openshift", "Chart.yaml");
}

public File resolveDefaultKubernetesResourceFile() {
return resolveFile("build", "classes", "java", "main", "META-INF", "jkube", "kubernetes.yml");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.jkube.kit.common.JKubeConfiguration;
import org.eclipse.jkube.kit.common.KitLogger;
Expand Down Expand Up @@ -69,6 +69,9 @@ public class HelmService {
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 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 final JKubeConfiguration jKubeConfiguration;
private final ResourceServiceConfig resourceServiceConfig;
private final KitLogger logger;
Expand Down Expand Up @@ -230,11 +233,9 @@ 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 = createChartFromFragment(resourceServiceConfig, jKubeConfiguration.getProperties());
final Chart chartFromFragment = readFragment(CHART_FRAGMENT_PATTERN, Chart.class);
final Chart mergedChart = Serialization.merge(chartFromHelmConfig, chartFromFragment);

final File outputChartFile = new File(outputDir, CHART_FILENAME);
ResourceUtil.save(outputChartFile, mergedChart, ResourceFileType.yaml);
ResourceUtil.save(new File(outputDir, CHART_FILENAME), mergedChart, ResourceFileType.yaml);
}

private static Chart chartFromHelmConfig(HelmConfig helmConfig) {
Expand All @@ -253,14 +254,14 @@ private static Chart chartFromHelmConfig(HelmConfig helmConfig) {
.build();
}

private static Chart createChartFromFragment(ResourceServiceConfig resourceServiceConfig, Properties properties) {
File helmChartFragment = resolveHelmFragment(CHART_FRAGMENT_PATTERN, resourceServiceConfig);
if (helmChartFragment != null && helmChartFragment.exists()) {
private <T> T readFragment(Pattern filePattern, Class<T> type) {
final File helmChartFragment = resolveHelmFragment(filePattern, resourceServiceConfig);
if (helmChartFragment != null) {
try {
String interpolatedFragmentContent = interpolate(helmChartFragment, properties, DEFAULT_FILTER);
return Serialization.unmarshal(interpolatedFragmentContent, Chart.class);
return Serialization.unmarshal(
interpolate(helmChartFragment, jKubeConfiguration.getProperties(), DEFAULT_FILTER), type);
} catch (Exception e) {
throw new IllegalArgumentException("Failure in parsing Helm Chart fragment: " + e.getMessage(), e);
throw new IllegalArgumentException("Failure in parsing Helm fragment (" + helmChartFragment.getName() + "): " + e.getMessage(), e);
}
}
return null;
Expand All @@ -272,8 +273,8 @@ private static File resolveHelmFragment(Pattern filePattern, ResourceServiceConf
for (File fragmentDir : fragmentDirs) {
if (fragmentDir.exists() && fragmentDir.isDirectory()) {
final File[] fragments = fragmentDir.listFiles((dir, name) -> filePattern.matcher(name).matches());
if (fragments != null && fragments.length > 0) {
return fragments[0];
if (fragments != null) {
return Stream.of(fragments).filter(File::exists).findAny().orElse(null);
}
}
}
Expand Down Expand Up @@ -320,15 +321,15 @@ private static void interpolateChartTemplates(List<HelmParameter> helmParameters
}
}

private static void createValuesYaml(List<HelmParameter> helmParameters, File outputDir) throws IOException {
final Map<String, Object> values = helmParameters.stream()
private void createValuesYaml(List<HelmParameter> helmParameters, File outputDir) throws IOException {
final Map<String, Object> valuesFromParameters = helmParameters.stream()
.filter(hp -> hp.getValue() != null)
// 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 File outputValuesFile = new File(outputDir, VALUES_FILENAME);
ResourceUtil.save(outputValuesFile, getNestedMap(values), ResourceFileType.yaml);
final Map<String, Object> valuesFromFragment = readFragment(VALUES_FRAGMENT_PATTERN, Map.class);
final Map<String, Object> mergedValues = Serialization.merge(getNestedMap(valuesFromParameters), valuesFromFragment);
ResourceUtil.save(new File(outputDir, VALUES_FILENAME), mergedValues, ResourceFileType.yaml);
}

private static List<HelmParameter> collectParameters(HelmConfig helmConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -93,8 +94,9 @@ void prepareSourceDirValid_withNonExistentDirectory_shouldThrowException() throw
@Test
void generateHelmCharts() throws IOException {
// Given
helmConfig.types(Collections.singletonList(HelmType.KUBERNETES));
helmConfig.apiVersion("v1337").chart("Chart Name").version("1337");
helmConfig
.types(Collections.singletonList(HelmType.KUBERNETES))
.apiVersion("v1337").chart("Chart Name").version("1337");
// When
helmService.generateHelmCharts(helmConfig.build());
// Then
Expand All @@ -110,32 +112,42 @@ void generateHelmCharts() throws IOException {
}

@Test
void generateHelmCharts_whenInvalidChartYamlFragmentProvided_thenThrowException() {
void generateHelmCharts_withInvalidChartYamlFragmentProvided_throwsException() {
// Given
helmConfig.types(Collections.singletonList(HelmType.KUBERNETES));
resourceServiceConfig = ResourceServiceConfig.builder().resourceDirs(Collections.singletonList(
new File(Objects.requireNonNull(getClass().getResource("/invalid-helm-fragments")).getFile())))
new File(Objects.requireNonNull(getClass().getResource("/invalid-helm-chart-fragment")).getFile())))
.build();
// When + Then
assertThatIllegalArgumentException()
.isThrownBy(() ->
new HelmService(jKubeConfiguration, resourceServiceConfig, new KitLogger.SilentLogger()).generateHelmCharts(helmConfig.build()))
.withMessageContaining("Failure in parsing Helm Chart fragment: ");
.withMessageStartingWith("Failure in parsing Helm fragment (Chart.helm.yaml): ");
}

@Test
void generateHelmCharts_whenValidChartYamlFragmentProvided_thenMergeFragmentChart() throws Exception {
void generateHelmCharts_withInvalidValuesYamlFragmentProvided_throwsException() {
// Given
helmConfig.types(Collections.singletonList(HelmType.KUBERNETES));
Properties properties = new Properties();
properties.put("chart.name", "name-from-fragment");
jKubeConfiguration = jKubeConfiguration.toBuilder()
.project(JavaProject.builder().properties(properties).build())
resourceServiceConfig = ResourceServiceConfig.builder().resourceDirs(Collections.singletonList(
new File(Objects.requireNonNull(getClass().getResource("/invalid-helm-values-fragment")).getFile())))
.build();
// When + Then
assertThatIllegalArgumentException()
.isThrownBy(() ->
new HelmService(jKubeConfiguration, resourceServiceConfig, new KitLogger.SilentLogger()).generateHelmCharts(helmConfig.build()))
.withMessageStartingWith("Failure in parsing Helm fragment (values.helm.yaml): ");
}

@Test
void generateHelmCharts_withValidChartYamlFragment_usesMergedChart() throws Exception {
// Given
jKubeConfiguration.getProject().getProperties().put("chart.name", "name-from-fragment");
resourceServiceConfig = ResourceServiceConfig.builder().resourceDirs(Collections.singletonList(
new File(Objects.requireNonNull(getClass().getResource("/valid-helm-fragments")).getFile())))
.build();
helmConfig
.types(Collections.singletonList(HelmType.KUBERNETES))
.apiVersion("v1")
.chart("Chart Name")
.version("1337")
Expand Down Expand Up @@ -166,6 +178,37 @@ void generateHelmCharts_whenValidChartYamlFragmentProvided_thenMergeFragmentChar
.hasFieldOrPropertyWithValue("dependencies", Collections.singletonList(Collections.singletonMap("name", "dependency-from-config")));
}

@Test
void generateHelmCharts_withValidValuesYamlFragment_usesMergedValues() throws Exception {
// Given
jKubeConfiguration.getProject().getProperties().put("property.in.fragment", "the-value");
resourceServiceConfig = ResourceServiceConfig.builder().resourceDirs(Collections.singletonList(
new File(Objects.requireNonNull(getClass().getResource("/valid-helm-fragments")).getFile())))
.build();
helmConfig
.types(Collections.singletonList(HelmType.KUBERNETES))
.parameters(Arrays.asList(
HelmParameter.builder().name("ingress.name").value("the-ingress-from-parameter").build(),
HelmParameter.builder().name("ingress.enabled").value("is overridden").build()
));
// When
new HelmService(jKubeConfiguration, resourceServiceConfig, new KitLogger.SilentLogger())
.generateHelmCharts(helmConfig.build());
// Then
final Map<?, ?> savedValues = Serialization.unmarshal(helmOutputDirectory.resolve("kubernetes").resolve("values.yaml").toFile(), Map.class);
assertThat(savedValues)
.hasFieldOrPropertyWithValue("replaceableProperty", "the-value")
.hasFieldOrPropertyWithValue("replicaCount", 1)
.hasFieldOrPropertyWithValue("ingress.name", "the-ingress-from-parameter")
.hasFieldOrPropertyWithValue("ingress.enabled", false)
.hasFieldOrPropertyWithValue("ingress.tls", Collections.emptyList())
.extracting("ingress.annotations").asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
.containsOnly(
entry("kubernetes.io/ingress.class", "nginx"),
entry("kubernetes.io/tls-acme", "true")
);
}

@Test
void createChartYamlWithDependencies() throws Exception {
// Given
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid-yaml: invalid: invalid
Loading

0 comments on commit 543c13b

Please sign in to comment.