Skip to content

feat: implement helm history command #350

Description

@manusa

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

  • Create ReleaseHistory result class in helm-java module (with Apache License header)
  • Create HistoryOptions Go struct in native/internal/helm/history.go
  • Implement History function in Go using action.NewHistory and url.Values pattern
  • Add CGO export History in native/main.go
  • Create HistoryOptions.java JNA structure in lib/api (with Apache License header)
  • Add History method to HelmLib interface
  • Create HistoryCommand.java in helm-java module (with Apache License header)
  • Add history(String releaseName) factory method to Helm.java
  • Add integration tests as nested class in HelmKubernetesTest (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 History class within HelmKubernetesTest:

  • HelmKubernetesTest.History
    • Valid
      • afterInstall - History shows single revision after fresh install
      • afterUpgrade - History shows multiple revisions after upgrade
      • withMax - Limit number of returned revisions
      • withNamespace - Get history with explicit namespace
      • withKubeConfigContents - Use inline kubeconfig
    • Invalid
      • nonExistentRelease - 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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions