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

feat: support for Helm values.yaml fragments #2401

Merged
merged 2 commits into from
Sep 29, 2023
Merged
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
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 = 'imagePullPolicy'
value = '{{ .Values.deployment.imagePullPolicy }}'
}]
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
replicaCount: "1"
deployment:
imagePullPolicy: "Always"
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:
- imagePullPolicy: ${imagePullPolicy}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# 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
#

deployment:
imagePullPolicy: Always
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,21 +34,39 @@ 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")
.contains("Adding revision history limit to 2")
.contains("validating")
.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,16 +75,16 @@ 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")
.contains("Adding revision history limit to 2")
.contains("validating")
.contains(String.format("Creating Helm Chart \"%s\" for OpenShift", projectName));
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
36 changes: 27 additions & 9 deletions jkube-kit/doc/src/main/asciidoc/inc/helm/_jkube_helm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@
ifeval::["{plugin-type}" == "maven"]
This goal is for creating
https://helm.sh/docs/topics/charts[Helm charts]
for your Maven project so that you can install, update or delete your app in Kubernetes
for your Maven project so that you can install, update, or delete your app in Kubernetes
using https://github.com/helm/helm[Helm].
For creating a Helm chart you simply call `{goal-prefix}:helm` goal on the command line:
To generate the Helm chart you simply need to call `{goal-prefix}:helm` goal on the command line:

include::maven/_resource_helm.adoc[]
The `{goal-prefix}:resource` goal is required to create the resource descriptors which are included in the Helm chart.
If you have already built the resource then you can omit this goal.

[NOTE]
The `{goal-prefix}:resource` goal is required to create the resource descriptors that are included in the Helm chart.
If you have already generated the resources in a previous step then you can omit this goal.
endif::[]

ifeval::["{plugin-type}" == "gradle"]
This task is for creating
https://helm.sh/docs/topics/charts[Helm charts]
for your Gradle project so that you can install, update or delete your app in Kubernetes
for your Gradle project so that you can install, update, or delete your app in Kubernetes
using https://github.com/helm/helm[Helm].
For creating a Helm chart you simply call `{task-prefix}Helm` task on the command line:
To generate the Helm chart you simply need to call `{task-prefix}Helm` task on the command line:

include::gradle/_resource_helm.adoc[]
The `{task-prefix}Resource` goal is required to create the resource descriptors which are included in the Helm chart.
If you have already built the resource then you can omit this task.

[NOTE]
The `{task-prefix}Resource` goal is required to create the resource descriptors that are included in the Helm chart.
If you have already generated the resources in a previous step then you can omit this task.

endif::[]

Expand Down Expand Up @@ -230,10 +235,23 @@ Defaults to empty string.
| In case we are generating a `.Values` variable, the default value.

In case the placeholder has to be replaced by an expression, the Golang expression
e.g. `{{ .Chart.Name | upper }}`.
e.g. `{{ .Chart.Name \| upper }}`.

|===

=== Helm-specific fragments

In addition to the standard Kubernetes <<introduction-examples-resource-fragments, resource fragments>>, you can also provide fragments for Helm `Chart.yaml` and `values.yaml` files.

For the `Chart.yaml` file you can provide a `Chart.helm.yaml` fragment in the `src/main/jkube` directory.

For the `values.yaml` file you can provide a `values.helm.yaml` fragment in the `src/main/jkube` directory.

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.

=== Installing the generated Helm chart

In a next step you can install this via the https://github.com/helm/helm/releases[helm command line tool] as follows:

ifeval::["{plugin-type}" == "maven"]
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
Loading