Skip to content

feat: implement helm rollback command #351

@manusa

Description

@manusa

Description

The helm rollback command is currently not implemented in helm-java. This command rolls back a release to a previous revision, which is critical for recovery when deployments fail or need to be reverted.

Background

The helm rollback command:

  • Takes a release name as the first argument
  • Optionally takes a revision number as the second argument
  • If revision is omitted or set to 0, it rolls back to the immediately previous release
  • Supports waiting for resources to be ready before marking rollback successful
  • Can clean up resources created during a failed rollback

This is one of the most critical Helm commands for maintaining cluster stability and enabling rapid recovery. See the official documentation.

Related Issues

This addresses part of issue #97 which mentions missing rollback command but lacks implementation details.

Proposed API

Following the existing patterns in the codebase (similar to UninstallCommand, UpgradeCommand), the implementation should provide a fluent API:

// Basic usage - rollback to previous revision
Release result = Helm.rollback("my-release")
    .withKubeConfig(kubeConfigPath)
    .call();

// Rollback to specific revision
Release result = Helm.rollback("my-release")
    .toRevision(3)
    .withKubeConfig(kubeConfigPath)
    .call();

// With wait and timeout
Release result = Helm.rollback("my-release")
    .toRevision(2)
    .waitReady()
    .withTimeout(300)
    .withKubeConfig(kubeConfigPath)
    .call();

// With cleanup on failure
Release result = Helm.rollback("my-release")
    .toRevision(1)
    .cleanupOnFail()
    .withNamespace("production")
    .withKubeConfig(kubeConfigPath)
    .call();

// Dry run
Release result = Helm.rollback("my-release")
    .toRevision(2)
    .dryRun()
    .withKubeConfig(kubeConfigPath)
    .call();

// With all options
Release result = Helm.rollback("my-release")
    .toRevision(5)
    .cleanupOnFail()
    .force()
    .noHooks()
    .waitReady()
    .withTimeout(600)
    .withHistoryMax(10)
    .withNamespace("my-namespace")
    .withKubeConfig(kubeConfigPath)
    .debug()
    .call();

Implementation Guide

1. Create Go Options struct and function (native/internal/helm/rollback.go)

package helm

import (
    "time"
    "helm.sh/helm/v3/pkg/action"
)

type RollbackOptions struct {
    ReleaseName        string
    Revision           int
    CleanupOnFail      bool
    DryRun             bool
    Force              bool
    NoHooks            bool
    Wait               bool
    Timeout            int  // seconds
    HistoryMax         int
    Namespace          string
    KubeConfig         string
    KubeConfigContents string
    Debug              bool
}

func Rollback(options *RollbackOptions) (string, error) {
    var log action.DebugLog = nil
    if options.Debug {
        log = debugLog
    }
    
    cfg, err := NewCfg(&CfgOptions{
        KubeConfig:         options.KubeConfig,
        KubeConfigContents: options.KubeConfigContents,
        Namespace:          options.Namespace,
        Log:                log,
    })
    if err != nil {
        return "", err
    }
    
    client := action.NewRollback(cfg)
    client.Version = options.Revision  // 0 means previous revision
    client.CleanupOnFail = options.CleanupOnFail
    client.DryRun = options.DryRun
    client.Force = options.Force
    client.DisableHooks = options.NoHooks
    client.Wait = options.Wait
    if options.Timeout > 0 {
        client.Timeout = time.Duration(options.Timeout) * time.Second
    }
    if options.HistoryMax > 0 {
        client.MaxHistory = options.HistoryMax
    }
    
    err = client.Run(options.ReleaseName)
    if err != nil {
        return "", err
    }
    
    // Get the release after rollback to return status
    statusClient := action.NewStatus(cfg)
    rel, err := statusClient.Run(options.ReleaseName)
    if err != nil {
        return "", err
    }
    
    return StatusReport(rel, true, options.Debug), nil
}

2. Add CGO export in native/main.go

Add the C struct definition:

struct RollbackOptions {
    char* releaseName;
    int revision;
    int cleanupOnFail;
    int dryRun;
    int force;
    int noHooks;
    int wait;
    int timeout;
    int historyMax;
    char* namespace;
    char* kubeConfig;
    char* kubeConfigContents;
    int debug;
};

Add the export function:

//export Rollback
func Rollback(options *C.struct_RollbackOptions) C.Result {
    return result(helm.Rollback(&helm.RollbackOptions{
        ReleaseName:        C.GoString(options.releaseName),
        Revision:           int(options.revision),
        CleanupOnFail:      options.cleanupOnFail == 1,
        DryRun:             options.dryRun == 1,
        Force:              options.force == 1,
        NoHooks:            options.noHooks == 1,
        Wait:               options.wait == 1,
        Timeout:            int(options.timeout),
        HistoryMax:         int(options.historyMax),
        Namespace:          C.GoString(options.namespace),
        KubeConfig:         C.GoString(options.kubeConfig),
        KubeConfigContents: C.GoString(options.kubeConfigContents),
        Debug:              options.debug == 1,
    }))
}

3. Create JNA Options class (lib/api/src/main/java/com/marcnuri/helm/jni/RollbackOptions.java)

package com.marcnuri.helm.jni;

import com.sun.jna.Structure;

@Structure.FieldOrder({
  "releaseName",
  "revision",
  "cleanupOnFail",
  "dryRun",
  "force",
  "noHooks",
  "wait",
  "timeout",
  "historyMax",
  "namespace",
  "kubeConfig",
  "kubeConfigContents",
  "debug"
})
public class RollbackOptions extends Structure {
  public String releaseName;
  public int revision;
  public int cleanupOnFail;
  public int dryRun;
  public int force;
  public int noHooks;
  public int wait;
  public int timeout;
  public int historyMax;
  public String namespace;
  public String kubeConfig;
  public String kubeConfigContents;
  public int debug;

  public RollbackOptions(
    String releaseName,
    int revision,
    int cleanupOnFail,
    int dryRun,
    int force,
    int noHooks,
    int wait,
    int timeout,
    int historyMax,
    String namespace,
    String kubeConfig,
    String kubeConfigContents,
    int debug
  ) {
    this.releaseName = releaseName;
    this.revision = revision;
    this.cleanupOnFail = cleanupOnFail;
    this.dryRun = dryRun;
    this.force = force;
    this.noHooks = noHooks;
    this.wait = wait;
    this.timeout = timeout;
    this.historyMax = historyMax;
    this.namespace = namespace;
    this.kubeConfig = kubeConfig;
    this.kubeConfigContents = kubeConfigContents;
    this.debug = debug;
  }
}

4. Add method to HelmLib interface (lib/api/src/main/java/com/marcnuri/helm/jni/HelmLib.java)

Result Rollback(RollbackOptions options);

5. Create RollbackCommand class (helm-java/src/main/java/com/marcnuri/helm/RollbackCommand.java)

package com.marcnuri.helm;

import com.marcnuri.helm.jni.HelmLib;
import com.marcnuri.helm.jni.RollbackOptions;
import java.nio.file.Path;

public class RollbackCommand extends HelmCommand<Release> {

  private final String releaseName;
  private int revision;
  private boolean cleanupOnFail;
  private boolean dryRun;
  private boolean force;
  private boolean noHooks;
  private boolean wait;
  private int timeout;
  private int historyMax;
  private String namespace;
  private Path kubeConfig;
  private String kubeConfigContents;
  private boolean debug;

  public RollbackCommand(HelmLib helmLib, String releaseName) {
    super(helmLib);
    this.releaseName = releaseName;
  }

  @Override
  public Release call() {
    return Release.parseSingle(run(hl -> hl.Rollback(new RollbackOptions(
      releaseName,
      revision,
      toInt(cleanupOnFail),
      toInt(dryRun),
      toInt(force),
      toInt(noHooks),
      toInt(wait),
      timeout,
      historyMax,
      namespace,
      toString(kubeConfig),
      kubeConfigContents,
      toInt(debug)
    ))));
  }

