Skip to content

feat: implement helm pull command #101

@bradctrlplus

Description

@bradctrlplus

Description

The helm pull command is currently not implemented in helm-java. This command downloads a chart from a repository and optionally unpacks it in a local directory, which is essential for inspecting, modifying, or repackaging charts before installation.

Background

The helm pull command:

  • Downloads a chart from a repository to the local filesystem
  • Can unpack (untar) the chart after downloading
  • Supports cryptographic verification of charts
  • Can fetch charts from both traditional Helm repositories and OCI registries
  • Useful for offline installations, chart inspection, and chart customization

See the official documentation.

Use Cases

  1. Inspect charts before installation - Download and review chart contents
  2. Offline installations - Pre-download charts for air-gapped environments
  3. Chart customization - Download, modify, and repackage charts
  4. CI/CD pipelines - Cache charts locally for reproducible builds
  5. Chart verification - Verify chart signatures before deployment

Proposed API

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

// Basic usage - download chart to current directory
Path chartPath = Helm.pull("bitnami/nginx")
    .call();

// Download to specific destination
Path chartPath = Helm.pull("bitnami/nginx")
    .withDestination(Paths.get("/tmp/charts"))
    .call();

// Download and untar
Path chartPath = Helm.pull("bitnami/nginx")
    .withDestination(Paths.get("/tmp/charts"))
    .untar()
    .call();

// Download specific version
Path chartPath = Helm.pull("bitnami/nginx")
    .withVersion("15.0.0")
    .withDestination(Paths.get("/tmp/charts"))
    .call();

// Download from specific repo URL
Path chartPath = Helm.pull("nginx")
    .withRepo("https://charts.bitnami.com/bitnami")
    .withDestination(Paths.get("/tmp/charts"))
    .call();

// With authentication
Path chartPath = Helm.pull("my-private-chart")
    .withRepo("https://private-repo.example.com")
    .withUsername("user")
    .withPassword("pass")
    .withDestination(Paths.get("/tmp/charts"))
    .call();

// With verification
Path chartPath = Helm.pull("bitnami/nginx")
    .verify()
    .withKeyring(Paths.get("~/.gnupg/pubring.gpg"))
    .withDestination(Paths.get("/tmp/charts"))
    .call();

// From OCI registry
Path chartPath = Helm.pull("oci://registry.example.com/charts/nginx")
    .withVersion("1.0.0")
    .withDestination(Paths.get("/tmp/charts"))
    .call();

// Full example with all options
Path chartPath = Helm.pull("bitnami/nginx")
    .withVersion("^15.0.0")
    .withRepo("https://charts.bitnami.com/bitnami")
    .withDestination(Paths.get("/tmp/charts"))
    .untar()
    .withUntarDir("nginx-chart")
    .withUsername("user")
    .withPassword("pass")
    .withCaFile(Paths.get("/path/to/ca.crt"))
    .withCertFile(Paths.get("/path/to/cert.crt"))
    .withKeyFile(Paths.get("/path/to/key.key"))
    .verify()
    .withKeyring(Paths.get("~/.gnupg/pubring.gpg"))
    .devel()
    .passCredentials()
    .debug()
    .call();

Implementation Guide

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

package helm

import (
    "os"
    "path/filepath"
    
    "helm.sh/helm/v3/pkg/action"
    "helm.sh/helm/v3/pkg/cli"
)

type PullOptions struct {
    Chart              string
    Version            string
    Repo               string
    Destination        string
    Untar              bool
    UntarDir           string
    Username           string
    Password           string
    CertFile           string
    KeyFile            string
    CaFile             string
    InsecureSkipTlsVerify bool
    PlainHttp          bool
    Verify             bool
    Keyring            string
    Devel              bool
    PassCredentials    bool
    RepositoryConfig   string
    RepositoryCache    string
    Debug              bool
}

func Pull(options *PullOptions) (string, error) {
    var log action.DebugLog = nil
    if options.Debug {
        log = debugLog
    }
    
    settings := cli.New()
    if options.RepositoryConfig != "" {
        settings.RepositoryConfig = options.RepositoryConfig
    }
    if options.RepositoryCache != "" {
        settings.RepositoryCache = options.RepositoryCache
    }
    
    client := action.NewPullWithOpts(action.WithConfig(&action.Configuration{}))
    client.Settings = settings
    
    // Version options
    if options.Version != "" {
        client.Version = options.Version
    }
    client.Devel = options.Devel
    
    // Destination
    if options.Destination != "" {
        client.DestDir = options.Destination
    } else {
        client.DestDir = "."
    }
    
    // Untar options
    client.Untar = options.Untar
    if options.UntarDir != "" {
        client.UntarDir = options.UntarDir
    }
    
    // Repository options
    if options.Repo != "" {
        client.RepoURL = options.Repo
    }
    client.Username = options.Username
    client.Password = options.Password
    client.PassCredentialsAll = options.PassCredentials
    
    // TLS options
    client.CertFile = options.CertFile
    client.KeyFile = options.KeyFile
    client.CaFile = options.CaFile
    client.InsecureSkipTLSverify = options.InsecureSkipTlsVerify
    client.PlainHTTP = options.PlainHttp
    
    // Verification options
    client.Verify = options.Verify
    if options.Keyring != "" {
        client.Keyring = options.Keyring
    }
    
    output, err := client.Run(options.Chart)
    if err != nil {
        return "", err
    }
    
    return output, nil
}

2. Add CGO export in native/main.go

Add the C struct definition:

struct PullOptions {
    char* chart;
    char* version;
    char* repo;
    char* destination;
    int untar;
    char* untarDir;
    char* username;
    char* password;
    char* certFile;
    char* keyFile;
    char* caFile;
    int insecureSkipTlsVerify;
    int plainHttp;
    int verify;
    char* keyring;
    int devel;
    int passCredentials;
    char* repositoryConfig;
    char* repositoryCache;
    int debug;
};

Add the export function:

//export Pull
func Pull(options *C.struct_PullOptions) C.Result {
    return result(helm.Pull(&helm.PullOptions{
        Chart:                 C.GoString(options.chart),
        Version:               C.GoString(options.version),
        Repo:                  C.GoString(options.repo),
        Destination:           C.GoString(options.destination),
        Untar:                 options.untar == 1,
        UntarDir:              C.GoString(options.untarDir),
        Username:              C.GoString(options.username),
        Password:              C.GoString(options.password),
        CertFile:              C.GoString(options.certFile),
        KeyFile:               C.GoString(options.keyFile),
        CaFile:                C.GoString(options.caFile),
        InsecureSkipTlsVerify: options.insecureSkipTlsVerify == 1,
        PlainHttp:             options.plainHttp == 1,
        Verify:                options.verify == 1,
        Keyring:               C.GoString(options.keyring),
        Devel:                 options.devel == 1,
        PassCredentials:       options.passCredentials == 1,
        RepositoryConfig:      C.GoString(options.repositoryConfig),
        RepositoryCache:       C.GoString(options.repositoryCache),
        Debug:                 options.debug == 1,
    }))
}

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

package com.marcnuri.helm.jni;

import com.sun.jna.Structure;

@Structure.FieldOrder({
  "chart",
  "version",
  "repo",
  "destination",
  "untar",
  "untarDir",
  "username",
  "password",
  "certFile",
  "keyFile",
  "caFile",
  "insecureSkipTlsVerify",
  "plainHttp",
  "verify",
  "keyring",
  "devel",
  "passCredentials",
  "repositoryConfig",
  "repositoryCache",
  "debug"
})
public class PullOptions extends Structure {
  public String chart;
  public String version;
  public String repo;
  public String destination;
  public int untar;
  public String untarDir;
  public String username;
  public String password;
  public String certFile;
  public String keyFile;
  public String caFile;
  public int insecureSkipTlsVerify;
  public int plainHttp;
  public int verify;
  public String keyring;
  public int devel;
  public int passCredentials;
  public String repositoryConfig;
  public String repositoryCache;
  public int debug;

  public PullOptions(
    String chart,
    String version,
    String repo,
    String destination,
    int untar,
    String untarDir,
    String username,
    String password,
    String certFile,
    String keyFile,
    String caFile,
    int insecureSkipTlsVerify,
    int plainHttp,
    int verify,
    String keyring,
    int devel,
    int passCredentials,
    String repositoryConfig,
    String repositoryCache,
    int debug
  ) {
    this.chart = chart;
    this.version = version;
    this.repo = repo;
    this.destination = destination;
    this.untar = untar;
    this.untarDir = untarDir;
    this.username = username;
    this.password = password;
    this.certFile = certFile;
    this.keyFile = keyFile;
    this.caFile = caFile;
    this.insecureSkipTlsVerify = insecureSkipTlsVerify;
    this.plainHttp = plainHttp;
    this.verify = verify;
    this.keyring = keyring;
    this.devel = devel;
    this.passCredentials = passCredentials;
    this.repositoryConfig = repositoryConfig;
    this.repositoryCache = repositoryCache;
    this.debug = debug;
  }
}

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

