Skip to content

Commit

Permalink
feat (jkube-kit/resource) : Support for Chart.yml fragments (eclipse-…
Browse files Browse the repository at this point in the history
…jkube#2092)

Signed-off-by: Rohan Kumar <[email protected]>
  • Loading branch information
rohanKanojia authored and manusa committed Jun 12, 2023
1 parent 899e047 commit 1480c75
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 22 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.13-SNAPSHOT
* Fix #1478: Should detect and warn the user if creating ingress and ingress controller not available
* Fix #2092: Support for `Chart.yaml` fragments
* Fix #2150: Bump Kubernetes Client to 6.6.0 (fixes issues when trace-logging OpenShift builds)
* Fix #2162: Bump Kubernetes Client to 6.6.1 (HttpClient with support for PUT + InputStream)
* Fix #2165: Introduce a Kubernetes resource Security Hardening profile (opt-in)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ protected JKubeServiceHub.JKubeServiceHubBuilder initJKubeServiceHubBuilder() {
.reactorProjects(Collections.singletonList(kubernetesExtension.javaProject))
.sourceDirectory(kubernetesExtension.getBuildSourceDirectoryOrDefault())
.outputDirectory(kubernetesExtension.getBuildOutputDirectoryOrDefault())
.resolvedResourceSourceDirs(ResourceUtil.getFinalResourceDirs(kubernetesExtension.getResourceSourceDirectoryOrDefault(), kubernetesExtension.getResourceEnvironmentOrNull()))
.registryConfig(RegistryConfig.builder()
.settings(Collections.emptyList())
.authConfig(kubernetesExtension.authConfig != null ? kubernetesExtension.authConfig.toMap() : null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class JKubeConfiguration implements Serializable {
private Map<String, String> buildArgs;
private RegistryConfig registryConfig;
private List<JavaProject> reactorProjects;
private List<File> resolvedResourceSourceDirs;

public File getBasedir() {
return project.getBaseDirectory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Properties;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -89,6 +90,32 @@ void inSourceDir_withRelativePath_shouldReturnResolvedPath() {
assertThat(result).isEqualTo(new File("/src/other"));
}

@Test
void builder() {
// Given
JKubeConfiguration.JKubeConfigurationBuilder builder = JKubeConfiguration.builder()
.project(JavaProject.builder().artifactId("test-project").build())
.sourceDirectory("src/main/jkube")
.outputDirectory("target")
.buildArgs(Collections.singletonMap("foo", "bar"))
.registryConfig(RegistryConfig.builder()
.registry("r.example.com")
.build())
.resolvedResourceSourceDirs(Collections.singletonList(new File("src/main/jkube")));

// When
JKubeConfiguration jKubeConfiguration = builder.build();

// Then
assertThat(jKubeConfiguration)
.hasFieldOrPropertyWithValue("project.artifactId", "test-project")
.hasFieldOrPropertyWithValue("sourceDirectory", "src/main/jkube")
.hasFieldOrPropertyWithValue("outputDirectory", "target")
.hasFieldOrPropertyWithValue("buildArgs", Collections.singletonMap("foo", "bar"))
.hasFieldOrPropertyWithValue("registryConfig.registry", "r.example.com")
.hasFieldOrPropertyWithValue("resolvedResourceSourceDirs", Collections.singletonList(new File("src/main/jkube")));
}

/**
* Verifies that deserialization works for raw deserialization disregarding annotations.
*/
Expand Down
7 changes: 6 additions & 1 deletion jkube-kit/doc/src/main/asciidoc/inc/helm/_jkube_helm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ If you have already built the resource then you can omit this task.

endif::[]

The configuration is defined in a `helm` section within the plugin's configuration:
In order to configure generated chart you need to provide chart related configuration in form of {plugin-configuration-type} to {plugin}.

You can also provide a `Chart.yaml` fragment in `src/main/jkube` directory, {plugin} would merge it's contents with the
opinionated defaults to create final chart.

The {plugin-configuration-type} configuration is defined in a `helm` section within the plugin's configuration:

ifeval::["{plugin-type}" == "maven"]
include::maven/_example_helm_config.adoc[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,11 @@ private KubernetesResourceUtil() { }

static final Map<String,String> FILENAME_TO_KIND_MAPPER = new HashMap<>();
static final Map<String,String> KIND_TO_FILENAME_MAPPER = new HashMap<>();
static final Set<String> EXCLUDED_RESOURCE_FILENAMES = new HashSet<>();

static {
initializeKindFilenameMapper();
initializeExcludedResourceFilenames();
}

/**
Expand All @@ -158,7 +160,8 @@ public static KubernetesListBuilder readResourceFragmentsFrom(

final KubernetesListBuilder builder = new KubernetesListBuilder();
if (resourceFiles != null) {
for (File file : resourceFiles) {
List<File> filteredResourceFiles = getFilteredResourceFiles(resourceFiles);
for (File file : filteredResourceFiles) {
builder.addToItems(getResource(platformMode, apiVersions, file, defaultName));
}
}
Expand Down Expand Up @@ -197,6 +200,10 @@ protected static void initializeKindFilenameMapper() {
updateKindFilenameMapper(mappings);
}

private static void initializeExcludedResourceFilenames() {
EXCLUDED_RESOURCE_FILENAMES.add(".helm.yaml");
}

protected static void remove(String kind, String filename) {
FILENAME_TO_KIND_MAPPER.remove(filename);
KIND_TO_FILENAME_MAPPER.remove(kind);
Expand Down Expand Up @@ -932,6 +939,18 @@ public static void mergeMetadata(HasMetadata item1, HasMetadata item2) {
}
}

private static List<File> getFilteredResourceFiles(File[] resourceFiles) {
List<File> filteredResourceFiles = new ArrayList<>();
for (File resourceFile : resourceFiles) {
boolean hasNoExcludedFileNamePattern = EXCLUDED_RESOURCE_FILENAMES.stream()
.noneMatch(s -> resourceFile.getName().endsWith(s));
if (hasNoExcludedFileNamePattern) {
filteredResourceFiles.add(resourceFile);
}
}
return filteredResourceFiles;
}

/**
* Returns a merge of the given maps and then removes any resulting empty string values (which is the way to remove, say, a label or annotation
* when overriding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,17 @@ void readResourceFragmentsFrom_withValidDirectory_shouldReadAllFragments() throw
tuple(GenericKubernetesResource.class, "jkube/v1", "CustomKind", "custom")
);
}

@Test
void readResourceFragmentsFrom_withExcludedFile_shouldNotIncludeExcludedFile() throws IOException {
// Given
File[] resourceFiles = new File[] { new File("Chart.helm.yaml")};
// When
final KubernetesListBuilder result = KubernetesResourceUtil.readResourceFragmentsFrom(
kubernetes, DEFAULT_RESOURCE_VERSIONING, "pong", resourceFiles);
// Then
assertThat(result.buildItems()).isEmpty();
}

@Test
void mergePodSpec_withFragmentWithContainerNameAndSidecarDisabled_shouldPreserveContainerNameFromFragment() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.fabric8.kubernetes.client.utils.Serialization;
import org.eclipse.jkube.kit.common.JKubeConfiguration;
import org.eclipse.jkube.kit.common.KitLogger;
import org.eclipse.jkube.kit.common.RegistryConfig;
Expand All @@ -49,7 +51,10 @@
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;


import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.eclipse.jkube.kit.common.JKubeFileInterpolator.DEFAULT_FILTER;
import static org.eclipse.jkube.kit.common.JKubeFileInterpolator.interpolate;
import static org.eclipse.jkube.kit.common.util.MapUtil.getNestedMap;
import static org.eclipse.jkube.kit.common.util.TemplateUtil.escapeYamlTemplate;
import static org.eclipse.jkube.kit.resource.helm.HelmServiceUtil.isRepositoryValid;
Expand All @@ -60,6 +65,7 @@ public class HelmService {

private static final String YAML_EXTENSION = ".yaml";
private static final String CHART_API_VERSION = "v1";
private static final String FRAGMENT_FILE_EXTENSION = ".helm.yaml";
private static final String CHART_FILENAME = "Chart" + YAML_EXTENSION;
private static final String VALUES_FILENAME = "values" + YAML_EXTENSION;
private static final String GOLANG_EXPRESSION_REGEX = "\\{\\{.+}}";
Expand Down Expand Up @@ -94,7 +100,7 @@ public void generateHelmCharts(HelmConfig helmConfig) throws IOException {
logger.debug("Processing source files");
processSourceFiles(sourceDir, templatesDir);
logger.debug("Creating %s", CHART_FILENAME);
createChartYaml(helmConfig, outputDir);
createChartYaml(helmConfig, outputDir, jKubeConfiguration);
logger.debug("Copying additional files");
copyAdditionalFiles(helmConfig, outputDir);
logger.debug("Gathering parameters for placeholders");
Expand Down Expand Up @@ -230,8 +236,65 @@ private static void splitAndSaveTemplate(Template template, File templatesDir) t
}
}

static void createChartYaml(HelmConfig helmConfig, File outputDir) throws IOException {
final Chart chart = new Chart();
static void createChartYaml(HelmConfig helmConfig, File outputDir, JKubeConfiguration jKubeConfiguration) throws IOException {
final Chart chartFromHelmConfig = createChartFromHelmConfig(helmConfig);
final Chart chartFromFragment = createChartFromFragment(jKubeConfiguration);
final Chart mergedChart = mergeCharts(chartFromHelmConfig, chartFromFragment);

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

private static Chart mergeCharts(Chart chartFromHelmConfig, Chart chartFromFragment) {
if (chartFromFragment == null) {
return chartFromHelmConfig;
}
Chart.ChartBuilder chartBuilder = chartFromFragment.toBuilder();
updateOriginal((c1, c2) -> StringUtils.isBlank(c1.getApiVersion()) && StringUtils.isNotBlank(c2.getApiVersion()),
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.apiVersion(chartFromHelmConfig.getApiVersion()));
updateOriginal((c1, c2) -> StringUtils.isBlank(c1.getName()) && StringUtils.isNotBlank(c2.getName()),
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.name(chartFromHelmConfig.getName()));
updateOriginal((c1, c2) -> StringUtils.isBlank(c1.getHome()) && StringUtils.isNotBlank(c2.getHome()),
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.home(chartFromHelmConfig.getHome()));
updateOriginal((c1, c2) -> StringUtils.isBlank(c1.getVersion()) && StringUtils.isNotBlank(c2.getVersion()),
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.version(chartFromHelmConfig.getVersion()));
updateOriginal((c1, c2) -> StringUtils.isBlank(c1.getDescription()) && StringUtils.isNotBlank(c2.getDescription()),
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.description(chartFromHelmConfig.getDescription()));
updateOriginal((c1, c2) -> StringUtils.isBlank(c1.getEngine()) && StringUtils.isNotBlank(c2.getEngine()),
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.engine(chartFromHelmConfig.getEngine()));
updateOriginal((c1, c2) -> StringUtils.isBlank(c1.getIcon()) && StringUtils.isNotBlank(c2.getIcon()),
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.icon(chartFromHelmConfig.getIcon()));
updateOriginal((c1, c2) -> c1.getSources() == null && c2.getSources() != null,
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.sources(chartFromHelmConfig.getSources()));
updateOriginal((c1, c2) -> c1.getKeywords() == null && c2.getKeywords() != null,
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.keywords(chartFromHelmConfig.getKeywords()));
updateOriginal((c1, c2) -> c1.getMaintainers() == null && c2.getMaintainers() != null,
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.maintainers(chartFromHelmConfig.getMaintainers()));
updateOriginal((c1, c2) -> c1.getDependencies() == null && c2.getDependencies() != null,
chartBuilder, chartFromFragment, chartFromHelmConfig,
cb -> cb.dependencies(chartFromHelmConfig.getDependencies()));

return chartBuilder.build();
}

private static void updateOriginal(BiPredicate<Chart, Chart> condition, Chart.ChartBuilder chartBuilder, Chart original, Chart opinionated, Consumer<Chart.ChartBuilder> chartBuilderConsumer) {
if (condition.test(original, opinionated)) {
chartBuilderConsumer.accept(chartBuilder);
}
}

private static Chart createChartFromHelmConfig(HelmConfig helmConfig) {
Chart chart = new Chart();
chart.setApiVersion(CHART_API_VERSION);
chart.setName(helmConfig.getChart());
chart.setVersion(helmConfig.getVersion());
Expand All @@ -243,9 +306,34 @@ static void createChartYaml(HelmConfig helmConfig, File outputDir) throws IOExce
chart.setKeywords(helmConfig.getKeywords());
chart.setEngine(helmConfig.getEngine());
chart.setDependencies(helmConfig.getDependencies());
return chart;
}

File outputChartFile = new File(outputDir, CHART_FILENAME);
ResourceUtil.save(outputChartFile, chart, ResourceFileType.yaml);
private static Chart createChartFromFragment(JKubeConfiguration jKubeConfiguration) {
File helmChartFragment = getChartYamlFileFromFragmentsDir(jKubeConfiguration.getResolvedResourceSourceDirs());
if (helmChartFragment != null && helmChartFragment.exists()) {
try {
String interpolatedFragmentContent = interpolate(helmChartFragment, jKubeConfiguration.getProperties(), DEFAULT_FILTER);
return Serialization.yamlMapper().readValue(interpolatedFragmentContent, Chart.class);
} catch (IOException e) {
throw new IllegalArgumentException("Failure in parsing Helm Chart fragment : " + e.getMessage());
}
}
return null;
}

private static File getChartYamlFileFromFragmentsDir(List<File> fragmentDirs) {
File foundChartYamlFile = null;
if (fragmentDirs != null) {
Optional<File> fileOptional = fragmentDirs.stream()
.map(f -> new File(f, String.format("%s%s", "Chart", FRAGMENT_FILE_EXTENSION)))
.filter(File::exists)
.findFirst();
if (fileOptional.isPresent()) {
foundChartYamlFile = fileOptional.get();
}
}
return foundChartYamlFile;
}

private static void copyAdditionalFiles(HelmConfig helmConfig, File outputDir) throws IOException {
Expand Down
Loading

0 comments on commit 1480c75

Please sign in to comment.