Skip to content

feat: implement helm status command #349

@manusa

Description

@manusa

Description

The helm status command is currently not implemented in helm-java. This command is essential for monitoring and troubleshooting Helm deployments as it displays the current state and comprehensive information about a named release.

Background

The helm status command shows:

  • Last deployment time
  • Kubernetes namespace where the release exists
  • Current state (unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade, or pending-rollback)
  • Revision number
  • Description (completion or error message)
  • List of Kubernetes resources in the release
  • Test suite results (if applicable)
  • Additional notes provided by the chart

This is one of the most commonly used Helm commands for operations and troubleshooting. See the official documentation.

Related Commands

This is related to issue #97 which mentions missing commands. However, helm status was not explicitly listed there.

Proposed API

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

// Basic usage
Release status = Helm.status("my-release")
    .withKubeConfig(kubeConfigPath)
    .call();

// With namespace
Release status = Helm.status("my-release")
    .withNamespace("my-namespace")
    .withKubeConfig(kubeConfigPath)
    .call();

// With specific revision
Release status = Helm.status("my-release")
    .withRevision(3)
    .withKubeConfig(kubeConfigPath)
    .call();

// Using kubeconfig contents
Release status = Helm.status("my-release")
    .withKubeConfigContents(kubeConfigYaml)
    .call();

Implementation Guide

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

package helm

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

type StatusOptions struct {
    ReleaseName        string
    Revision           int
    Namespace          string
    KubeConfig         string
    KubeConfigContents string
}

func Status(options *StatusOptions) (string, error) {
    cfg, err := NewCfg(&CfgOptions{
        KubeConfig:         options.KubeConfig,
        KubeConfigContents: options.KubeConfigContents,
        Namespace:          options.Namespace,
    })
    if err != nil {
        return "", err
    }
    
    client := action.NewStatus(cfg)
    if options.Revision > 0 {
        client.Version = options.Revision
    }
    
    rel, err := client.Run(options.ReleaseName)
    if err != nil {
        return "", err
    }
    
    // Use existing StatusReport function from helm.go
    return StatusReport(rel, true, false), nil
}

2. Add CGO export in native/main.go

Add the C struct definition (around line 199, following existing patterns):

struct StatusOptions {
    char* releaseName;
    int revision;
    char* namespace;
    char* kubeConfig;
    char* kubeConfigContents;
};

Add the export function (following the pattern of GetValues):

//export Status
func Status(options *C.struct_StatusOptions) C.Result {
    return runCommand(func() (string, error) {
        return helm.Status(&helm.StatusOptions{
            ReleaseName:        C.GoString(options.releaseName),
            Revision:           int(options.revision),
            Namespace:          C.GoString(options.namespace),
            KubeConfig:         C.GoString(options.kubeConfig),
            KubeConfigContents: C.GoString(options.kubeConfigContents),
        })
    })
}

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

package com.marcnuri.helm.jni;

import com.sun.jna.Structure;

@Structure.FieldOrder({
  "releaseName",
  "revision",
  "namespace",
  "kubeConfig",
  "kubeConfigContents"
})
public class StatusOptions extends Structure {
  public String releaseName;
  public int revision;
  public String namespace;
  public String kubeConfig;
  public String kubeConfigContents;

  public StatusOptions(String releaseName, int revision, String namespace, 
                       String kubeConfig, String kubeConfigContents) {
    this.releaseName = releaseName;
    this.revision = revision;
    this.namespace = namespace;
    this.kubeConfig = kubeConfig;
    this.kubeConfigContents = kubeConfigContents;
  }
}

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

Result Status(StatusOptions options);

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

Following the pattern of ListCommand and GetCommand.GetValuesSubcommand:

package com.marcnuri.helm;

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

public class StatusCommand extends HelmCommand<Release> {

  private final String releaseName;
  private int revision;
  private String namespace;
  private Path kubeConfig;
  private String kubeConfigContents;

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

  @Override
  public Release call() {
    return Release.parseSingle(run(hl -> hl.Status(new StatusOptions(
      releaseName,
      revision,
      namespace,
      toString(kubeConfig),
      kubeConfigContents
    ))));
  }

  /**
   * Get status of a specific revision.
   *
   * @param revision the revision number.
   * @return this {@link StatusCommand} instance.
   */
  public StatusCommand withRevision(int revision) {
    this.revision = revision;
    return this;
  }

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

6. Add factory method in Helm.java

/**
 * Display the status of the named release.
 *
 * @param releaseName name of the release.
 * @return a new {@link StatusCommand} instance.
 */
public static StatusCommand status(String releaseName) {
  return new StatusCommand(HelmLibHolder.INSTANCE, releaseName);
}

7. 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 Status {

  @Nested
  class Valid {

    @Test
    void withName() {
      helm.install()
        .withKubeConfig(kubeConfigFile)
        .withName("status-with-name")
        .call();
      final Release result = Helm.status("status-with-name")
        .withKubeConfig(kubeConfigFile)
        .call();
      assertThat(result)
        .returns("status-with-name", Release::getName)
        .returns("deployed", Release::getStatus)
        .returns("1", Release::getRevision);
    }

    @Test
    void withNamespace() {
      helm.install()
        .withKubeConfig(kubeConfigFile)
        .withName("status-with-namespace")
        .withNamespace("status-ns")
        .createNamespace()
        .call();
      final Release result = Helm.status("status-with-namespace")
        .withNamespace("status-ns")
        .withKubeConfig(kubeConfigFile)
        .call();
      assertThat(result)
        .returns("status-with-namespace", Release::getName)
        .returns("status-ns", Release::getNamespace)
        .returns("deployed", Release::getStatus);
    }

    @Test
    void withRevision() {
      helm.install()
        .withKubeConfig(kubeConfigFile)
        .withName("status-with-revision")
        .call();
      helm.upgrade()
        .withKubeConfig(kubeConfigFile)
        .withName("status-with-revision")
        .set("image.tag", "v2")
        .call();
      final Release result = Helm.status("status-with-revision")
        .withRevision(1)
        .withKubeConfig(kubeConfigFile)
        .call();
      assertThat(result)
        .returns("status-with-revision", Release::getName)
        .returns("1", Release::getRevision);
    }

    @Test
    void withKubeConfigContents() {
      helm.install()
        .withKubeConfig(kubeConfigFile)
        .withName("status-with-kubeconfig-contents")
        .call();
      final Release result = Helm.status("status-with-kubeconfig-contents")
        .withKubeConfigContents(kubeConfigContents)
        .call();
      assertThat(result)
        .returns("status-with-kubeconfig-contents", Release::getName)
        .returns("deployed", Release::getStatus);
    }
  }

  @Nested
  class Invalid {

    @Test
    void nonExistentRelease() {
      final StatusCommand statusCommand = Helm.status("non-existent-release")
        .withKubeConfig(kubeConfigFile);
      assertThatThrownBy(statusCommand::call)
        .isInstanceOf(IllegalStateException.class)
        .hasMessageContaining("release: not found");
    }
  }
}

Acceptance Criteria

  • Create StatusOptions Go struct in native/internal/helm/status.go
  • Implement Status function in Go using action.NewStatus
  • Add CGO export Status in native/main.go
  • Create StatusOptions.java JNA structure in lib/api (with Apache License header)
  • Add Status method to HelmLib interface
  • Create StatusCommand.java in helm-java module (with Apache License header)
  • Add status(String releaseName) factory method to Helm.java
  • Existing Release class can be reused (already contains all needed fields)
  • 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 Status class within HelmKubernetesTest:

  • HelmKubernetesTest.Status
    • Valid
      • withName - Get status of an existing release
      • withNamespace - Get status with explicit namespace
      • withRevision - Get status of specific revision
      • withKubeConfigContents - Use inline kubeconfig
    • Invalid
      • nonExistentRelease - Should throw appropriate exception

Additional Information

  • CLI Reference: https://helm.sh/docs/helm/helm_status/
  • Helm SDK: Uses action.NewStatus from helm.sh/helm/v3/pkg/action
  • Priority: High - This is an essential command for release monitoring and troubleshooting
  • Complexity: Low-Medium - Similar pattern to existing GetValuesSubcommand

Notes

The existing Release class already contains the fields returned by status:

  • name, namespace, status, revision, lastDeployed, chart, appVersion, output

The StatusReport function in native/internal/helm/helm.go (line 96) already formats the release information and can be reused.

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