  /**
   * Rollback to a specific revision.
   * If set to 0 or not called, rolls back to the previous release.
   *
   * @param revision the revision number to rollback to.
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand toRevision(int revision) {
    this.revision = revision;
    return this;
  }

  /**
   * Allow deletion of new resources created in this rollback when rollback fails.
   *
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand cleanupOnFail() {
    this.cleanupOnFail = true;
    return this;
  }

  /**
   * Simulate a rollback.
   *
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand dryRun() {
    this.dryRun = true;
    return this;
  }

  /**
   * Force resource update through delete/recreate if needed.
   *
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand force() {
    this.force = true;
    return this;
  }

  /**
   * Prevent hooks from running during rollback.
   *
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand noHooks() {
    this.noHooks = true;
    return this;
  }

  /**
   * Wait until all resources are in a ready state before marking the rollback as successful.
   *
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand waitReady() {
    this.wait = true;
    return this;
  }

  /**
   * Time to wait for any individual Kubernetes operation (in seconds).
   * Default is 300 seconds.
   *
   * @param timeout timeout in seconds.
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand withTimeout(int timeout) {
    this.timeout = timeout;
    return this;
  }

  /**
   * Limit the maximum number of revisions saved per release.
   * Use 0 for no limit. Default is 10.
   *
   * @param historyMax maximum number of revisions to keep.
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand withHistoryMax(int historyMax) {
    this.historyMax = historyMax;
    return this;
  }

  /**
   * Kubernetes namespace scope for this request.
   *
   * @param namespace the Kubernetes namespace for this request.
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand 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 RollbackCommand} instance.
   */
  public RollbackCommand 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 RollbackCommand} instance.
   */
  public RollbackCommand withKubeConfigContents(String kubeConfigContents) {
    this.kubeConfigContents = kubeConfigContents;
    return this;
  }

  /**
   * Enable verbose output.
   *
   * @return this {@link RollbackCommand} instance.
   */
  public RollbackCommand debug() {
    this.debug = true;
    return this;
  }
}

6. Add factory method in Helm.java

/**
 * Roll back a release to a previous revision.
 *
 * @param releaseName name of the release.
 * @return a new {@link RollbackCommand} instance.
 */
public static RollbackCommand rollback(String releaseName) {
  return new RollbackCommand(HelmLibHolder.INSTANCE.helmLib(), releaseName);
}

7. Add tests (helm-java/src/test/java/com/marcnuri/helm/HelmRollbackTest.java)

Acceptance Criteria

  • Create RollbackOptions Go struct in native/internal/helm/rollback.go
  • Implement Rollback function in Go using action.NewRollback
  • Add CGO export Rollback in native/main.go
  • Create RollbackOptions.java JNA structure in lib/api
  • Add Rollback method to HelmLib interface
  • Create RollbackCommand.java in helm-java module
  • Add rollback(String releaseName) factory method to Helm.java
  • Returns Release object with updated status after rollback
  • Add unit tests for the new command
  • Add integration tests using Testcontainers/KinD

Tests

Following the project's testing philosophy (black-box, no mocks, nested structure):

  • HelmRollbackTest
    • Valid
      • toPreviousRevision - Rollback without specifying revision (goes to previous)
      • toSpecificRevision - Rollback to a specific revision number
      • withDryRun - Simulate rollback without persisting
      • withWait - Wait for resources to be ready
      • withNamespace - Rollback with explicit namespace
      • withKubeConfigContents - Use inline kubeconfig
    • Invalid
      • nonExistentRelease - Should throw appropriate exception
      • invalidRevision - Should handle non-existent revision

Additional Information

  • CLI Reference: https://helm.sh/docs/helm/helm_rollback/
  • Helm SDK: Uses action.NewRollback from helm.sh/helm/v3/pkg/action
  • Priority: High - Critical for recovery and maintaining cluster stability
  • Complexity: Medium - Similar pattern to UninstallCommand
  • Dependencies: Should be implemented after helm history for complete workflow

CLI Options Mapping

CLI Flag Java Method Description
[REVISION] toRevision(int) Target revision (0 = previous)
--cleanup-on-fail cleanupOnFail() Delete new resources on failure
--dry-run dryRun() Simulate rollback
--force force() Force resource updates
--no-hooks noHooks() Prevent hooks from running
--timeout withTimeout(int) Wait timeout in seconds
--wait waitReady() Wait for resources to be ready
--history-max withHistoryMax(int) Max revisions to keep
-n, --namespace withNamespace(String) Kubernetes namespace

Notes

  • The rollback returns a Release object showing the state after rollback
  • The existing Release.parseSingle() can be reused
  • StatusReport function from helm.go formats the response
  • Should work in conjunction with helm history to identify target revisions

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions