Skip to content

Commit

Permalink
feat: HelmService provides linting features
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa committed Feb 6, 2024
1 parent 005241e commit 13e01f8
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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
*/
package org.eclipse.jkube.kit.common;

public class JKubeException extends RuntimeException {

public JKubeException(String message) {
super(message);
}

}
4 changes: 4 additions & 0 deletions jkube-kit/helm/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>com.marcnuri.helm-java</groupId>
<artifactId>helm-java</artifactId>
</dependency>

<dependency>
<groupId>org.eclipse.jkube</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public class HelmConfig {
private HelmRepository stableRepository;
private HelmRepository snapshotRepository;
private String security;
private boolean lintStrict;
private boolean lintQuiet;


@JsonProperty("dependencies")
private List<HelmDependency> dependencies;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand All @@ -28,7 +30,11 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.marcnuri.helm.Helm;
import com.marcnuri.helm.LintCommand;
import com.marcnuri.helm.LintResult;
import org.eclipse.jkube.kit.common.JKubeConfiguration;
import org.eclipse.jkube.kit.common.JKubeException;
import org.eclipse.jkube.kit.common.KitLogger;
import org.eclipse.jkube.kit.common.RegistryConfig;
import org.eclipse.jkube.kit.common.RegistryServerConfiguration;
Expand Down Expand Up @@ -155,25 +161,52 @@ public void uploadHelmChart(HelmConfig helm) throws BadUploadException, IOExcept
}
}

public void lint(HelmConfig helmConfig) {
for (HelmConfig.HelmType helmType : helmConfig.getTypes()) {
final Path helmPackage = resolveTarballFile(helmConfig, helmType);
logger.info("Linting %s %s", helmConfig.getChart(), helmConfig.getVersion());
logger.info("Using packaged file: %s", helmPackage.toFile().getAbsolutePath());
final LintCommand lintCommand = new Helm(helmPackage).lint();
if (helmConfig.isLintStrict()) {
lintCommand.strict();
}
if (helmConfig.isLintQuiet()) {
lintCommand.quiet();
}
final LintResult lintResult = lintCommand.call();
if (lintResult.isFailed()) {
for (String message : lintResult.getMessages()) {
// [[W]] see AnsiUtil.COLOR_MAP and computeEmphasisColor to understand the color guides
logger.error("[[W]]%s", message);
}
throw new JKubeException("Linting failed");
} else {
for (String message : lintResult.getMessages()) {
logger.info("[[W]]%s", message);
}
logger.info("Linting successful");
}
}
}

private void uploadHelmChart(HelmConfig helmConfig, HelmRepository helmRepository)
throws IOException, BadUploadException {

final HelmUploaderManager helmUploaderManager = new HelmUploaderManager();
for (HelmConfig.HelmType helmType : helmConfig.getTypes()) {
logger.info("Uploading Helm Chart \"%s\" to %s", helmConfig.getChart(), helmRepository.getName());
logger.debug("OutputDir: %s", helmConfig.getOutputDir());

final File tarballOutputDir =
new File(Objects.requireNonNull(helmConfig.getTarballOutputDir(),
"Tarball output directory is required"), helmType.getOutputDir());
final File tarballFile = new File(tarballOutputDir, String.format("%s-%s%s.%s",
helmConfig.getChart(), helmConfig.getVersion(), resolveHelmClassifier(helmConfig), helmConfig.getChartExtension()));

helmUploaderManager.getHelmUploader(helmRepository.getType()).uploadSingle(tarballFile, helmRepository);
helmUploaderManager.getHelmUploader(helmRepository.getType())
.uploadSingle(resolveTarballFile(helmConfig, helmType).toFile(), helmRepository);
logger.info("Upload Successful");
}
}

private static Path resolveTarballFile(HelmConfig helmConfig, HelmConfig.HelmType helmType) {
return Paths.get(Objects.requireNonNull(helmConfig.getTarballOutputDir(), "Tarball output directory is required"))
.resolve(helmType.getOutputDir())
.resolve(String.format("%s-%s%s.%s", helmConfig.getChart(), helmConfig.getVersion(), resolveHelmClassifier(helmConfig), helmConfig.getChartExtension()));
}

static File prepareSourceDir(HelmConfig helmConfig, HelmConfig.HelmType type) throws IOException {
final File sourceDir = new File(helmConfig.getSourceDir(), type.getSourceDir());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -73,6 +74,9 @@ public class HelmServiceUtil {
protected static final String PROPERTY_SECURITY = "jkube.helm.security";
protected static final String DEFAULT_SECURITY = "~/.m2/settings-security.xml";

protected static final String PROPERTY_HELM_LINT_STRICT = "jkube.helm.lint.strict";
protected static final String PROPERTY_HELM_LINT_QUIET = "jkube.helm.lint.quiet";

private HelmServiceUtil() { }

public static HelmConfig.HelmConfigBuilder initHelmConfig(
Expand Down Expand Up @@ -110,6 +114,8 @@ public static HelmConfig.HelmConfigBuilder initHelmConfig(
helmConfig.setTarFileClassifier(resolveFromPropertyOrDefault(PROPERTY_TARBALL_CLASSIFIER, project, helmConfig::getTarFileClassifier, () -> EMPTY));
helmConfig.setTarballOutputDir(resolveFromPropertyOrDefault(PROPERTY_TARBALL_OUTPUT_DIR, project, helmConfig::getTarballOutputDir,
helmConfig::getOutputDir));
helmConfig.setLintStrict(resolveBooleanFromPropertyOrDefault(PROPERTY_HELM_LINT_STRICT, project, helmConfig::isLintStrict));
helmConfig.setLintQuiet(resolveBooleanFromPropertyOrDefault(PROPERTY_HELM_LINT_QUIET, project, helmConfig::isLintQuiet));
return helmConfig.toBuilder();
}

Expand Down Expand Up @@ -165,6 +171,13 @@ static String resolveFromPropertyOrDefault(String property, JavaProject project,
.orElseGet(defaultValue == null ? () -> null : defaultValue));
}

static boolean resolveBooleanFromPropertyOrDefault(String property, JavaProject project, BooleanSupplier getter) {
return Optional.ofNullable(getProperty(property, project))
.filter(StringUtils::isNotBlank)
.map(Boolean::parseBoolean)
.orElse(getter.getAsBoolean());
}

static List<File> getAdditionalFiles(HelmConfig helm, JavaProject project) {
List<File> additionalFiles = new ArrayList<>();
if (helm.getAdditionalFiles() != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
* 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
*/
package org.eclipse.jkube.kit.resource.helm;

import com.marcnuri.helm.Helm;
import org.eclipse.jkube.kit.common.JKubeConfiguration;
import org.eclipse.jkube.kit.common.JKubeException;
import org.eclipse.jkube.kit.common.KitLogger;
import org.eclipse.jkube.kit.config.resource.ResourceServiceConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Comparator;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@DisplayName("HelmService.uploadHelmChart")
class HelmServiceLintIT {

@TempDir
private Path tempDir;
private KitLogger kitLogger;
private Path outputDir;
private HelmConfig helmConfig;
private HelmService helmService;

@BeforeEach
void setUp() {
kitLogger = spy(new KitLogger.SilentLogger());
outputDir = tempDir.resolve("output");
helmConfig = HelmConfig.builder()
.chart("helm-test")
.version("0.1.0")
.chartExtension("tgz")
.types(Arrays.asList(HelmConfig.HelmType.KUBERNETES, HelmConfig.HelmType.OPENSHIFT))
.tarballOutputDir(outputDir.toFile().getAbsolutePath())
.build();
helmService = new HelmService(JKubeConfiguration.builder().build(), new ResourceServiceConfig(), kitLogger);
}

@Nested
class Valid {

@BeforeEach
void validChartPackage() throws IOException {
final Helm helm = Helm.create().withName("helm-test").withDir(tempDir).call();
// Create templates as file (instead of dir) to force a warning
Files.walk(tempDir.resolve("helm-test").resolve("templates"))
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
Files.createFile(tempDir.resolve("helm-test").resolve("templates"));
helm
.packageIt().withDestination(outputDir.resolve("kubernetes")).call()
.packageIt().withDestination(outputDir.resolve("openshift")).call();
}

@Test
void genericInfoMessage() {
helmService.lint(helmConfig);
verify(kitLogger, atLeastOnce())
.info("Linting %s %s", "helm-test", "0.1.0");
}

@Test
void kubernetesPriorInfoMessage() {
helmService.lint(helmConfig);
verify(kitLogger, times(1))
.info(eq("Using packaged file: %s"), endsWith("kubernetes" + File.separator + "helm-test-0.1.0.tgz"));
}

@Test
void openshiftPriorInfoMessage() {
helmService.lint(helmConfig);
verify(kitLogger, times(1))
.info(eq("Using packaged file: %s"), endsWith("openshift" + File.separator + "helm-test-0.1.0.tgz"));
}

@Test
void lintInfoMessageInWhite() {
helmService.lint(helmConfig);
verify(kitLogger, atLeastOnce())
.info("[[W]]%s", "[INFO] Chart.yaml: icon is recommended");
}

@Test
void successMessage() {
helmService.lint(helmConfig);
verify(kitLogger, atLeastOnce()).info("Linting successful");
}

@Nested
class Strict {
@BeforeEach
void setUp() {
helmConfig = helmConfig.toBuilder().lintStrict(true).build();
helmService = new HelmService(JKubeConfiguration.builder().build(), new ResourceServiceConfig(), kitLogger);
}

@Test
void lintErrorMessageInWhite() {
assertThatExceptionOfType(JKubeException.class).isThrownBy(() -> helmService.lint(helmConfig));
verify(kitLogger, atLeastOnce())
.error("[[W]]%s", "[WARNING] templates/: not a directory");
}

@Test
void lintingException() {
assertThatExceptionOfType(JKubeException.class)
.isThrownBy(() -> helmService.lint(helmConfig))
.withMessage("Linting failed");
}
}

@Nested
class Quiet {
@BeforeEach
void setUp() {
helmConfig = helmConfig.toBuilder().lintQuiet(true).build();
helmService = new HelmService(JKubeConfiguration.builder().build(), new ResourceServiceConfig(), kitLogger);
}

@Test
void lintInfoMessageOmitted() {
helmService.lint(helmConfig);
verify(kitLogger, never())
.info("[[W]]%s", "[INFO] Chart.yaml: icon is recommended");
}

@Test
void lintWarnMessage() {
helmService.lint(helmConfig);
verify(kitLogger, atLeastOnce())
.info("[[W]]%s", "[WARNING] templates/: not a directory");
}
}

}

@Nested
class Invalid {

@BeforeEach
void invalidChartPackage() throws IOException {
final Helm chart = Helm.create().withName("helm-test").withDir(tempDir).call();
Files.write(tempDir.resolve("helm-test").resolve("Chart.yaml"),
"\nicon: ://invalid-url".getBytes(StandardCharsets.UTF_8),
StandardOpenOption.APPEND
);
chart
.packageIt().withDestination(outputDir.resolve("kubernetes")).call()
.packageIt().withDestination(outputDir.resolve("openshift")).call();
}

@Test
void genericInfoMessage() {
assertThatExceptionOfType(JKubeException.class).isThrownBy(() -> helmService.lint(helmConfig));
verify(kitLogger, atLeastOnce())
.info("Linting %s %s", "helm-test", "0.1.0");
}

@Test
void kubernetesPriorInfoMessage() {
assertThatExceptionOfType(JKubeException.class).isThrownBy(() -> helmService.lint(helmConfig));
verify(kitLogger, times(1))
.info(eq("Using packaged file: %s"), endsWith("kubernetes" + File.separator + "helm-test-0.1.0.tgz"));
}

@Test
void openshiftPriorInfoMessageNotThrownDueToPriorExceptionHaltingProcessing() {
assertThatExceptionOfType(JKubeException.class).isThrownBy(() -> helmService.lint(helmConfig));
verify(kitLogger, never())
.info(eq("Using packaged file: %s"), endsWith("openshift" + File.separator + "helm-test-0.1.0.tgz"));
}

@Test
void lintErrorMessageInWhite() {
assertThatExceptionOfType(JKubeException.class).isThrownBy(() -> helmService.lint(helmConfig));
verify(kitLogger, atLeastOnce())
.error("[[W]]%s", "[ERROR] Chart.yaml: invalid icon URL '://invalid-url'");
}

@Test
void lintingException() {
assertThatExceptionOfType(JKubeException.class)
.isThrownBy(() -> helmService.lint(helmConfig))
.withMessage("Linting failed");
}
}
}
Loading

0 comments on commit 13e01f8

Please sign in to comment.