-
Notifications
You must be signed in to change notification settings - Fork 20
Description
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 revisions
List<ReleaseHistory> history = Helm.history("my-release")
.withKubeConfig(kubeConfigPath)
.call();
// With namespace
List<ReleaseHistory> history = Helm.history("my-release")
.withNamespace("my-namespace")
.withKubeConfig(kubeConfigPath)
.call();
// Limit number of revisions
List<ReleaseHistory> history = Helm.history("my-release")
.withMax(10)
.withKubeConfig(kubeConfigPath)
.call();
// Using kubeconfig contents
List<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)
package com.marcnuri.helm;
import java.time.ZonedDateTime;
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;
// 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"
)
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)
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.
maxReleases := options.Max
if maxReleases <= 0 {
maxReleases = 256 // Default from Helm CLI
}
if len(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 := range releases {
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
}3. Add CGO export in native/main.go
Add the C struct definition:
struct HistoryOptions {
char* releaseName;
int max;
char* namespace;
char* kubeConfig;
char* kubeConfigContents;
};Add the export function:
//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),
})
})
}4. Create JNA Options class (lib/api/src/main/java/com/marcnuri/helm/jni/HistoryOptions.java)
package com.marcnuri.helm.jni;
import com.sun.jna.Structure;
@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;
}
}5. Add method to HelmLib interface (lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java)
Result History(HistoryOptions options);6. Create HistoryCommand class (helm-java/src/main/java/com/marcnuri/helm/HistoryCommand.java)
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;
public class HistoryCommand extends HelmCommand<List<ReleaseHistory>> {
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<ReleaseHistory> 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;
}
}7. Add factory method in Helm.java
/**
* 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);
}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:
@Nested
class History {
@Nested
class Valid {
@Test
void afterInstall() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-after-install")
.call();
final List<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");
}
@Test
void afterUpgrade() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-after-upgrade")
.call();
helm.upgrade()
.withKubeConfig(kubeConfigFile)
.withName("history-after-upgrade")
.set("image.tag", "latest")
.call();
final List<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");
}
@Test
void withMax() {
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();
final List<ReleaseHistory> result = Helm.history("history-with-max")
.withKubeConfig(kubeConfigFile)
.withMax(2)
.call();
assertThat(result)
.hasSize(2)
.extracting(ReleaseHistory::getRevision)
.containsExactly(2, 3);
}
@Test
void withNamespace() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-with-namespace")
.withNamespace("history-namespace")
.createNamespace()
.call();
final List<ReleaseHistory> result = Helm.history("history-with-namespace")
.withKubeConfig(kubeConfigFile)
.withNamespace("history-namespace")
.call();
assertThat(result)
.hasSize(1)
.first()
.returns(1, ReleaseHistory::getRevision);
}
@Test
void withKubeConfigContents() {
helm.install()
.withKubeConfig(kubeConfigFile)
.withName("history-with-kube-config-contents")
.call();
final List<ReleaseHistory> result = Helm.history("history-with-kube-config-contents")
.withKubeConfigContents(kubeConfigContents)
.call();
assertThat(result)
.hasSize(1)
.first()
.returns(1, ReleaseHistory::getRevision);
}
}
@Nested
class Invalid {
@Test
void nonExistentRelease() {
final HistoryCommand historyCommand = Helm.history("non-existent-release")
.withKubeConfig(kubeConfigFile);
assertThatThrownBy(historyCommand::call)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("release: not found");
}
}
}Acceptance Criteria
- Create
ReleaseHistoryresult class inhelm-javamodule (with Apache License header) - Create
HistoryOptionsGo struct innative/internal/helm/history.go - Implement
Historyfunction in Go usingaction.NewHistoryandurl.Valuespattern - Add CGO export
Historyinnative/main.go - Create
HistoryOptions.javaJNA structure inlib/api(with Apache License header) - Add
Historymethod toHelmLibinterface - Create
HistoryCommand.javainhelm-javamodule (with Apache License header) - Add
history(String releaseName)factory method toHelm.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.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
- CLI Reference: https://helm.sh/docs/helm/helm_history/
- Helm SDK: Uses
action.NewHistoryfromhelm.sh/helm/v3/pkg/action - Priority: High - Essential for auditing and rollback workflows
- Complexity: Medium - Requires new result type (
ReleaseHistory) - Related: Should be implemented before or alongside
helm rollback(some commands seem to have not been implemented yet, such as update, history, and rollback. #97)
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