Skip to content

Commit

Permalink
feat: add helm dependency update subcommand (#377)
Browse files Browse the repository at this point in the history
Signed-off-by: Jeromy Cannon <jeromy@swirldslabs.com>
  • Loading branch information
jeromy-cannon authored Oct 5, 2023
1 parent 5d08a32 commit 2b3609d
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ default Release installChart(String releaseName, Chart chart) {
*/
void testChart(String releaseName, TestChartOptions options);

/**
* Executes the Helm CLI {@code dependency update} sub-command and updates the dependencies of the specified Helm
* chart.
*
* @param chartName the name of the chart to update.
*/
void dependencyUpdate(String chartName);

/**
* Creates a new {@link HelmClientBuilder} instance with the default configuration.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ public interface HelmClientBuilder {
*/
HelmClientBuilder defaultNamespace(String namespace);

/**
* Sets the working directory for the {@link HelmClient} instance.
* @param workingDirectory the working directory.
* @return the {@link HelmClientBuilder} instance.
* @implNote The working directory is set to the pwd if not explicitly provided, if that fails it will use the
* parent folder of the helm executable.
*/
HelmClientBuilder workingDirectory(Path workingDirectory);

/**
* Sets the Kubernetes API server address and port number for the {@link HelmClient} instance.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.hedera.fullstack.helm.client.model.test.TestChartOptions;
import com.hedera.fullstack.helm.client.proxy.request.HelmRequest;
import com.hedera.fullstack.helm.client.proxy.request.authentication.KubeAuthentication;
import com.hedera.fullstack.helm.client.proxy.request.chart.ChartDependencyUpdateRequest;
import com.hedera.fullstack.helm.client.proxy.request.chart.ChartInstallRequest;
import com.hedera.fullstack.helm.client.proxy.request.chart.ChartTestRequest;
import com.hedera.fullstack.helm.client.proxy.request.chart.ChartUninstallRequest;
Expand Down Expand Up @@ -69,6 +70,11 @@ public final class DefaultHelmClient implements HelmClient {
*/
private final String defaultNamespace;

/**
* The working directory to use when executing Helm commands.
*/
private final Path workingDirectory;

/**
* Creates a new instance of the {@link DefaultHelmClient} class.
*
Expand All @@ -78,9 +84,26 @@ public final class DefaultHelmClient implements HelmClient {
*/
public DefaultHelmClient(
final Path helmExecutable, final KubeAuthentication authentication, final String defaultNamespace) {
this(helmExecutable, authentication, defaultNamespace, null);
}

/**
* Creates a new instance of the {@link DefaultHelmClient} class.
*
* @param helmExecutable the path to the Helm executable.
* @param authentication the authentication configuration to use when executing Helm commands.
* @param defaultNamespace the default namespace to use when executing Helm commands.
* @param workingDirectory the working directory to use when executing Helm commands.
*/
public DefaultHelmClient(
final Path helmExecutable,
final KubeAuthentication authentication,
final String defaultNamespace,
final Path workingDirectory) {
this.helmExecutable = Objects.requireNonNull(helmExecutable, "helmExecutable must not be null");
this.authentication = Objects.requireNonNull(authentication, "authentication must not be null");
this.defaultNamespace = defaultNamespace;
this.workingDirectory = workingDirectory;
}

@Override
Expand Down Expand Up @@ -130,6 +153,14 @@ public void testChart(final String releaseName, final TestChartOptions options)
});
}

@Override
public void dependencyUpdate(final String chartName) {
executeInternal(new ChartDependencyUpdateRequest(chartName), Void.class, (b, c) -> {
b.call();
return null;
});
}

/**
* Applies the default namespace and authentication configuration to the given builder.
*
Expand All @@ -140,11 +171,16 @@ private void applyBuilderDefaults(final HelmExecutionBuilder builder) {
builder.argument(NAMESPACE_ARG_NAME, defaultNamespace);
}

if (workingDirectory != null) {
builder.workingDirectory(workingDirectory);
}

authentication.apply(builder);
}

/**
* Executes the given request and returns the response as the given class. The request is executed using the default namespace.
* Executes the given request and returns the response as the given class. The request is executed using the default
* namespace.
*
* @param request the request to execute.
* @param responseClass the class of the response.
Expand Down Expand Up @@ -172,7 +208,8 @@ private <T extends HelmRequest, R> R execute(
}

/**
* Executes the given request and returns the response as a list of the given class. The request is executed using the default namespace.
* Executes the given request and returns the response as a list of the given class. The request is executed using
* the default namespace.
*
* @param request the request to execute.
* @param responseClass the class of the response.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public final class DefaultHelmClientBuilder implements HelmClientBuilder {
*/
private String defaultNamespace;

/**
* The working directory to be used by the {@link HelmClient}.
*/
private Path workingDirectory;

/**
* The kubernetes API server address and port number to which the client should connect. Defaults to a {@code null}
* value which indicates that the Helm {@code --kube-apiserver <address_and_port>} argument should not be specified.
Expand Down Expand Up @@ -86,6 +91,12 @@ public HelmClientBuilder defaultNamespace(final String namespace) {
return this;
}

@Override
public HelmClientBuilder workingDirectory(final Path workingDirectory) {
this.workingDirectory = workingDirectory;
return this;
}

@Override
public HelmClientBuilder kubeApiServer(String kubeApiServer) {
this.kubeApiServer = kubeApiServer;
Expand Down Expand Up @@ -139,6 +150,6 @@ public HelmClient build() {
kubeTlsServerName,
kubeToken,
kubeConfig);
return new DefaultHelmClient(helmExecutable, kubeAuthentication, defaultNamespace);
return new DefaultHelmClient(helmExecutable, kubeAuthentication, defaultNamespace, workingDirectory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.proxy.request.chart;

import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder;
import com.hedera.fullstack.helm.client.proxy.request.HelmRequest;
import java.util.Objects;

/**
* A request to do a dependency update on a chart.
*
* @param chartName the name of the chart to update.
*/
public record ChartDependencyUpdateRequest(String chartName) implements HelmRequest {
public ChartDependencyUpdateRequest {
Objects.requireNonNull(chartName, "chartName must not be null");
if (chartName.isBlank()) {
throw new IllegalArgumentException("chartName must not be blank");
}
}

@Override
public void apply(HelmExecutionBuilder builder) {
builder.subcommands("dependency", "update");
builder.positional(chartName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@

import static com.hedera.fullstack.base.api.util.ExceptionUtils.suppressExceptions;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Named.named;

import com.hedera.fullstack.base.api.version.SemanticVersion;
import com.hedera.fullstack.helm.client.HelmClient;
import com.hedera.fullstack.helm.client.HelmExecutionException;
import com.hedera.fullstack.helm.client.model.Chart;
import com.hedera.fullstack.helm.client.model.Repository;
import com.hedera.fullstack.helm.client.model.chart.Release;
Expand All @@ -32,6 +34,7 @@
import com.jcovalent.junit.logging.LogEntryBuilder;
import com.jcovalent.junit.logging.LoggingOutput;
import com.jcovalent.junit.logging.assertj.LoggingOutputAssert;
import java.io.File;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.*;
Expand Down Expand Up @@ -88,8 +91,10 @@ private record ChartInstallOptionsTestParameters(InstallChartOptions options, Li

@BeforeAll
static void beforeAll() {
helmClient =
HelmClient.builder().defaultNamespace("helm-client-test-ns").build();
helmClient = HelmClient.builder()
.defaultNamespace("helm-client-test-ns")
.workingDirectory(new File(".").toPath())
.build();
assertThat(helmClient).isNotNull();
}

Expand Down Expand Up @@ -359,4 +364,21 @@ void testTestChartWithOptions() {
suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME));
}
}

@Test
@DisplayName("Test Helm dependency update subcommand")
void testHelmDependencyUpdate() {
helmClient.dependencyUpdate("../charts/hedera-network");
}

@Test
@DisplayName("Test Helm dependency build subcommand failure")
void testHelmDependencyBuildFailure() {
HelmExecutionException exception =
assertThrows(HelmExecutionException.class, () -> helmClient.dependencyUpdate("../charts/not-a-chart"));
assertThat(exception.getMessage()).contains("Execution of the Helm command failed with exit code: 1");
assertThat(exception.getStdOut())
.contains(
"Error: could not find ../charts/not-a-chart: stat ../charts/not-a-chart: no such file or directory");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.proxy.request.chart;

import static org.assertj.core.api.Assertions.assertThat;
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.proxy.request.chart.ChartDependencyUpdateRequest;
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 ChartDependencyUpdateRequestTest {
@Mock
HelmExecutionBuilder helmExecutionBuilderMock;

@Test
@DisplayName("Verify ChartDependencyUpdateRequest apply")
void testChartDependencyUpdateRequestApply() {
final ChartDependencyUpdateRequest request = new ChartDependencyUpdateRequest("mocked");
assertThat(request).isNotNull();
assertThat(request.chartName()).isEqualTo("mocked");

when(helmExecutionBuilderMock.subcommands("dependency", "update")).thenReturn(helmExecutionBuilderMock);
when(helmExecutionBuilderMock.positional("mocked")).thenReturn(helmExecutionBuilderMock);
request.apply(helmExecutionBuilderMock);
verify(helmExecutionBuilderMock, times(1)).subcommands("dependency", "update");
verify(helmExecutionBuilderMock, times(1)).positional("mocked");
}
}

0 comments on commit 2b3609d

Please sign in to comment.