From d0d255acb1c2aef86c0a86c7921a8d7671e98eb5 Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 15:58:22 +0100 Subject: [PATCH 01/11] Issue #350 - create ReleaseHistory result class --- .../com/marcnuri/helm/ReleaseHistory.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java diff --git a/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java b/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java new file mode 100644 index 00000000..6b0e3832 --- /dev/null +++ b/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java @@ -0,0 +1,95 @@ +package com.marcnuri.helm; + +import static com.marcnuri.helm.HelmCommand.parseUrlEncodedLines; + +import com.marcnuri.helm.jni.Result; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author Giuseppe Cardaropoli + */ +public class ReleaseHistory { + private final int revision; + private final ZonedDateTime updated; + private final String status; + private final String chart; + private final String appVersion; + + private final String description; + + @SuppressWarnings("java:S107") + private ReleaseHistory(int revision, ZonedDateTime updated, String status, String chart, String appVersion, String description) { + this.revision = revision; + this.updated = updated; + this.status = status; + this.chart = chart; + this.appVersion = appVersion; + this.description = description; + } + + public int getRevision() { + return revision; + } + + public ZonedDateTime getUpdated() { + return updated; + } + + public String getStatus() { + return status; + } + + public String getChart() { + return chart; + } + + public String getAppVersion() { + return appVersion; + } + + public String getDescription() { + return description; + } + + static List parseMultiple(Result result) { + if (result == null) { + throw new IllegalArgumentException("Result cannot be null"); + } + final List releases = new ArrayList<>(); + for (Map entries : parseUrlEncodedLines(result.out)) { + releases.add(new ReleaseHistory( + parseInt(entries.get("revision")), + parseDate(entries.get("updated")), + entries.get("status"), + entries.get("chart"), + entries.get("appVersion"), + entries.get("description") + )); + } + return releases; + } + + private static ZonedDateTime parseDate(String date) { + if (date == null || date.isEmpty()) { + return null; + } + return ZonedDateTime.parse(date, DateTimeFormatter.RFC_1123_DATE_TIME); + } + + private static int parseInt(String number) { + if (number == null || number.isEmpty()) { + return 0; + } + return Integer.parseInt(number); + } + + @Override + public String toString() { + return "ReleaseHistory{" + "revision=" + revision + ", updated=" + updated + ", status='" + status + '\'' + ", chart='" + + chart + '\'' + ", appVersion='" + appVersion + '\'' + ", description='" + description + '\'' + '}'; + } +} From ec4e54b0031bca4309f74011579ccae86d092128 Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 15:58:53 +0100 Subject: [PATCH 02/11] Issue #350 - create ReleaseHistory result class --- .../src/main/java/com/marcnuri/helm/ReleaseHistory.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java b/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java index 6b0e3832..9869eb56 100644 --- a/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java +++ b/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java @@ -86,10 +86,4 @@ private static int parseInt(String number) { } return Integer.parseInt(number); } - - @Override - public String toString() { - return "ReleaseHistory{" + "revision=" + revision + ", updated=" + updated + ", status='" + status + '\'' + ", chart='" + - chart + '\'' + ", appVersion='" + appVersion + '\'' + ", description='" + description + '\'' + '}'; - } } From 75cd7ecc32fb41a8ac3b33575243f9030e212e0e Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 15:59:38 +0100 Subject: [PATCH 03/11] Issue #350 - create helm history options on Java and Go --- .../com/marcnuri/helm/jni/HistoryOptions.java | 47 ++++++++++++ native/internal/helm/history.go | 71 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 lib/api/src/main/java/com/marcnuri/helm/jni/HistoryOptions.java create mode 100644 native/internal/helm/history.go diff --git a/lib/api/src/main/java/com/marcnuri/helm/jni/HistoryOptions.java b/lib/api/src/main/java/com/marcnuri/helm/jni/HistoryOptions.java new file mode 100644 index 00000000..5c9d450c --- /dev/null +++ b/lib/api/src/main/java/com/marcnuri/helm/jni/HistoryOptions.java @@ -0,0 +1,47 @@ +/* + * Copyright 2026 Marc Nuri + * + * 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.marcnuri.helm.jni; + +import com.sun.jna.Structure; + +/** + * @author Giuseppe Cardaropoli + */ +@Structure.FieldOrder({ + "releaseName", + "max", + "namespace", + "kubeConfig", + "kubeConfigContents" +}) +public class HistoryOptions extends Structure { + public String releaseName; + public int max; + public String namespace; + public String kubeConfig; + public String kubeConfigContents; + + public HistoryOptions(String releaseName, int max, String namespace, + String kubeConfig, String kubeConfigContents) { + this.releaseName = releaseName; + this.max = max; + this.namespace = namespace; + this.kubeConfig = kubeConfig; + this.kubeConfigContents = kubeConfigContents; + } + +} diff --git a/native/internal/helm/history.go b/native/internal/helm/history.go new file mode 100644 index 00000000..26766758 --- /dev/null +++ b/native/internal/helm/history.go @@ -0,0 +1,71 @@ +/* + * Copyright 2024 Marc Nuri + * + * 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 helm + +import ( + "bytes" + "fmt" + "net/url" + "time" + + "helm.sh/helm/v3/pkg/action" +) + +type HistoryOptions struct { + ReleaseName string + Max int + Namespace string + KubeConfig string + KubeConfigContents string +} + +func History(options *HistoryOptions) (string, error) { + cfg, err := NewCfg(&CfgOptions{ + KubeConfig: options.KubeConfig, + KubeConfigContents: options.KubeConfigContents, + Namespace: options.Namespace, + }) + if err != nil { + return "", err + } + + client := action.NewHistory(cfg) + if options.Max > 0 { + client.Max = options.Max + } else { + client.Max = 256 // Default from Helm CLI + } + + releases, err := client.Run(options.ReleaseName) + if err != nil { + return "", err + } + + // Format output similar to List command (URL-encoded lines) + var out bytes.Buffer + for _, rel := range releases { + out.WriteString(fmt.Sprintf("revision=%d&updated=%s&status=%s&chart=%s&appVersion=%s&description=%s\n", + rel.Version, + url.QueryEscape(rel.Info.LastDeployed.Format(time.RFC1123Z)), + url.QueryEscape(rel.Info.Status.String()), + url.QueryEscape(formatChartname(rel.Chart)), + url.QueryEscape(rel.Chart.AppVersion()), + url.QueryEscape(rel.Info.Description), + )) + } + return out.String(), nil +} From b4bf66989c41f7b5c424f937b4aa8df38c7ed313 Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 16:00:04 +0100 Subject: [PATCH 04/11] Issue #350 - add export function --- native/main.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/native/main.go b/native/main.go index 339fd621..ffa3c97f 100644 --- a/native/main.go +++ b/native/main.go @@ -40,6 +40,14 @@ struct DependencyOptions { int debug; }; +struct HistoryOptions { + char* releaseName; + int max; + char* namespace; + char* kubeConfig; + char* kubeConfigContents; +}; + struct InstallOptions { char* name; int generateName; @@ -260,12 +268,13 @@ struct UpgradeOptions { import "C" import ( "fmt" - "github.com/manusa/helm-java/native/internal/helm" "io" "os" "strings" "time" "unsafe" + + "github.com/manusa/helm-java/native/internal/helm" ) // Run the given function and return the result as a C struct. @@ -347,6 +356,19 @@ func DependencyUpdate(options *C.struct_DependencyOptions) C.Result { }) } +//export History +func History(options *C.struct_HistoryOptions) C.Result { + return runCommand(func() (string, error) { + return helm.History(&helm.HistoryOptions{ + ReleaseName: C.GoString(options.releaseName), + Max: int(options.max), + Namespace: C.GoString(options.namespace), + KubeConfig: C.GoString(options.kubeConfig), + KubeConfigContents: C.GoString(options.kubeConfigContents), + }) + }) +} + //export Install func Install(options *C.struct_InstallOptions) C.Result { var timeout time.Duration From 50a9f5f507ed88965ecdcc4fe7479f05737b2e39 Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 16:00:40 +0100 Subject: [PATCH 05/11] Issue #350 - create helm history command class and related factory --- .../src/main/java/com/marcnuri/helm/Helm.java | 4 + .../com/marcnuri/helm/HistoryCommand.java | 95 +++++++++++++++++++ .../java/com/marcnuri/helm/jni/HelmLib.java | 2 + 3 files changed, 101 insertions(+) create mode 100644 helm-java/src/main/java/com/marcnuri/helm/HistoryCommand.java diff --git a/helm-java/src/main/java/com/marcnuri/helm/Helm.java b/helm-java/src/main/java/com/marcnuri/helm/Helm.java index 2a29a2d4..0273d8f6 100644 --- a/helm-java/src/main/java/com/marcnuri/helm/Helm.java +++ b/helm-java/src/main/java/com/marcnuri/helm/Helm.java @@ -60,6 +60,10 @@ public DependencyCommand dependency() { return new DependencyCommand(HelmLibHolder.INSTANCE, path); } + public static HistoryCommand history(String releaseName) { + return new HistoryCommand(HelmLibHolder.INSTANCE, releaseName); + } + /** * This commands installs the referenced chart archive. * diff --git a/helm-java/src/main/java/com/marcnuri/helm/HistoryCommand.java b/helm-java/src/main/java/com/marcnuri/helm/HistoryCommand.java new file mode 100644 index 00000000..fdf65fbd --- /dev/null +++ b/helm-java/src/main/java/com/marcnuri/helm/HistoryCommand.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024 Marc Nuri + * + * 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.marcnuri.helm; + +import com.marcnuri.helm.jni.HelmLib; +import com.marcnuri.helm.jni.HistoryOptions; +import java.nio.file.Path; +import java.util.List; + +/** + * @author Giuseppe Cardaropoli + */ +public class HistoryCommand extends HelmCommand>{ + + private final String releaseName; + private int max; + private String namespace; + private Path kubeConfig; + private String kubeConfigContents; + + public HistoryCommand(HelmLib helmLib, String releaseName) { + super(helmLib); + this.releaseName = releaseName; + } + + @Override + public List call() { + return ReleaseHistory.parseMultiple(run(hl -> hl.History(new HistoryOptions( + releaseName, + max, + namespace, + toString(kubeConfig), + kubeConfigContents + )))); + } + + /** + * Maximum number of revisions to include in history. + * Default is 256. + * + * @param max maximum number of revisions. + * @return this {@link HistoryCommand} instance. + */ + public HistoryCommand withMax(int max) { + this.max = max; + return this; + } + + /** + * Kubernetes namespace scope for this request. + * + * @param namespace the Kubernetes namespace for this request. + * @return this {@link HistoryCommand} instance. + */ + public HistoryCommand withNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + /** + * Set the path to the ~/.kube/config file to use. + * + * @param kubeConfig the path to Kube config file. + * @return this {@link HistoryCommand} instance. + */ + public HistoryCommand withKubeConfig(Path kubeConfig) { + this.kubeConfig = kubeConfig; + return this; + } + + /** + * Set the Kube config to use. + * + * @param kubeConfigContents the contents of the Kube config file. + * @return this {@link HistoryCommand} instance. + */ + public HistoryCommand withKubeConfigContents(String kubeConfigContents) { + this.kubeConfigContents = kubeConfigContents; + return this; + } +} diff --git a/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java b/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java index e16b9dbe..3fcfc142 100644 --- a/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java +++ b/lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java @@ -33,6 +33,8 @@ public interface HelmLib extends Library { Result DependencyUpdate(DependencyOptions options); + Result History(HistoryOptions options); + Result Install(InstallOptions options); Result Lint(LintOptions options); From 10b3804c452d943cf17cde410126e9ca79b20307 Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 16:07:32 +0100 Subject: [PATCH 06/11] Issue #350 - create helm history command class and related factory --- helm-java/src/main/java/com/marcnuri/helm/Helm.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helm-java/src/main/java/com/marcnuri/helm/Helm.java b/helm-java/src/main/java/com/marcnuri/helm/Helm.java index 0273d8f6..a2282764 100644 --- a/helm-java/src/main/java/com/marcnuri/helm/Helm.java +++ b/helm-java/src/main/java/com/marcnuri/helm/Helm.java @@ -60,6 +60,12 @@ public DependencyCommand dependency() { return new DependencyCommand(HelmLibHolder.INSTANCE, path); } + /** + * Fetch release history. + * + * @param releaseName name of the release. + * @return a new {@link HistoryCommand} instance. + */ public static HistoryCommand history(String releaseName) { return new HistoryCommand(HelmLibHolder.INSTANCE, releaseName); } From cc7d547e24488539084799cd2e33048bc9f2f391 Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 18:09:58 +0100 Subject: [PATCH 07/11] Issue #350 - apply the limit manually, keeping only the most recent revisions --- native/internal/helm/history.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/native/internal/helm/history.go b/native/internal/helm/history.go index 26766758..4de2220e 100644 --- a/native/internal/helm/history.go +++ b/native/internal/helm/history.go @@ -34,27 +34,36 @@ type HistoryOptions struct { } func History(options *HistoryOptions) (string, error) { + cfg, err := NewCfg(&CfgOptions{ KubeConfig: options.KubeConfig, KubeConfigContents: options.KubeConfigContents, Namespace: options.Namespace, }) + if err != nil { return "", err } client := action.NewHistory(cfg) - if options.Max > 0 { - client.Max = options.Max - } else { - client.Max = 256 // Default from Helm CLI - } - releases, err := client.Run(options.ReleaseName) + if err != nil { return "", err } + // Apply Max filter manually since action.History.Run() does not honor the Max field. + // The Run() method returns all revisions for a release, and the Max limit is expected + // to be applied by the caller (as done in the Helm CLI). We keep only the most recent + // 'maxReleases' revisions by slicing from the end of the list. + maxReleases := options.Max + if maxReleases <= 0 { + maxReleases = 256 // Default from Helm CLI + } + if len(releases) > maxReleases { + releases = releases[len(releases)-maxReleases:] + } + // Format output similar to List command (URL-encoded lines) var out bytes.Buffer for _, rel := range releases { From 3748905b6039d44cc6d160ce862703ed24c6d2b5 Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Mon, 9 Feb 2026 18:17:01 +0100 Subject: [PATCH 08/11] Issue #350 - add unit and integration tests --- .../com/marcnuri/helm/HelmHistoryTest.java | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java diff --git a/helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java b/helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java new file mode 100644 index 00000000..fcf77315 --- /dev/null +++ b/helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java @@ -0,0 +1,193 @@ +/* + * Copyright 2024 Marc Nuri + * + * 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.marcnuri.helm; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import com.dajudge.kindcontainer.KindContainer; +import com.dajudge.kindcontainer.KindContainerVersion; +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.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * @author Giuseppe Cardaropoli + */ +public class HelmHistoryTest { + + static KindContainer kindContainer; + static String kubeConfigContents; + static Path kubeConfigFile; + + private Helm helm; + + @BeforeAll + static void setUpKubernetes(@TempDir Path tempDir) throws IOException { + kindContainer = new KindContainer<>(KindContainerVersion.VERSION_1_31_0); + kindContainer.start(); + + kubeConfigContents = kindContainer.getKubeconfig(); + kubeConfigFile = tempDir.resolve("config.yaml"); + Files.write(kubeConfigFile, kubeConfigContents.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + } + + @AfterAll + static void tearDownKubernetes() { + if (kindContainer != null) { + kindContainer.stop(); + } + } + + @BeforeEach + void setUp(@TempDir Path tempDir) { + helm = Helm.create() + .withName("test-history") + .withDir(tempDir) + .call(); + } + + @Nested + class Valid { + + @Test + void afterInstall() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-after-install") + .call(); + + List releaseHistories = Helm.history("test-history-after-install") + .withKubeConfig(kubeConfigFile) + .call(); + + assertThat(releaseHistories).hasSize(1); + assertThat(releaseHistories.get(0).getRevision()).isEqualTo(1); + assertThat(releaseHistories.get(0).getDescription()).containsIgnoringCase("Install complete"); + } + + @Test + void afterUpgrade() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-after-install-and-upgrade") + .call(); + + helm.upgrade() + .withKubeConfig(kubeConfigFile) + .withName("test-history-after-install-and-upgrade") + .set("image.tag", "latest") + .call(); + + List releaseHistories = Helm.history("test-history-after-install-and-upgrade") + .withKubeConfig(kubeConfigFile) + .call(); + + assertThat(releaseHistories).hasSize(2); + assertThat(releaseHistories.get(0).getDescription()).containsIgnoringCase("Install complete"); + assertThat(releaseHistories.get(1).getDescription()).containsIgnoringCase("Upgrade complete"); + } + + @Test + void withMax() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-max") + .call(); + + helm.upgrade() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-max") + .set("image.tag", "v1") + .call(); + + helm.upgrade() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-max") + .set("image.tag", "v2") + .call(); + + List releaseHistories = Helm.history("test-history-with-max") + .withKubeConfig(kubeConfigFile) + .withMax(2) + .call(); + + assertThat(releaseHistories).hasSize(2); + assertThat(releaseHistories.get(0).getRevision()).isEqualTo(2); + assertThat(releaseHistories.get(1).getRevision()).isEqualTo(3); + } + + @Test + void withNamespace() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-namespace") + .withNamespace("history-namespace") + .createNamespace() + .call(); + + List releaseHistories = Helm.history("test-history-with-namespace") + .withKubeConfig(kubeConfigFile) + .withNamespace("history-namespace") + .call(); + + assertThat(releaseHistories).hasSize(1); + assertThat(releaseHistories.get(0).getRevision()).isEqualTo(1); + } + + @Test + void withKubeConfigContents() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-kube-config-contents") + .call(); + + List releaseHistories = Helm.history("test-history-with-kube-config-contents") + .withKubeConfigContents(kubeConfigContents) + .call(); + + assertThat(releaseHistories).hasSize(1); + assertThat(releaseHistories.get(0).getRevision()).isEqualTo(1); + } + + } + + @Nested + class Invalid { + + @Test + void nonExistentRelease() { + assertThatThrownBy(() -> + Helm.history("non-existent-release") + .withKubeConfig(kubeConfigFile) + .call() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("release: not found"); + } + } + +} From 1f36648c2606bb50cbd6fe723bfbba8a344daa6e Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Fri, 13 Feb 2026 11:55:18 +0100 Subject: [PATCH 09/11] Issue #350 - add missing apache license --- .../java/com/marcnuri/helm/ReleaseHistory.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java b/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java index 9869eb56..84e1197a 100644 --- a/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java +++ b/helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024 Marc Nuri + * + * 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.marcnuri.helm; import static com.marcnuri.helm.HelmCommand.parseUrlEncodedLines; From af9b6ad5c7212612daed6b75d689f498e74421fa Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Fri, 13 Feb 2026 11:55:47 +0100 Subject: [PATCH 10/11] Issue #350 - use url.Values pattern --- native/internal/helm/history.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/native/internal/helm/history.go b/native/internal/helm/history.go index 4de2220e..39f5f988 100644 --- a/native/internal/helm/history.go +++ b/native/internal/helm/history.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "net/url" + "strconv" "time" "helm.sh/helm/v3/pkg/action" @@ -64,17 +65,18 @@ func History(options *HistoryOptions) (string, error) { releases = releases[len(releases)-maxReleases:] } - // Format output similar to List command (URL-encoded lines) - var out bytes.Buffer + out := bytes.NewBuffer(make([]byte, 0)) for _, rel := range releases { - out.WriteString(fmt.Sprintf("revision=%d&updated=%s&status=%s&chart=%s&appVersion=%s&description=%s\n", - rel.Version, - url.QueryEscape(rel.Info.LastDeployed.Format(time.RFC1123Z)), - url.QueryEscape(rel.Info.Status.String()), - url.QueryEscape(formatChartname(rel.Chart)), - url.QueryEscape(rel.Chart.AppVersion()), - url.QueryEscape(rel.Info.Description), - )) + values := make(url.Values) + values.Set("revision", strconv.Itoa(rel.Version)) + if tspb := rel.Info.LastDeployed; !tspb.IsZero() { + values.Set("updated", tspb.Format(time.RFC1123Z)) + } + values.Set("status", rel.Info.Status.String()) + values.Set("chart", formatChartname(rel.Chart)) + values.Set("appVersion", formatAppVersion(rel.Chart)) + values.Set("description", rel.Info.Description) + _, _ = fmt.Fprintln(out, values.Encode()) } return out.String(), nil } From 28111e68f36f4f9ef618ef7be8bd8106112c61ab Mon Sep 17 00:00:00 2001 From: "e-g.cardaropoli" Date: Fri, 13 Feb 2026 12:07:32 +0100 Subject: [PATCH 11/11] Issue #350 - move helm history command tests in HelmKubernetesTest --- .../com/marcnuri/helm/HelmHistoryTest.java | 193 ------------------ .../com/marcnuri/helm/HelmKubernetesTest.java | 138 +++++++++++++ 2 files changed, 138 insertions(+), 193 deletions(-) delete mode 100644 helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java diff --git a/helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java b/helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java deleted file mode 100644 index fcf77315..00000000 --- a/helm-java/src/test/java/com/marcnuri/helm/HelmHistoryTest.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2024 Marc Nuri - * - * 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.marcnuri.helm; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -import com.dajudge.kindcontainer.KindContainer; -import com.dajudge.kindcontainer.KindContainerVersion; -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.List; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -/** - * @author Giuseppe Cardaropoli - */ -public class HelmHistoryTest { - - static KindContainer kindContainer; - static String kubeConfigContents; - static Path kubeConfigFile; - - private Helm helm; - - @BeforeAll - static void setUpKubernetes(@TempDir Path tempDir) throws IOException { - kindContainer = new KindContainer<>(KindContainerVersion.VERSION_1_31_0); - kindContainer.start(); - - kubeConfigContents = kindContainer.getKubeconfig(); - kubeConfigFile = tempDir.resolve("config.yaml"); - Files.write(kubeConfigFile, kubeConfigContents.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); - } - - @AfterAll - static void tearDownKubernetes() { - if (kindContainer != null) { - kindContainer.stop(); - } - } - - @BeforeEach - void setUp(@TempDir Path tempDir) { - helm = Helm.create() - .withName("test-history") - .withDir(tempDir) - .call(); - } - - @Nested - class Valid { - - @Test - void afterInstall() { - helm.install() - .withKubeConfig(kubeConfigFile) - .withName("test-history-after-install") - .call(); - - List releaseHistories = Helm.history("test-history-after-install") - .withKubeConfig(kubeConfigFile) - .call(); - - assertThat(releaseHistories).hasSize(1); - assertThat(releaseHistories.get(0).getRevision()).isEqualTo(1); - assertThat(releaseHistories.get(0).getDescription()).containsIgnoringCase("Install complete"); - } - - @Test - void afterUpgrade() { - helm.install() - .withKubeConfig(kubeConfigFile) - .withName("test-history-after-install-and-upgrade") - .call(); - - helm.upgrade() - .withKubeConfig(kubeConfigFile) - .withName("test-history-after-install-and-upgrade") - .set("image.tag", "latest") - .call(); - - List releaseHistories = Helm.history("test-history-after-install-and-upgrade") - .withKubeConfig(kubeConfigFile) - .call(); - - assertThat(releaseHistories).hasSize(2); - assertThat(releaseHistories.get(0).getDescription()).containsIgnoringCase("Install complete"); - assertThat(releaseHistories.get(1).getDescription()).containsIgnoringCase("Upgrade complete"); - } - - @Test - void withMax() { - helm.install() - .withKubeConfig(kubeConfigFile) - .withName("test-history-with-max") - .call(); - - helm.upgrade() - .withKubeConfig(kubeConfigFile) - .withName("test-history-with-max") - .set("image.tag", "v1") - .call(); - - helm.upgrade() - .withKubeConfig(kubeConfigFile) - .withName("test-history-with-max") - .set("image.tag", "v2") - .call(); - - List releaseHistories = Helm.history("test-history-with-max") - .withKubeConfig(kubeConfigFile) - .withMax(2) - .call(); - - assertThat(releaseHistories).hasSize(2); - assertThat(releaseHistories.get(0).getRevision()).isEqualTo(2); - assertThat(releaseHistories.get(1).getRevision()).isEqualTo(3); - } - - @Test - void withNamespace() { - helm.install() - .withKubeConfig(kubeConfigFile) - .withName("test-history-with-namespace") - .withNamespace("history-namespace") - .createNamespace() - .call(); - - List releaseHistories = Helm.history("test-history-with-namespace") - .withKubeConfig(kubeConfigFile) - .withNamespace("history-namespace") - .call(); - - assertThat(releaseHistories).hasSize(1); - assertThat(releaseHistories.get(0).getRevision()).isEqualTo(1); - } - - @Test - void withKubeConfigContents() { - helm.install() - .withKubeConfig(kubeConfigFile) - .withName("test-history-with-kube-config-contents") - .call(); - - List releaseHistories = Helm.history("test-history-with-kube-config-contents") - .withKubeConfigContents(kubeConfigContents) - .call(); - - assertThat(releaseHistories).hasSize(1); - assertThat(releaseHistories.get(0).getRevision()).isEqualTo(1); - } - - } - - @Nested - class Invalid { - - @Test - void nonExistentRelease() { - assertThatThrownBy(() -> - Helm.history("non-existent-release") - .withKubeConfig(kubeConfigFile) - .call() - ) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("release: not found"); - } - } - -} diff --git a/helm-java/src/test/java/com/marcnuri/helm/HelmKubernetesTest.java b/helm-java/src/test/java/com/marcnuri/helm/HelmKubernetesTest.java index 1aec2338..a385d608 100644 --- a/helm-java/src/test/java/com/marcnuri/helm/HelmKubernetesTest.java +++ b/helm-java/src/test/java/com/marcnuri/helm/HelmKubernetesTest.java @@ -18,6 +18,7 @@ import com.dajudge.kindcontainer.KindContainer; import com.dajudge.kindcontainer.KindContainerVersion; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -879,4 +880,141 @@ void missingRelease() { } } } + + @Nested + class History { + + @Nested + class Valid { + + @Test + void afterInstall() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-after-install") + .call(); + + List releaseHistories = Helm.history("test-history-after-install") + .withKubeConfig(kubeConfigFile) + .call(); + + assertThat(releaseHistories) + .hasSize(1) + .first() + .returns(1, ReleaseHistory::getRevision) + .extracting(ReleaseHistory::getDescription).asString() + .containsIgnoringCase("Install complete"); + } + + @Test + void afterUpgrade() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-after-install-and-upgrade") + .call(); + + helm.upgrade() + .withKubeConfig(kubeConfigFile) + .withName("test-history-after-install-and-upgrade") + .set("image.tag", "latest") + .call(); + + List releaseHistories = Helm.history("test-history-after-install-and-upgrade") + .withKubeConfig(kubeConfigFile) + .call(); + + assertThat(releaseHistories) + .hasSize(2) + .satisfiesExactly( + first -> assertThat(first.getDescription()).containsIgnoringCase("Install complete"), + second -> assertThat(second.getDescription()).containsIgnoringCase("Upgrade complete") + ); + } + + @Test + void withMax() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-max") + .call(); + + helm.upgrade() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-max") + .set("image.tag", "v1") + .call(); + + helm.upgrade() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-max") + .set("image.tag", "v2") + .call(); + + List releaseHistories = Helm.history("test-history-with-max") + .withKubeConfig(kubeConfigFile) + .withMax(2) + .call(); + + assertThat(releaseHistories) + .hasSize(2) + .satisfiesExactly( + first -> assertThat(first.getRevision()).isEqualTo(2), + second -> assertThat(second.getRevision()).isEqualTo(3) + ); + } + + @Test + void withNamespace() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-namespace") + .withNamespace("history-namespace") + .createNamespace() + .call(); + + List releaseHistories = Helm.history("test-history-with-namespace") + .withKubeConfig(kubeConfigFile) + .withNamespace("history-namespace") + .call(); + + assertThat(releaseHistories) + .hasSize(1) + .first() + .returns(1, ReleaseHistory::getRevision); + } + + @Test + void withKubeConfigContents() { + helm.install() + .withKubeConfig(kubeConfigFile) + .withName("test-history-with-kube-config-contents") + .call(); + + List releaseHistories = Helm.history("test-history-with-kube-config-contents") + .withKubeConfigContents(kubeConfigContents) + .call(); + + assertThat(releaseHistories) + .hasSize(1) + .first() + .returns(1, ReleaseHistory::getRevision); + } + + } + + @Nested + class Invalid { + + @Test + void nonExistentRelease() { + AssertionsForClassTypes.assertThatThrownBy(() -> + Helm.history("non-existent-release") + .withKubeConfig(kubeConfigFile) + .call() + ) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("release: not found"); + } + } + } }