From 05444df6156543efc2e2e57d7c399ed5d50050d0 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 4 Oct 2023 07:59:54 -0500 Subject: [PATCH] feat: enhance Helm Install to support multiple set and values parameters (#367) Signed-off-by: Jeromy Cannon --- .../helm/client/execution/HelmExecution.java | 79 +++++++++---------- .../execution/HelmExecutionBuilder.java | 74 ++++++++++++----- .../fullstack/helm/client/model/Chart.java | 7 -- .../model/install/InstallChartOptions.java | 11 ++- .../install/InstallChartOptionsBuilder.java | 19 ++++- .../client/resource/HelmSoftwareLoader.java | 18 ++--- .../helm/client/test/HelmClientTest.java | 66 +++++++++------- .../execution/HelmExecutionBuilderTest.java | 59 ++++++++++++++ .../test/execution/HelmExecutionTest.java | 28 +++++++ .../model/InstallChartOptionsBuilderTest.java | 24 +++++- .../chart/ChartInstallRequestTest.java | 63 ++++++++++++++- .../service/locator/api/ArtifactLoader.java | 38 ++++----- 12 files changed, 352 insertions(+), 134 deletions(-) create mode 100644 fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionBuilderTest.java diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java index 405896200..1e2d0d22a 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java @@ -218,30 +218,29 @@ public T responseAs(final Class responseClass, final Duration timeout) { } if (exitCode() != 0) { - throw new HelmExecutionException(exitCode()); + throw new HelmExecutionException( + exitCode(), + StreamUtils.streamToString(suppressExceptions(this::standardOutput)), + StreamUtils.streamToString(suppressExceptions(this::standardError))); } - final String standardOutput = StreamUtils.streamToString(suppressExceptions(this::standardOutput)); final String standardError = StreamUtils.streamToString(suppressExceptions(this::standardError)); - LOGGER.atDebug() - .setMessage( - "ResponseAs exiting with exitCode: {}\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.debug( + "ResponseAs exiting with exitCode: {}TODO\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + responseClass.getName(), + standardOutput, + standardError); try { return OBJECT_MAPPER.readValue(standardOutput, responseClass); } catch (final Exception e) { - LOGGER.atWarn() - .setMessage("ResponseAs failed to deserialize response into class: {}\n\tresponse: {}") - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .setCause(e) - .log(); + LOGGER.warn( + String.format( + "ResponseAs failed to deserialize response into class: %s%n\tresponse: %s", + responseClass.getName(), standardOutput), + e); throw new HelmParserException(String.format(MSG_DESERIALIZATION_ERROR, responseClass.getName()), e); } @@ -291,14 +290,12 @@ public List responseAsList(final Class responseClass, final Duration t final String standardOutput = StreamUtils.streamToString(suppressExceptions(this::standardOutput)); final String standardError = StreamUtils.streamToString(suppressExceptions(this::standardError)); - LOGGER.atDebug() - .setMessage( - "ResponseAsList exiting with exitCode: {}\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.debug( + "ResponseAsList exiting with exitCode: {}\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + responseClass.getName(), + standardOutput, + standardError); try { return OBJECT_MAPPER @@ -306,13 +303,11 @@ public List responseAsList(final Class responseClass, final Duration t .readValues(standardOutput) .readAll(); } catch (final Exception e) { - LOGGER.atWarn() - .setMessage( - "ResponseAsList failed to deserialize the output into a list of the specified class: {}\n\tresponse: {}") - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .setCause(e) - .log(); + LOGGER.warn( + String.format( + "ResponseAsList failed to deserialize the output into a list of the specified class: %s%n\tresponse: %s", + responseClass.getName(), standardOutput), + e); throw new HelmParserException(String.format(MSG_LIST_DESERIALIZATION_ERROR, responseClass.getName()), e); } @@ -349,20 +344,18 @@ public void call(final Duration timeout) { final String standardOutput = StreamUtils.streamToString(suppressExceptions(this::standardOutput)); final String standardError = StreamUtils.streamToString(suppressExceptions(this::standardError)); - LOGGER.atDebug() - .setMessage("Call exiting with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.debug( + "Call exiting with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + standardOutput, + standardError); if (exitCode() != 0) { - LOGGER.atWarn() - .setMessage("Call failed with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.warn( + "Call failed with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + standardOutput, + standardError); throw new HelmExecutionException(exitCode(), standardError, standardOutput); } diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java index 58a4751b7..d4bd2ad85 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java @@ -16,7 +16,9 @@ package com.hedera.fullstack.helm.client.execution; +import com.hedera.fullstack.base.api.collections.KeyValuePair; import com.hedera.fullstack.helm.client.HelmConfigurationException; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.*; @@ -28,7 +30,8 @@ */ public final class HelmExecutionBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(HelmExecutionBuilder.class); - + public static final String NAME_MUST_NOT_BE_NULL = "name must not be null"; + public static final String VALUE_MUST_NOT_BE_NULL = "value must not be null"; /** * The path to the helm executable. */ @@ -42,7 +45,12 @@ public final class HelmExecutionBuilder { /** * The arguments to be passed to the helm command. */ - private final Map arguments; + private final HashMap arguments; + + /** + * The list of options and a list of their one or more values. + */ + private final List>> optionsWithMultipleValues; /** * The flags to be passed to the helm command. @@ -73,10 +81,15 @@ public HelmExecutionBuilder(final Path helmExecutable) { this.helmExecutable = Objects.requireNonNull(helmExecutable, "helmExecutable must not be null"); this.subcommands = new ArrayList<>(); this.arguments = new HashMap<>(); + this.optionsWithMultipleValues = new ArrayList<>(); this.positionals = new ArrayList<>(); this.flags = new ArrayList<>(); this.environmentVariables = new HashMap<>(); - this.workingDirectory = this.helmExecutable.getParent(); + + String workingDirectoryString = System.getenv("PWD"); + this.workingDirectory = (workingDirectoryString == null || workingDirectoryString.isBlank()) + ? this.helmExecutable.getParent() + : new File(workingDirectoryString).toPath(); } /** @@ -100,12 +113,28 @@ public HelmExecutionBuilder subcommands(final String... commands) { * @throws NullPointerException if either {@code name} or {@code value} is {@code null}. */ public HelmExecutionBuilder argument(final String name, final String value) { - Objects.requireNonNull(name, "name must not be null"); - Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(name, NAME_MUST_NOT_BE_NULL); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); this.arguments.put(name, value); return this; } + /** + * Adds an option with a provided list of values to the helm command. This is used for options that have can have + * multiple values for a single option. (e.g. --set and --values) + * + * @param name the name of the option. + * @param value the list of values for the given option. + * @return this builder. + * @throws NullPointerException if either {@code name} or {@code value} is {@code null}. + */ + public HelmExecutionBuilder optionsWithMultipleValues(final String name, final List value) { + Objects.requireNonNull(name, NAME_MUST_NOT_BE_NULL); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); + this.optionsWithMultipleValues.add(new KeyValuePair<>(name, value)); + return this; + } + /** * Adds a positional argument to the helm command. * @@ -114,7 +143,7 @@ public HelmExecutionBuilder argument(final String name, final String value) { * @throws NullPointerException if {@code value} is {@code null}. */ public HelmExecutionBuilder positional(final String value) { - Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); this.positionals.add(value); return this; } @@ -128,20 +157,20 @@ public HelmExecutionBuilder positional(final String value) { * @throws NullPointerException if either {@code name} or {@code value} is {@code null}. */ public HelmExecutionBuilder environmentVariable(final String name, final String value) { - Objects.requireNonNull(name, "name must not be null"); - Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(name, NAME_MUST_NOT_BE_NULL); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); this.environmentVariables.put(name, value); return this; } /** - * Sets the working directory for the helm process. + * Sets the Path of the working directory for the helm process. * - * @param workingDirectory the working directory. + * @param workingDirectoryPath the Path of the working directory. * @return this builder. */ - public HelmExecutionBuilder workingDirectory(final Path workingDirectory) { - this.workingDirectory = Objects.requireNonNull(workingDirectory, "workingDirectory must not be null"); + public HelmExecutionBuilder workingDirectory(final Path workingDirectoryPath) { + this.workingDirectory = Objects.requireNonNull(workingDirectoryPath, "workingDirectoryPath must not be null"); return this; } @@ -191,18 +220,23 @@ private String[] buildCommand() { command.addAll(subcommands); command.addAll(flags); - for (final Map.Entry entry : arguments.entrySet()) { - command.add(String.format("--%s", entry.getKey())); - command.add(entry.getValue()); - } + arguments.forEach((key, value) -> { + command.add(String.format("--%s", key)); + command.add(value); + }); + + optionsWithMultipleValues.forEach(entry -> entry.value().forEach(value -> { + command.add(String.format("--%s", entry.key())); + command.add(value); + })); command.addAll(positionals); String[] commandArray = command.toArray(new String[0]); - LOGGER.atDebug() - .setMessage("Helm command: {}") - .addArgument(String.join(" ", Arrays.copyOfRange(commandArray, 1, commandArray.length))) - .log(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Helm command: {}", String.join(" ", Arrays.copyOfRange(commandArray, 1, commandArray.length))); + } return commandArray; } diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java index ab42ca286..1f199a5b8 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java @@ -16,19 +16,12 @@ package com.hedera.fullstack.helm.client.model; -import java.util.Objects; - /** * Represents a chart and is used to interact with the Helm install and uninstall commands. * @param repoName the name of repository which contains the Helm chart. * @param name the name of the Helm chart. */ public record Chart(String name, String repoName) { - - public Chart { - Objects.requireNonNull(repoName, "repoName must not be null"); - } - public Chart(String name) { this(name, null); } diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java index 802687861..2a3ac661e 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java @@ -18,6 +18,7 @@ import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; import com.hedera.fullstack.helm.client.model.Options; +import java.util.List; /** * The options to be supplied to the helm install command. @@ -32,6 +33,7 @@ * @param passCredentials - pass credentials to all domains. * @param password - chart repository password where to locate the requested chart. * @param repo - chart repository url where to locate the requested chart. + * @param set - set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) * @param skipCrds - if set, no CRDs will be installed. By default, CRDs are installed if not already present. * @param timeout - time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s). * @param username - chart repository username where to locate the requested chart. @@ -54,10 +56,11 @@ public record InstallChartOptions( boolean passCredentials, String password, String repo, + List set, boolean skipCrds, String timeout, String username, - String values, + List values, boolean verify, String version, boolean waitFor) @@ -94,6 +97,10 @@ public void apply(final HelmExecutionBuilder builder) { builder.argument("repo", repo()); } + if (set() != null) { + builder.optionsWithMultipleValues("set", set()); + } + if (timeout() != null) { builder.argument("timeout", timeout()); } @@ -103,7 +110,7 @@ public void apply(final HelmExecutionBuilder builder) { } if (values() != null) { - builder.argument("values", values()); + builder.optionsWithMultipleValues("values", values()); } if (version() != null) { diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java index afec8d648..524eca9f9 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java @@ -16,6 +16,8 @@ package com.hedera.fullstack.helm.client.model.install; +import java.util.List; + /** * The builder for the {@link InstallChartOptions}. */ @@ -29,10 +31,11 @@ public final class InstallChartOptionsBuilder { private boolean passCredentials; private String password; private String repo; + private List set; private boolean skipCrds; private String timeout; private String username; - private String values; + private List values; private boolean verify; private String version; private boolean waitFor; @@ -149,6 +152,17 @@ public InstallChartOptionsBuilder repo(String repo) { return this; } + /** + * set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + * + * @param valueOverride set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + * @return the current InstallChartOptionsBuilder. + */ + public InstallChartOptionsBuilder set(List valueOverride) { + this.set = valueOverride; + return this; + } + /** * if set, no CRDs will be installed. By default, CRDs are installed if not already present. * @@ -188,7 +202,7 @@ public InstallChartOptionsBuilder username(String username) { * @param values specify values in a YAML file or a URL (can specify multiple). * @return the current InstallChartOptionsBuilder. */ - public InstallChartOptionsBuilder values(String values) { + public InstallChartOptionsBuilder values(List values) { this.values = values; return this; } @@ -247,6 +261,7 @@ public InstallChartOptions build() { passCredentials, password, repo, + set, skipCrds, timeout, username, diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java index 74e270c6e..cfd0063fd 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java @@ -63,9 +63,10 @@ private HelmSoftwareLoader() { * Unpacks the Helm executable contained in the JAR file into a temporary directory. * * @return the path to the Helm executable. - * @throws HelmConfigurationException if the Helm executable cannot be unpacked or the - * operating system/architecture combination is not supported. - * @implNote This method expects the executable to be present at the following location in the JAR file: {@code /software///helm}. + * @throws HelmConfigurationException if the Helm executable cannot be unpacked or the operating system/architecture + * combination is not supported. + * @implNote This method expects the executable to be present at the following location in the JAR file: + * {@code /software///helm}. */ public static Path installSupportedVersion() { try { @@ -86,12 +87,11 @@ public static Path installSupportedVersion() { pathBuilder.append(".exe"); } - LOGGER.atDebug() - .setMessage("Loading Helm executable from JAR file. [os={}, arch={}, path={}]") - .addArgument(os.name()) - .addArgument(arch.name()) - .addArgument(pathBuilder.toString()) - .log(); + LOGGER.debug( + "Loading Helm executable from JAR file. [os={}, arch={}, path={}]", + os.name(), + arch.name(), + pathBuilder); return RESOURCE_LOADER.load(pathBuilder.toString()); } catch (IOException | SecurityException | IllegalStateException e) { diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java index c634e3cbf..475baa18e 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java @@ -54,7 +54,11 @@ class HelmClientTest { private static final String HAPROXY_RELEASE_NAME = "haproxy-release"; - private static HelmClient defaultClient; + private static final Repository INCUBATOR_REPOSITORY = + new Repository("incubator", "https://charts.helm.sh/incubator"); + + private static final Repository JETSTACK_REPOSITORY = new Repository("jetstack", "https://charts.jetstack.io"); + private static HelmClient helmClient; private static final int INSTALL_TIMEOUT = 10; private static final List EXPECTED_LOG_ENTRIES = List.of( @@ -83,8 +87,9 @@ private record ChartInstallOptionsTestParameters(InstallChartOptions options, Li @BeforeAll static void beforeAll() { - defaultClient = HelmClient.defaultClient(); - assertThat(defaultClient).isNotNull(); + helmClient = + HelmClient.builder().defaultNamespace("helm-client-test-ns").build(); + assertThat(helmClient).isNotNull(); } void removeRepoIfPresent(HelmClient client, Repository repo) { @@ -94,10 +99,17 @@ void removeRepoIfPresent(HelmClient client, Repository repo) { } } + void addRepoIfMissing(HelmClient client, Repository repo) { + final List repositories = client.listRepositories(); + if (!repositories.contains(repo)) { + client.addRepository(repo); + } + } + @Test @DisplayName("Version Command Executes Successfully") void testVersionCommand() { - final SemanticVersion helmVersion = defaultClient.version(); + final SemanticVersion helmVersion = helmClient.version(); assertThat(helmVersion).isNotNull().isNotEqualTo(SemanticVersion.ZERO); assertThat(helmVersion.major()).isGreaterThanOrEqualTo(3); @@ -108,27 +120,27 @@ void testVersionCommand() { @Test @DisplayName("Repository List Executes Successfully") void testRepositoryListCommand() { - final List repositories = defaultClient.listRepositories(); + final List repositories = helmClient.listRepositories(); assertThat(repositories).isNotNull(); } @Test @DisplayName("Repository Add Executes Successfully") void testRepositoryAddCommand() { - final int originalRepoListSize = defaultClient.listRepositories().size(); - removeRepoIfPresent(defaultClient, INGRESS_REPOSITORY); + final int originalRepoListSize = helmClient.listRepositories().size(); + removeRepoIfPresent(helmClient, INCUBATOR_REPOSITORY); try { - assertThatNoException().isThrownBy(() -> defaultClient.addRepository(INGRESS_REPOSITORY)); - final List repositories = defaultClient.listRepositories(); + assertThatNoException().isThrownBy(() -> helmClient.addRepository(INCUBATOR_REPOSITORY)); + final List repositories = helmClient.listRepositories(); assertThat(repositories) .isNotNull() .isNotEmpty() - .contains(INGRESS_REPOSITORY) + .contains(INCUBATOR_REPOSITORY) .hasSize(originalRepoListSize + 1); } finally { - assertThatNoException().isThrownBy(() -> defaultClient.removeRepository(INGRESS_REPOSITORY)); - final List repositories = defaultClient.listRepositories(); + assertThatNoException().isThrownBy(() -> helmClient.removeRepository(INCUBATOR_REPOSITORY)); + final List repositories = helmClient.listRepositories(); assertThat(repositories).isNotNull().hasSize(originalRepoListSize); } } @@ -136,19 +148,19 @@ void testRepositoryAddCommand() { @Test @DisplayName("Repository Remove Executes With Error") void testRepositoryRemoveCommand_WithError(final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, INGRESS_REPOSITORY); + removeRepoIfPresent(helmClient, JETSTACK_REPOSITORY); - int existingRepoCount = defaultClient.listRepositories().size(); + int existingRepoCount = helmClient.listRepositories().size(); final String expectedMessage; if (existingRepoCount == 0) { expectedMessage = "Error: no repositories configured"; } else { - expectedMessage = String.format("Error: no repo named \"%s\" found", INGRESS_REPOSITORY.name()); + expectedMessage = String.format("Error: no repo named \"%s\" found", JETSTACK_REPOSITORY.name()); } assertThatException() - .isThrownBy(() -> defaultClient.removeRepository(INGRESS_REPOSITORY)) + .isThrownBy(() -> helmClient.removeRepository(JETSTACK_REPOSITORY)) .withStackTraceContaining(expectedMessage); LoggingOutputAssert.assertThat(loggingOutput) .hasAtLeastOneEntry(List.of( @@ -166,19 +178,17 @@ void testRepositoryRemoveCommand_WithError(final LoggingOutput loggingOutput) { @DisplayName("Install Chart Executes Successfully") @Timeout(INSTALL_TIMEOUT) void testInstallChartCommand(final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, HAPROXYTECH_REPOSITORY); + addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY); try { - assertThatNoException().isThrownBy(() -> defaultClient.addRepository(HAPROXYTECH_REPOSITORY)); - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - Release release = defaultClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); + Release release = helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART); assertThat(release).isNotNull(); assertThat(release.name()).isEqualTo(HAPROXY_RELEASE_NAME); assertThat(release.info().description()).isEqualTo("Install complete"); assertThat(release.info().status()).isEqualTo("deployed"); } finally { - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - suppressExceptions(() -> defaultClient.removeRepository(HAPROXYTECH_REPOSITORY)); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); } LoggingOutputAssert.assertThat(loggingOutput).hasAtLeastOneEntry(EXPECTED_LOG_ENTRIES); } @@ -186,16 +196,14 @@ void testInstallChartCommand(final LoggingOutput loggingOutput) { private static void testChartInstallWithCleanup( InstallChartOptions options, List expectedLogEntries, final LoggingOutput loggingOutput) { try { - assertThatNoException().isThrownBy(() -> defaultClient.addRepository(HAPROXYTECH_REPOSITORY)); - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - Release release = defaultClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, options); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); + Release release = helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, options); assertThat(release).isNotNull(); assertThat(release.name()).isEqualTo(HAPROXY_RELEASE_NAME); assertThat(release.info().description()).isEqualTo("Install complete"); assertThat(release.info().status()).isEqualTo("deployed"); } finally { - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - suppressExceptions(() -> defaultClient.removeRepository(HAPROXYTECH_REPOSITORY)); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); } LoggingOutputAssert.assertThat(loggingOutput).hasAtLeastOneEntry(expectedLogEntries); } @@ -205,7 +213,7 @@ private static void testChartInstallWithCleanup( @MethodSource @DisplayName("Parameterized Chart Installation with Options Executes Successfully") void testChartInstallOptions(ChartInstallOptionsTestParameters parameters, final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, HAPROXYTECH_REPOSITORY); + addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY); testChartInstallWithCleanup(parameters.options(), parameters.expectedLogEntries(), loggingOutput); } @@ -329,7 +337,7 @@ static Stream> testChartInstallOptions( @DisplayName("Install Chart with Provenance Validation") @Disabled("Provenance validation is not supported in our unit tests due to lack of signed charts.") void testInstallChartWithProvenanceValidation(final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, HAPROXYTECH_REPOSITORY); + addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY); final InstallChartOptions options = InstallChartOptions.builder().createNamespace(true).verify(true).build(); diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionBuilderTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionBuilderTest.java new file mode 100644 index 000000000..ee443fc5e --- /dev/null +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionBuilderTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.fullstack.helm.client.test.execution; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; +import java.io.File; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HelmExecutionBuilderTest { + @Test + @DisplayName("Test optionsWithMultipleValues null checks") + void testOptionsWithMultipleValuesNullChecks() { + HelmExecutionBuilder builder = new HelmExecutionBuilder(new File(".").toPath()); + assertThrows(NullPointerException.class, () -> { + builder.optionsWithMultipleValues(null, null); + }); + assertThrows(NullPointerException.class, () -> { + builder.optionsWithMultipleValues("test string", null); + }); + } + + @Test + @DisplayName("Test environmentVariable null checks") + void testEnvironmentVariableNullChecks() { + HelmExecutionBuilder builder = new HelmExecutionBuilder(new File(".").toPath()); + assertThrows(NullPointerException.class, () -> { + builder.environmentVariable(null, null); + }); + assertThrows(NullPointerException.class, () -> { + builder.environmentVariable("test string", null); + }); + } + + @Test + @DisplayName("Test workingDirectory null checks") + void testWorkingDirectoryNullChecks() { + HelmExecutionBuilder builder = new HelmExecutionBuilder(new File(".").toPath()); + assertThrows(NullPointerException.class, () -> { + builder.workingDirectory(null); + }); + } +} diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java index 53f7d74f3..a11bddd80 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java @@ -29,6 +29,7 @@ import com.jcovalent.junit.logging.LogEntryBuilder; import com.jcovalent.junit.logging.LoggingOutput; import com.jcovalent.junit.logging.assertj.LoggingOutputAssert; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.time.Duration; @@ -138,4 +139,31 @@ void testResponseAsWarnMessage(final LoggingOutput loggingOutput) throws Interru .message("ResponseAs exiting with exitCode: 0") .build())); } + + @Test + @DisplayName("Test response as throws HelmExecutionException with standard error and standard out") + void testResponseAsThrowsHelmExecutionException() throws InterruptedException, IOException { + doReturn(inputStreamMock).when(processMock).getInputStream(); + doReturn(inputStreamMock).when(processMock).getErrorStream(); + final HelmExecution helmExecution = Mockito.spy(new HelmExecution(processMock)); + final Duration timeout = Duration.ofSeconds(1); + doReturn(1).when(helmExecution).exitCode(); + doReturn(true).when(helmExecution).waitFor(any(Duration.class)); + String standardOutputMessage = "standardOutput Message"; + doReturn(new ByteArrayInputStream(standardOutputMessage.getBytes())) + .when(helmExecution) + .standardOutput(); + String standardErrorMessage = "standardError Message"; + doReturn(new ByteArrayInputStream(standardErrorMessage.getBytes())) + .when(helmExecution) + .standardError(); + + HelmExecutionException exception = assertThrows(HelmExecutionException.class, () -> { + helmExecution.responseAs(Repository.class, timeout); + }); + + assertThat(exception.getMessage()).contains("Execution of the Helm command failed with exit code: 1"); + assertThat(exception.getStdOut()).contains(standardOutputMessage); + assertThat(exception.getStdErr()).contains(standardErrorMessage); + } } diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java index 0589985c2..09442bb2c 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java @@ -17,12 +17,24 @@ package com.hedera.fullstack.helm.client.test.model; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; import com.hedera.fullstack.helm.client.model.install.InstallChartOptions; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class InstallChartOptionsBuilderTest { + @Mock + private HelmExecutionBuilder builderMock; @Test @DisplayName("Test InstallChartOptionsBuilder") @@ -37,10 +49,11 @@ void testInstallChartOptionsBuilder() { .passCredentials(true) .password("password") .repo("repo") + .set(List.of("set", "livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt]")) .skipCrds(true) .timeout("timeout") .username("username") - .values("values") + .values(List.of("values1", "values2")) .verify(true) .version("version") .waitFor(true) @@ -55,12 +68,19 @@ void testInstallChartOptionsBuilder() { assertTrue(options.passCredentials()); assertEquals("password", options.password()); assertEquals("repo", options.repo()); + assertTrue(options.set().stream().anyMatch("livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt]"::equals)); + assertTrue(options.set().stream().anyMatch("set"::equals)); assertTrue(options.skipCrds()); assertEquals("timeout", options.timeout()); assertEquals("username", options.username()); - assertEquals("values", options.values()); + assertTrue(options.values().stream().anyMatch("values1"::equals)); + assertTrue(options.values().stream().anyMatch("values2"::equals)); assertTrue(options.verify()); assertEquals("version", options.version()); assertTrue(options.waitFor()); + + options.apply(builderMock); + + verify(builderMock, times(2)).optionsWithMultipleValues(anyString(), anyList()); } } diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java index a25a464cb..2b176a5f8 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java @@ -17,15 +17,32 @@ package com.hedera.fullstack.helm.client.test.proxy.request.chart; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; import com.hedera.fullstack.helm.client.model.Chart; import com.hedera.fullstack.helm.client.model.install.InstallChartOptions; import com.hedera.fullstack.helm.client.proxy.request.chart.ChartInstallRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class ChartInstallRequestTest { + @Mock + InstallChartOptions installChartOptionsMock; + + @Mock + Chart chartMock; + + @Mock + HelmExecutionBuilder helmExecutionBuilderMock; + @Test @DisplayName("Test ChartInstallRequest Chart constructor") void testChartInstallRequestChartConstructor() { @@ -44,4 +61,48 @@ void testChartInstallRequestChartConstructor() { .isEqualTo(opts) .isNotEqualTo(InstallChartOptions.defaults()); } + + @Test + @DisplayName("Test ChartInstallRequest apply with unqualified chart") + void testChartInstallRequestApplyUnqualifiedChart() { + final ChartInstallRequest chartInstallRequest = + new ChartInstallRequest("mocked", chartMock, installChartOptionsMock); + assertThat(chartInstallRequest).isNotNull(); + assertThat(chartInstallRequest.chart()).isNotNull().isEqualTo(chartMock); + assertThat(chartInstallRequest.releaseName()).isEqualTo("mocked"); + assertThat(chartInstallRequest.options()).isNotNull().isEqualTo(installChartOptionsMock); + + when(installChartOptionsMock.repo()).thenReturn("mockedRepo"); + when(chartMock.unqualified()).thenReturn("mockedUnqualified"); + when(helmExecutionBuilderMock.positional("mocked")).thenReturn(helmExecutionBuilderMock); + when(helmExecutionBuilderMock.positional("mockedUnqualified")).thenReturn(helmExecutionBuilderMock); + chartInstallRequest.apply(helmExecutionBuilderMock); + verify(helmExecutionBuilderMock, times(1)).subcommands("install"); + verify(installChartOptionsMock, times(1)).apply(helmExecutionBuilderMock); + verify(installChartOptionsMock, times(2)).repo(); + verify(chartMock, times(1)).unqualified(); + verify(helmExecutionBuilderMock, times(2)).positional(anyString()); + } + + @Test + @DisplayName("Test ChartInstallRequest apply with qualified chart") + void testChartInstallRequestApplyQualifiedChart() { + final ChartInstallRequest chartInstallRequest = + new ChartInstallRequest("mocked", chartMock, installChartOptionsMock); + assertThat(chartInstallRequest).isNotNull(); + assertThat(chartInstallRequest.chart()).isNotNull().isEqualTo(chartMock); + assertThat(chartInstallRequest.releaseName()).isEqualTo("mocked"); + assertThat(chartInstallRequest.options()).isNotNull().isEqualTo(installChartOptionsMock); + + when(installChartOptionsMock.repo()).thenReturn(null); + when(chartMock.qualified()).thenReturn("mockedQualified"); + when(helmExecutionBuilderMock.positional("mocked")).thenReturn(helmExecutionBuilderMock); + when(helmExecutionBuilderMock.positional("mockedQualified")).thenReturn(helmExecutionBuilderMock); + chartInstallRequest.apply(helmExecutionBuilderMock); + verify(helmExecutionBuilderMock, times(1)).subcommands("install"); + verify(installChartOptionsMock, times(1)).apply(helmExecutionBuilderMock); + verify(installChartOptionsMock, times(1)).repo(); + verify(chartMock, times(1)).qualified(); + verify(helmExecutionBuilderMock, times(2)).positional(anyString()); + } } diff --git a/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java b/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java index 1380a3d8f..7c99984e7 100644 --- a/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java +++ b/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java @@ -240,13 +240,14 @@ private void identifyArtifacts(final boolean recursive) { .map(Path::toAbsolutePath) .forEach(this::addArtifact); } catch (final IOException e) { - LOGGER.atWarn() - .setCause(e) - .log("Failed to walk directory, skipping artifact identification [ path = '{}' ]", current); + LOGGER.warn( + String.format( + "Failed to walk directory, skipping artifact identification [ path = '%s' ]", + current), + e); } } else { - LOGGER.atWarn() - .log("Skipping artifact identification, file is not a JAR archive [ path = '{}' ]", current); + LOGGER.warn("Skipping artifact identification, file is not a JAR archive [ path = '{}' ]", current); } } } @@ -266,12 +267,11 @@ private void addArtifact(final Path artifact) { classPath.add(artifact); } } catch (final IOException e) { - LOGGER.atWarn() - .setCause(e) - .log( - "Failed to identify artifact, an I/O error occurred [ fileName = '{}', path = '{}' ]", - artifact.getFileName(), - artifact); + LOGGER.warn( + String.format( + "Failed to identify artifact, an I/O error occurred [ fileName = '%s', path = '%s' ]", + artifact.getFileName(), artifact), + e); } } @@ -308,11 +308,11 @@ private void loadClassPath() { try { return uri.toURL(); } catch (final MalformedURLException e) { - LOGGER.atWarn() - .setCause(e) - .log( - "Failed to convert path to URL, unable to load class path entry [ path = '{}' ]", - uri.getPath()); + LOGGER.warn( + String.format( + "Failed to convert path to URL, unable to load class path entry [ path = '%s' ]", + uri.getPath()), + e); return null; } }) @@ -331,7 +331,7 @@ private void loadModules(final ModuleLayer parentLayer) { Objects.requireNonNull(parentLayer, "parentLayer must not be null"); if (modulePath.isEmpty()) { - LOGGER.atDebug().log("No module path entries found, skipping module layer creation"); + LOGGER.debug("No module path entries found, skipping module layer creation"); return; } @@ -341,10 +341,10 @@ private void loadModules(final ModuleLayer parentLayer) { parentLayer.configuration().resolveAndBind(finder, ModuleFinder.of(), Collections.emptySet()); moduleLayer = parentLayer.defineModulesWithOneLoader(cfg, classLoader); } catch (LayerInstantiationException | SecurityException e) { - LOGGER.atError().setCause(e).log("Failed to instantiate module layer, unable to load module path entries"); + LOGGER.error("Failed to instantiate module layer, unable to load module path entries", e); throw new ArtifactLoadingException(e); } catch (FindException | ResolutionException e) { - LOGGER.atError().setCause(e).log("Failed to resolve modules, unable to load module path entries"); + LOGGER.error("Failed to resolve modules, unable to load module path entries", e); throw new ArtifactLoadingException(e); } }