Description
The helm history command is currently not implemented in helm-java. This command prints historical revisions for a given release, which is essential for auditing deployments and identifying which revision to rollback to.
Background
The helm history command displays a table with:
- REVISION: Version number of the release
- UPDATED: Timestamp of the change
- STATUS: Current state (deployed, superseded, failed, etc.)
- CHART: Chart name and version
- APP VERSION: Application version
- DESCRIPTION: Change description (e.g., "Initial install", "Upgraded successfully", "Rolled back to 2")
This command is commonly used in conjunction with helm rollback to identify target revisions. See the official documentation.
Related Issues
This addresses part of issue #97 which mentions missing history command but lacks implementation details.
Proposed API
Following the existing patterns in the codebase (similar to ListCommand), the implementation should provide a fluent API:
// Basic usage - returns list of release revisionsList<ReleaseHistory> history = Helm.history("my-release")
.withKubeConfig(kubeConfigPath)
.call();
// With namespaceList<ReleaseHistory> history = Helm.history("my-release")
.withNamespace("my-namespace")
.withKubeConfig(kubeConfigPath)
.call();
// Limit number of revisionsList<ReleaseHistory> history = Helm.history("my-release")
.withMax(10)
.withKubeConfig(kubeConfigPath)
.call();
// Using kubeconfig contentsList<ReleaseHistory> history = Helm.history("my-release")
.withKubeConfigContents(kubeConfigYaml)
.call();Implementation Guide
1. Create ReleaseHistory result class (helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java)
packagecom.marcnuri.helm;
importjava.time.ZonedDateTime;
publicclassReleaseHistory {
privatefinalintrevision;
privatefinalZonedDateTimeupdated;
privatefinalStringstatus;
privatefinalStringchart;
privatefinalStringappVersion;
privatefinalStringdescription;
// Constructor, getters, and static parse methods// Similar pattern to Release.parseMultiple()
}2. Create Go Options struct and function (native/internal/helm/history.go)
package helm
import (
"bytes""fmt""net/url""strconv""time""helm.sh/helm/v3/pkg/action"
)
typeHistoryOptionsstruct {
ReleaseNamestringMaxintNamespacestringKubeConfigstringKubeConfigContentsstring
}
funcHistory(options*HistoryOptions) (string, error) {
cfg, err:=NewCfg(&CfgOptions{
KubeConfig: options.KubeConfig,
KubeConfigContents: options.KubeConfigContents,
Namespace: options.Namespace,
})
iferr!=nil {
return"", err
}
client:=action.NewHistory(cfg)
releases, err:=client.Run(options.ReleaseName)
iferr!=nil {
return"", err
}
// Apply Max filter manually since action.History.Run() does not honor the Max field.maxReleases:=options.MaxifmaxReleases<=0 {
maxReleases=256// Default from Helm CLI
}
iflen(releases) >maxReleases {
releases=releases[len(releases)-maxReleases:]
}
// Format output using url.Values (consistent with list.go pattern)out:=bytes.NewBuffer(make([]byte, 0))
for_, rel:=rangereleases {
values:=make(url.Values)
values.Set("revision", strconv.Itoa(rel.Version))
iftspb:=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())
}
returnout.String(), nil
}3. Add CGO export in native/main.go
Add the C struct definition:
structHistoryOptions {
char*releaseName;
intmax;
char*namespace;
char*kubeConfig;
char*kubeConfigContents;
};Add the export function:
//export HistoryfuncHistory(options*C.struct_HistoryOptions) C.Result {
returnrunCommand(func() (string, error) {
returnhelm.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),
})
})
}4. Create JNA Options class (lib/api/src/main/java/com/marcnuri/helm/jni/HistoryOptions.java)
packagecom.marcnuri.helm.jni;
importcom.sun.jna.Structure;
@Structure.FieldOrder({
"releaseName",
"max",
"namespace",
"kubeConfig",
"kubeConfigContents"
})
publicclassHistoryOptionsextendsStructure {
publicStringreleaseName;
publicintmax;
publicStringnamespace;
publicStringkubeConfig;
publicStringkubeConfigContents;
publicHistoryOptions(StringreleaseName, intmax, Stringnamespace, StringkubeConfig, StringkubeConfigContents) {
this.releaseName = releaseName;
this.max = max;
this.namespace = namespace;
this.kubeConfig = kubeConfig;
this.kubeConfigContents = kubeConfigContents;
}
}5. Add method to HelmLib interface (lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java)
ResultHistory(HistoryOptionsoptions);
6. Create HistoryCommand class (helm-java/src/main/java/com/marcnuri/helm/HistoryCommand.java)
packagecom.marcnuri.helm;
importcom.marcnuri.helm.jni.HelmLib;
importcom.marcnuri.helm.jni.HistoryOptions;
importjava.nio.file.Path;
importjava.util.List;
publicclassHistoryCommandextendsHelmCommand<List<ReleaseHistory>> {
privatefinalStringreleaseName;
privateintmax;
privateStringnamespace;
privatePathkubeConfig;
privateStringkubeConfigContents;
publicHistoryCommand(HelmLibhelmLib, StringreleaseName) {
super(helmLib);
this.releaseName = releaseName;
}
@OverridepublicList<ReleaseHistory> call() {
returnReleaseHistory.parseMultiple(run(hl -> hl.History(newHistoryOptions(
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. */publicHistoryCommandwithMax(intmax) {
this.max = max;
returnthis;
}
/** * Kubernetes namespace scope for this request. * * @param namespace the Kubernetes namespace for this request. * @return this {@link HistoryCommand} instance. */publicHistoryCommandwithNamespace(Stringnamespace) {
this.namespace = namespace;
returnthis;
}
/** * Set the path to the ~/.kube/config file to use. * * @param kubeConfig the path to kube config file. * @return this {@link HistoryCommand} instance. */publicHistoryCommandwithKubeConfig(PathkubeConfig) {
this.kubeConfig = kubeConfig;
returnthis;
}
/** * Set the kube config to use. * * @param kubeConfigContents the contents of the kube config file. * @return this {@link HistoryCommand} instance. */publicHistoryCommandwithKubeConfigContents(StringkubeConfigContents) {
this.kubeConfigContents = kubeConfigContents;
returnthis;
}
}7. Add factory method in Helm.java
/** * Fetch release history. * * @param releaseName name of the release. * @return a new {@link HistoryCommand} instance. */publicstaticHistoryCommandhistory(StringreleaseName) {
returnnewHistoryCommand(HelmLibHolder.INSTANCE, releaseName);
}8. Add tests as nested class in HelmKubernetesTest
IMPORTANT: Tests that require a KinD container must be added as a nested class within HelmKubernetesTest, not as a separate test file. This is for performance reasons - all Kubernetes integration tests share a single KinD container instance that is started once in @BeforeAll and stopped in @AfterAll.
Add the following nested class to HelmKubernetesTest:
@NestedclassHistory {
@NestedclassValid {
@TestvoidafterInstall() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-after-install")
.call();
finalList<ReleaseHistory> result = Helm.history("history-after-install")
.withKubeConfig(kubeConfigFile)
.call();
assertThat(result)
.hasSize(1)
.first()
.returns(1, ReleaseHistory::getRevision)
.extracting(ReleaseHistory::getDescription).asString()
.containsIgnoringCase("Install complete");
}
@TestvoidafterUpgrade() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-after-upgrade")
.call();
helm.upgrade()
.withKubeConfig(kubeConfigFile)
.withName("history-after-upgrade")
.set("image.tag", "latest")
.call();
finalList<ReleaseHistory> result = Helm.history("history-after-upgrade")
.withKubeConfig(kubeConfigFile)
.call();
assertThat(result).hasSize(2);
assertThat(result.get(0).getDescription()).containsIgnoringCase("Install complete");
assertThat(result.get(1).getDescription()).containsIgnoringCase("Upgrade complete");
}
@TestvoidwithMax() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-with-max")
.call();
helm.upgrade()
.withKubeConfig(kubeConfigFile)
.withName("history-with-max")
.set("image.tag", "v1")
.call();
helm.upgrade()
.withKubeConfig(kubeConfigFile)
.withName("history-with-max")
.set("image.tag", "v2")
.call();
finalList<ReleaseHistory> result = Helm.history("history-with-max")
.withKubeConfig(kubeConfigFile)
.withMax(2)
.call();
assertThat(result)
.hasSize(2)
.extracting(ReleaseHistory::getRevision)
.containsExactly(2, 3);
}
@TestvoidwithNamespace() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-with-namespace")
.withNamespace("history-namespace")
.createNamespace()
.call();
finalList<ReleaseHistory> result = Helm.history("history-with-namespace")
.withKubeConfig(kubeConfigFile)
.withNamespace("history-namespace")
.call();
assertThat(result)
.hasSize(1)
.first()
.returns(1, ReleaseHistory::getRevision);
}
@TestvoidwithKubeConfigContents() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-with-kube-config-contents")
.call();
finalList<ReleaseHistory> result = Helm.history("history-with-kube-config-contents")
.withKubeConfigContents(kubeConfigContents)
.call();
assertThat(result)
.hasSize(1)
.first()
.returns(1, ReleaseHistory::getRevision);
}
}
@NestedclassInvalid {
@TestvoidnonExistentRelease() {
finalHistoryCommandhistoryCommand = Helm.history("non-existent-release")
.withKubeConfig(kubeConfigFile);
assertThatThrownBy(historyCommand::call)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("release: not found");
}
}
}Acceptance Criteria
Tests
Following the project's testing philosophy (black-box, no mocks, nested structure):
Tests should be added as a nested History class within HelmKubernetesTest:
HelmKubernetesTest.HistoryValidafterInstall - History shows single revision after fresh installafterUpgrade - History shows multiple revisions after upgradewithMax - Limit number of returned revisionswithNamespace - Get history with explicit namespacewithKubeConfigContents - Use inline kubeconfig
InvalidnonExistentRelease - Should throw appropriate exception
Additional Information
Example CLI Output
$ helm history angry-bird
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install
2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully
3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2
4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully
Description
The
helm historycommand is currently not implemented in helm-java. This command prints historical revisions for a given release, which is essential for auditing deployments and identifying which revision to rollback to.Background
The
helm historycommand displays a table with:This command is commonly used in conjunction with
helm rollbackto identify target revisions. See the official documentation.Related Issues
This addresses part of issue #97 which mentions missing
historycommand but lacks implementation details.Proposed API
Following the existing patterns in the codebase (similar to
ListCommand), the implementation should provide a fluent API:Implementation Guide
1. Create ReleaseHistory result class (
helm-java/src/main/java/com/marcnuri/helm/ReleaseHistory.java)2. Create Go Options struct and function (
native/internal/helm/history.go)3. Add CGO export in
native/main.goAdd the C struct definition:
Add the export function:
4. Create JNA Options class (
lib/api/src/main/java/com/marcnuri/helm/jni/HistoryOptions.java)5. Add method to HelmLib interface (
lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java)6. Create HistoryCommand class (
helm-java/src/main/java/com/marcnuri/helm/HistoryCommand.java)7. Add factory method in
Helm.java8. Add tests as nested class in
HelmKubernetesTestIMPORTANT: Tests that require a KinD container must be added as a nested class within
HelmKubernetesTest, not as a separate test file. This is for performance reasons - all Kubernetes integration tests share a single KinD container instance that is started once in@BeforeAlland stopped in@AfterAll.Add the following nested class to
HelmKubernetesTest:Acceptance Criteria
ReleaseHistoryresult class inhelm-javamodule (with Apache License header)HistoryOptionsGo struct innative/internal/helm/history.goHistoryfunction in Go usingaction.NewHistoryandurl.ValuespatternHistoryinnative/main.goHistoryOptions.javaJNA structure inlib/api(with Apache License header)Historymethod toHelmLibinterfaceHistoryCommand.javainhelm-javamodule (with Apache License header)history(String releaseName)factory method toHelm.javaHelmKubernetesTest(NOT as a separate test file)Tests
Following the project's testing philosophy (black-box, no mocks, nested structure):
Tests should be added as a nested
Historyclass withinHelmKubernetesTest:HelmKubernetesTest.HistoryValidafterInstall- History shows single revision after fresh installafterUpgrade- History shows multiple revisions after upgradewithMax- Limit number of returned revisionswithNamespace- Get history with explicit namespacewithKubeConfigContents- Use inline kubeconfigInvalidnonExistentRelease- Should throw appropriate exceptionAdditional Information
action.NewHistoryfromhelm.sh/helm/v3/pkg/actionReleaseHistory)helm rollback(some commands seem to have not been implemented yet, such as update, history, and rollback. #97)Example CLI Output