Result Pull(PullOptions options);

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

package com.marcnuri.helm;

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

public class PullCommand extends HelmCommand<Path> {

  private final String chart;
  private String version;
  private String repo;
  private Path destination;
  private boolean untar;
  private String untarDir;
  private String username;
  private String password;
  private Path certFile;
  private Path keyFile;
  private Path caFile;
  private boolean insecureSkipTlsVerify;
  private boolean plainHttp;
  private boolean verify;
  private Path keyring;
  private boolean devel;
  private boolean passCredentials;
  private Path repositoryConfig;
  private Path repositoryCache;
  private boolean debug;

  public PullCommand(HelmLib helmLib, String chart) {
    super(helmLib);
    this.chart = chart;
  }

  @Override
  public Path call() {
    String output = run(hl -> hl.Pull(new PullOptions(
      chart,
      version,
      repo,
      toString(destination),
      toInt(untar),
      untarDir,
      username,
      password,
      toString(certFile),
      toString(keyFile),
      toString(caFile),
      toInt(insecureSkipTlsVerify),
      toInt(plainHttp),
      toInt(verify),
      toString(keyring),
      toInt(devel),
      toInt(passCredentials),
      toString(repositoryConfig),
      toString(repositoryCache),
      toInt(debug)
    ))).out;
    // Parse output to return the path to the downloaded chart
    return Paths.get(output.trim());
  }

  /**
   * Specify a version constraint for the chart version to use.
   * This constraint can be a specific tag (e.g. 1.1.1) or a valid range (e.g. ^2.0.0).
   * If not specified, the latest version is used.
   *
   * @param version the version constraint.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withVersion(String version) {
    this.version = version;
    return this;
  }

  /**
   * Chart repository URL where the requested chart is located.
   *
   * @param repo the repository URL.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withRepo(String repo) {
    this.repo = repo;
    return this;
  }

  /**
   * Location to write the chart. Defaults to current directory.
   *
   * @param destination the destination directory.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withDestination(Path destination) {
    this.destination = destination;
    return this;
  }

  /**
   * If true, will untar the chart after downloading it.
   *
   * @return this {@link PullCommand} instance.
   */
  public PullCommand untar() {
    this.untar = true;
    return this;
  }

  /**
   * Directory into which the chart is expanded when using untar.
   * Defaults to current directory.
   *
   * @param untarDir the directory name for the expanded chart.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withUntarDir(String untarDir) {
    this.untarDir = untarDir;
    return this;
  }

  /**
   * Chart repository username for authentication.
   *
   * @param username the username.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withUsername(String username) {
    this.username = username;
    return this;
  }

  /**
   * Chart repository password for authentication.
   *
   * @param password the password.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withPassword(String password) {
    this.password = password;
    return this;
  }

  /**
   * Identify HTTPS client using this SSL certificate file.
   *
   * @param certFile the path to the certificate file.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withCertFile(Path certFile) {
    this.certFile = certFile;
    return this;
  }

  /**
   * Identify HTTPS client using this SSL key file.
   *
   * @param keyFile the path to the key file.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withKeyFile(Path keyFile) {
    this.keyFile = keyFile;
    return this;
  }

  /**
   * Verify certificates of HTTPS-enabled servers using this CA bundle.
   *
   * @param caFile the path to the CA bundle file.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withCaFile(Path caFile) {
    this.caFile = caFile;
    return this;
  }

  /**
   * Skip TLS certificate verification for the chart download.
   *
   * @return this {@link PullCommand} instance.
   */
  public PullCommand insecureSkipTlsVerify() {
    this.insecureSkipTlsVerify = true;
    return this;
  }

  /**
   * Use insecure HTTP connections for the chart download.
   *
   * @return this {@link PullCommand} instance.
   */
  public PullCommand plainHttp() {
    this.plainHttp = true;
    return this;
  }

  /**
   * Verify the package before using it.
   * The chart MUST have a provenance file and MUST pass verification.
   *
   * @return this {@link PullCommand} instance.
   */
  public PullCommand verify() {
    this.verify = true;
    return this;
  }

  /**
   * Location of public keys used for verification.
   * Defaults to ~/.gnupg/pubring.gpg.
   *
   * @param keyring the path to the keyring file.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withKeyring(Path keyring) {
    this.keyring = keyring;
    return this;
  }

  /**
   * Use development versions too. Equivalent to version '>0.0.0-0'.
   * Ignored if --version is set.
   *
   * @return this {@link PullCommand} instance.
   */
  public PullCommand devel() {
    this.devel = true;
    return this;
  }

  /**
   * Pass credentials to all domains.
   *
   * @return this {@link PullCommand} instance.
   */
  public PullCommand passCredentials() {
    this.passCredentials = true;
    return this;
  }

  /**
   * Path to the file containing repository names and URLs.
   *
   * @param repositoryConfig the path to the repository config file.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withRepositoryConfig(Path repositoryConfig) {
    this.repositoryConfig = repositoryConfig;
    return this;
  }

  /**
   * Path to the directory containing cached repository indexes.
   *
   * @param repositoryCache the path to the repository cache directory.
   * @return this {@link PullCommand} instance.
   */
  public PullCommand withRepositoryCache(Path repositoryCache) {
    this.repositoryCache = repositoryCache;
    return this;
  }

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

6. Add factory method in Helm.java

/**
 * Download a chart from a repository.
 *
 * @param chart the chart reference (e.g., "bitnami/nginx" or OCI URL).
 * @return a new {@link PullCommand} instance.
 */
public static PullCommand pull(String chart) {
  return new PullCommand(HelmLibHolder.INSTANCE.helmLib(), chart);
}

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

Acceptance Criteria

  • Create PullOptions Go struct in native/internal/helm/pull.go
  • Implement Pull function in Go using action.NewPullWithOpts
  • Add CGO export Pull in native/main.go
  • Create PullOptions.java JNA structure in lib/api
  • Add Pull method to HelmLib interface
  • Create PullCommand.java in helm-java module
  • Add pull(String chart) factory method to Helm.java
  • Return the path to the downloaded chart file/directory
  • Support OCI registry URLs
  • Add unit tests for the new command
  • Add integration tests (can use the built-in test repo server)

Tests

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

  • HelmPullTest
    • Valid
      • fromRepo - Pull chart from a repository
      • withVersion - Pull specific version
      • withDestination - Download to specific directory
      • withUntar - Download and unpack
      • withRepoUrl - Pull using explicit repo URL
      • fromOciRegistry - Pull from OCI registry
    • Invalid
      • nonExistentChart - Should throw appropriate exception
      • invalidVersion - Should handle non-existent version

CLI Options Mapping

CLI Flag Java Method Description
[chart] constructor arg Chart reference
-d, --destination withDestination(Path) Download location
--untar untar() Unpack after download
--untardir withUntarDir(String) Unpack directory name
--version withVersion(String) Version constraint
--repo withRepo(String) Repository URL
--username withUsername(String) Repo username
--password withPassword(String) Repo password
--verify verify() Verify package
--keyring withKeyring(Path) Verification keyring
--prov - (fetch provenance only, low priority)
--devel devel() Include dev versions
--pass-credentials passCredentials() Pass creds to all domains
--ca-file withCaFile(Path) CA bundle
--cert-file withCertFile(Path) SSL certificate
--key-file withKeyFile(Path) SSL key
--insecure-skip-tls-verify insecureSkipTlsVerify() Skip TLS verify
--plain-http plainHttp() Use HTTP

Additional Information

  • CLI Reference: https://helm.sh/docs/helm/helm_pull/
  • Helm SDK: Uses action.NewPullWithOpts from helm.sh/helm/v3/pkg/action
  • Priority: Medium - Essential for offline installations and chart inspection
  • Complexity: Medium - Many options but straightforward pattern

Notes

  • The command should return the Path to the downloaded chart (either .tgz file or unpacked directory)
  • When --verify is used, the chart MUST have a provenance file and pass verification
  • The --prov flag (fetch provenance without verification) is low priority and can be added later
  • OCI registry support should work with URLs like oci://registry.example.com/charts/nginx

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions