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 committed May 4, 2023
1 parent e944780 commit 9cfd5a2
Show file tree
Hide file tree
Showing 13 changed files with 238 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Usage:
./scripts/extract-changelog-for-version.sh 1.3.37 5
```
### 1.13-SNAPSHOT
* Fix #2092: Support for `Chart.yaml` fragments

### 1.12.0 (2023-04-03)
* Fix #1179: Move storageClass related functionality out of VolumePermissionEnricher to PersistentVolumeClaimStorageClassEnricher
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 @@ -36,6 +36,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -138,9 +139,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 @@ -160,7 +163,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 @@ -199,6 +203,10 @@ protected static void initializeKindFilenameMapper() {
updateKindFilenameMapper(mappings);
}

private static void initializeExcludedResourceFilenames() {
EXCLUDED_RESOURCE_FILENAMES.add("chart.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 @@ -944,6 +952,12 @@ public static void mergeMetadata(HasMetadata item1, HasMetadata item2) {
}
}

private static List<File> getFilteredResourceFiles(File[] resourceFiles) {
return Arrays.stream(resourceFiles)
.filter(f -> !EXCLUDED_RESOURCE_FILENAMES.contains(f.getName().toLowerCase(Locale.ROOT)))
.collect(Collectors.toList());
}

/**
* 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 @@ -327,6 +327,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.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 @@ -48,7 +50,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 Down Expand Up @@ -93,7 +98,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 @@ -229,8 +234,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 @@ -242,9 +304,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, CHART_FILENAME))
.filter(File::exists)
.findFirst();
if (fileOptional.isPresent()) {
foundChartYamlFile = fileOptional.get();
}
}
return foundChartYamlFile;
}

private static void copyAdditionalFiles(HelmConfig helmConfig, File outputDir) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;

import org.eclipse.jkube.kit.common.JKubeConfiguration;
import org.eclipse.jkube.kit.common.JavaProject;
import org.eclipse.jkube.kit.common.KitLogger;
import org.eclipse.jkube.kit.common.Maintainer;
import org.eclipse.jkube.kit.common.RegistryConfig;
import org.eclipse.jkube.kit.common.RegistryServerConfiguration;
import org.eclipse.jkube.kit.common.ResourceFileType;
Expand All @@ -35,6 +39,7 @@
import org.mockito.MockedStatic;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -87,7 +92,7 @@ void createChartYaml() throws Exception {
// Given
helmConfig.chart("Chart Name").version("1337");
// When
HelmService.createChartYaml(helmConfig.build(), outputDir);
HelmService.createChartYaml(helmConfig.build(), outputDir, jKubeConfiguration);
// Then
ArgumentCaptor<Chart> argumentCaptor = ArgumentCaptor.forClass(Chart.class);
resourceUtilMockedStatic.verify(() -> ResourceUtil.save(notNull(), argumentCaptor.capture(), eq(ResourceFileType.yaml)));
Expand All @@ -98,6 +103,66 @@ void createChartYaml() throws Exception {
}
}

@Test
void createChartYaml_whenInvalidChartYamlFragmentProvided_thenThrowException() throws Exception {
try (MockedStatic<ResourceUtil> ignored = mockStatic(ResourceUtil.class)) {
// Given
File fragmentsDir = new File(Objects.requireNonNull(getClass().getResource("/invalid-helm-fragments")).getFile());
File outputDir = Files.createTempDirectory("chart-output").toFile();
jKubeConfiguration = jKubeConfiguration.toBuilder()
.project(JavaProject.builder().properties(new Properties()).build())
.resolvedResourceSourceDirs(Collections.singletonList(fragmentsDir))
.build();
// When + Then
assertThatIllegalArgumentException()
.isThrownBy(() -> HelmService.createChartYaml(helmConfig.build(), outputDir, jKubeConfiguration))
.withMessageContaining("Failure in parsing Helm Chart fragment : ");
}
}

@Test
void createChartYaml_whenValidChartYamlFragmentProvided_thenMergeFragmentChart() throws Exception {
try (MockedStatic<ResourceUtil> resourceUtilMockedStatic = mockStatic(ResourceUtil.class)) {
// Given
File fragmentsDir = new File(Objects.requireNonNull(getClass().getResource("/valid-helm-fragments")).getFile());
File outputDir = Files.createTempDirectory("chart-output").toFile();
Properties properties = new Properties();
properties.put("chart.name", "name-from-fragment");
jKubeConfiguration = jKubeConfiguration.toBuilder()
.project(JavaProject.builder().properties(properties).build())
.resolvedResourceSourceDirs(Collections.singletonList(fragmentsDir))
.build();
helmConfig
.chart("Chart Name")
.version("1337")
.description("Description from helmconfig")
.home("https://example.com")
.sources(Collections.singletonList("https://source.example.com"))
.keywords(Collections.singletonList("ci"))
.maintainers(Collections.singletonList(Maintainer.builder().name("maintainer-from-config").build()))
.icon("test-icon")
.engine("gotpl")
.dependencies(Collections.singletonList(HelmDependency.builder().name("dependency-from-config").build()));
// When
HelmService.createChartYaml(helmConfig.build(), outputDir, jKubeConfiguration);
// Then
ArgumentCaptor<Chart> argumentCaptor = ArgumentCaptor.forClass(Chart.class);
resourceUtilMockedStatic.verify(() -> ResourceUtil.save(notNull(), argumentCaptor.capture(), eq(ResourceFileType.yaml)));
assertThat(argumentCaptor.getValue())
.hasFieldOrPropertyWithValue("apiVersion", "v1")
.hasFieldOrPropertyWithValue("name", "name-from-fragment")
.hasFieldOrPropertyWithValue("version", "1337")
.hasFieldOrPropertyWithValue("description", "Description from helmconfig")
.hasFieldOrPropertyWithValue("home", "https://example.com")
.hasFieldOrPropertyWithValue("icon", "test-icon")
.hasFieldOrPropertyWithValue("engine", "gotpl")
.hasFieldOrPropertyWithValue("keywords", Collections.singletonList("ci"))
.hasFieldOrPropertyWithValue("sources", Collections.singletonList("https://source.example.com"))
.hasFieldOrPropertyWithValue("maintainers", Collections.singletonList(Maintainer.builder().name("maintainer-from-config").build()))
.hasFieldOrPropertyWithValue("dependencies", Collections.singletonList(HelmDependency.builder().name("dependency-from-config").build()));
}
}

@Test
void createChartYamlWithDependencies() throws Exception {
try (MockedStatic<ResourceUtil> resourceUtilMockedStatic = mockStatic(ResourceUtil.class)) {
Expand All @@ -112,7 +177,7 @@ void createChartYamlWithDependencies() throws Exception {
helmConfig.chart("Chart Name").version("1337")
.dependencies(Collections.singletonList(helmDependency));
// When
HelmService.createChartYaml(helmConfig.build(), outputDir);
HelmService.createChartYaml(helmConfig.build(), outputDir, jKubeConfiguration);
// Then
ArgumentCaptor<Chart> argumentCaptor = ArgumentCaptor.forClass(Chart.class);
resourceUtilMockedStatic.verify(() -> ResourceUtil.save(notNull(), argumentCaptor.capture(), eq(ResourceFileType.yaml)));
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: ${chart.name}
Loading

0 comments on commit 9cfd5a2

Please sign in to comment.