Helm — Architecture#

Architectural style#

Layered CLI Tool with an Embeddable Library Core.

Helm is a single-binary CLI application whose architecture is deliberately stratified into three clean layers: a Cobra command tree (pkg/cmd), a business-logic action layer (pkg/action), and a set of infrastructure adapters (pkg/kube, pkg/storage, pkg/engine, pkg/registry). The separation between the CLI layer and the action layer is architecturally load-bearing: it allows pkg/action to be imported as a library by third parties (Flux, Argo CD, etc.) without dragging in any CLI concerns.

The action layer’s shared state is managed by a single Configuration struct that acts as a dependency injection container — injected into every action type at construction time. This is manual DI, not a framework. Key dependencies (Kubernetes client, storage backend, registry client) are swappable via interfaces, making the system testable and extensible.

Component diagram (textual)#

┌──────────────────────────────────────────────────────────────────────┐
│  cmd/helm (main)                                                      │
│    Sets kube.ManagedFieldsManager, calls pkg/cmd.NewRootCmd, runs    │
└──────────────────┬───────────────────────────────────────────────────┘
                   │
┌──────────────────▼───────────────────────────────────────────────────┐
│  pkg/cmd  (Cobra command tree)                                        │
│    One file per subcommand: install.go, upgrade.go, rollback.go …    │
│    Owns: CLI flags, arg validation, output formatting                 │
│    Creates action objects, sets fields from flags, calls Run()        │
└──────────────────┬───────────────────────────────────────────────────┘
                   │ creates + calls
┌──────────────────▼───────────────────────────────────────────────────┐
│  pkg/action  (business logic layer — the embeddable API)              │
│    action.Configuration  ← central DI container                      │
│    Install / Upgrade / Rollback / Uninstall / List / Status / …      │
│    renderResources() ← template rendering + post-render pipeline     │
└────┬────────────┬────────────┬────────────────┬───────────────────────┘
     │            │            │                │
┌────▼───┐  ┌────▼──┐  ┌──────▼──────┐  ┌──────▼──────┐
│pkg/kube│  │pkg/   │  │pkg/storage  │  │pkg/registry │
│        │  │engine │  │             │  │             │
│kube.   │  │Go tpl │  │Storage      │  │OCI client   │
│Interface│ │+sprig │  │+ driver.*   │  │(ORAS-based) │
│↕client-│  │render │  │secrets/     │  │push/pull/   │
│ go     │  │       │  │configmaps/  │  │login/logout │
└────────┘  └───────┘  │memory/sql   │  └─────────────┘
                        └─────────────┘

        ↑ all of the above sit on internal/* utility packages

Core components#

action.Configuration#

  • Package: helm.sh/helm/v4/pkg/action
  • File: pkg/action/action.go
  • Responsibility: Central dependency injection container. Holds all shared dependencies for every action: Kubernetes REST client getter, release storage, kube client, OCI registry client, cluster capabilities cache, custom template functions, hook log output function, and a mutex for concurrent access.
  • Key types: Configuration struct, ConfigurationOption func type, RESTClientGetter interface
  • Dependencies: pkg/kube, pkg/storage, pkg/engine, pkg/registry, internal/logging
  • Init method: Configuration.Init(getter, namespace, helmDriver) selects the storage driver (secrets/configmaps/memory/sql) via a switch statement and wires up the Kubernetes client.

action.Install / action.Upgrade / action.Rollback / …#

  • Package: helm.sh/helm/v4/pkg/action
  • Files: install.go, upgrade.go, rollback.go, uninstall.go, etc.
  • Responsibility: Each action type encapsulates one Helm operation. Install fields hold all configuration knobs for that operation (DryRunStrategy, WaitStrategy, Timeout, ReleaseName, Namespace, …). The Run() method executes the full lifecycle: load chart → resolve dependencies → render templates → apply to Kubernetes → store release record.
  • Key types: Install, Upgrade, Rollback, Uninstall, List, Status, Get, History, ChartPathOptions
  • Dependencies: *Configuration (injected), pkg/chart, pkg/engine, pkg/kube, pkg/storage, pkg/downloader

pkg/kube.Interface#

  • Package: helm.sh/helm/v4/pkg/kube
  • File: pkg/kube/interface.go
  • Responsibility: Abstracts all Kubernetes cluster operations needed by actions. Methods: Get, Create, Update, Delete, Build (YAML → ResourceList), IsReachable, GetWaiter, GetPodList, OutputContainerLogsForPodList, BuildTable.
  • Key types: Interface, Waiter, InterfaceWaitOptions, ResourceList, Result
  • Dependencies: k8s.io/client-go, k8s.io/cli-runtime/pkg/resource
  • Implementations: kube.Client (real cluster), kubefake.FailingKubeClient / kubefake.Client (test)

pkg/storage + pkg/storage/driver#

  • Package: helm.sh/helm/v4/pkg/storage, helm.sh/helm/v4/pkg/storage/driver
  • Files: storage/storage.go, storage/driver/driver.go, driver/secrets.go, driver/configmaps.go, driver/memory.go, driver/sql.go
  • Responsibility: Versioned release record persistence. Storage is a thin wrapper that adds MaxHistory enforcement on top of a pluggable driver.Driver. The Driver interface is decomposed into small role interfaces (Creator, Updator, Deletor, Querier, Lister) that are composed into Driver.
  • Key types: storage.Storage, driver.Driver interface, driver.Secrets, driver.ConfigMaps, driver.Memory, driver.SQL
  • Dependencies: k8s.io/client-go (for secrets/configmaps drivers), PostgreSQL via jmoiron/sqlx + lib/pq (for SQL driver)

pkg/engine#

  • Package: helm.sh/helm/v4/pkg/engine
  • Responsibility: Renders a chart.Chart’s templates against a Values map using Go’s text/template augmented with sprig and Helm-specific functions. The Engine.Render() method returns a map[string]string of filename → rendered YAML content. Supports an optional lookup function that calls the live Kubernetes API for dynamic values.
  • Key types: Engine struct (zero-value usable), Render(ch, values) map[string]string
  • Dependencies: github.com/Masterminds/sprig/v3, k8s.io/client-go (for lookup function only)

pkg/cmd (Cobra command tree)#

  • Package: helm.sh/helm/v4/pkg/cmd
  • File: pkg/cmd/root.go + one file per subcommand
  • Responsibility: Defines the entire CLI surface via Cobra. NewRootCmd wires up the Configuration, registers cobra.OnInitialize (deferred driver initialization), adds all subcommands, and returns the root cobra.Command. Each subcommand file creates and configures the corresponding action type.
  • Key types: cobra.Command (from github.com/spf13/cobra), CommandError (custom exit-code carrier)
  • Dependencies: pkg/action, pkg/cli (EnvSettings), github.com/spf13/cobra

pkg/registry#

  • Package: helm.sh/helm/v4/pkg/registry
  • Responsibility: OCI registry client for pushing, pulling, logging in/out of container registries (used for chart distribution). Built on top of oras.land/oras-go and distribution/distribution content stores.
  • Key types: Client, ClientOption (functional options)
  • Dependencies: oras.land/oras-go, distribution/distribution

Data flow#

Tracing helm install myrelease ./mychart:

  1. main()pkg/cmd.NewRootCmd → Cobra parses args, routes to install command handler
  2. cobra.OnInitialize fires: actionConfig.Init(settings.RESTClientGetter(), namespace, helmDriver) — picks storage driver (e.g., Secrets), creates kube.Client backed by client-go
  3. Install command handler (pkg/cmd/install.go):
    • Creates action.Install{cfg: actionConfig}
    • Sets all fields from CLI flags (namespace, timeout, dry-run mode, etc.)
    • Calls install.Run(args)
  4. action.Install.Run():
    • Resolves chart path (local dir, tarball, OCI ref, HTTP URL) via pkg/getter
    • Loads chart via pkg/chart/loaderchart.Chart struct
    • Optionally updates dependencies via pkg/downloader
    • Coalesces values (--values files + --set flags) via pkg/cli/values
    • Calls cfg.renderResources()engine.Engine.Render() → rendered map[string]string
    • If post-renderer configured: merges YAML docs → calls PostRenderer.Run() → splits back
    • Sorts manifests (CRDs first, then by Kind) via releaseutil.SortManifests
    • Pre-install hooks: cfg.KubeClient.Create(hooks) → wait for completion
    • Main resources: cfg.KubeClient.Create(manifests) (client-side or server-side apply)
    • Wait for readiness if --wait set: kube.Waiter.Wait(resources, timeout)
    • Post-install hooks
    • Stores release record: cfg.Releases.Create(release) → serialized into Kubernetes Secret (or other backend)
  5. Release record written, NOTES.txt printed to stdout

Initialization / Bootstrap#

main()
  └─ kube.ManagedFieldsManager = "helm"
  └─ helmcmd.NewRootCmd(stdout, args, SetupLogging)
        ├─ action.NewConfiguration()           // empty Configuration, no storage yet
        ├─ newRootCmdWithConfig(actionConfig, …)
        │     ├─ parse flags early (for log level)
        │     ├─ SetupLogging(debug)            // configures log/slog
        │     └─ registers all subcommands
        └─ cobra.OnInitialize(func() {
              actionConfig.Init(                // deferred until command execution
                settings.RESTClientGetter(),
                settings.Namespace(),
                os.Getenv("HELM_DRIVER"),
              )
           })
  └─ cmd.Execute()

Dependency injection pattern: Manual constructor injection via the Configuration struct. No DI framework (no Wire, Dig, or Fx). Each action type receives *Configuration in its constructor (NewInstall(cfg), etc.). ConfigurationOption functional options allow customizing the Configuration before use.

The HELM_DRIVER environment variable selects the storage driver at startup via a switch in Configuration.Init(). The Kubernetes REST client is lazy — constructed from genericclioptions.RESTClientGetter on first use, not at initialization time (lazyClient wrapper pattern in pkg/action/lazyclient.go).

Configuration#

Helm uses a three-tier configuration model:

  1. Environment variables (HELM_*): Primary configuration mechanism. pkg/cli.EnvSettings reads ~20 HELM_* env vars (cache path, config path, namespace, driver, max history, kubeconfig, TLS settings, etc.) and exposes them as fields on a settings struct. These map to XDG-compliant filesystem paths via pkg/helmpath.

  2. Persistent CLI flags: Cobra persistent flags on the root command shadow env vars (e.g., --namespace, --debug, --kubeconfig, --kube-context). settings.AddFlags(flags) registers them all at once.

  3. Kubeconfig / cluster access: The genericclioptions.RESTClientGetter from k8s.io/cli-runtime reads the standard kubeconfig chain (KUBECONFIG env, ~/.kube/config, in-cluster service account). Helm does not have its own kubeconfig format.

No Viper. Configuration is simple and explicit: env vars → EnvSettings struct → passed into Configuration.Init().

Key design decisions#

  1. pkg/action as the stable embeddable API. The separation of CLI (pkg/cmd) from business logic (pkg/action) is the most intentional architectural choice. The package doc explicitly states: “This is a library for calling top-level Helm actions.” Projects like Flux and Argo CD import pkg/action directly. This imposes discipline: pkg/action must not import pkg/cmd, and its public API must be treated as a library contract.

  2. Configuration as a manual DI container. Rather than using a DI framework, Helm passes a single *Configuration struct into every action. This is explicit, debuggable, and easy to understand. The ConfigurationOption functional-option pattern allows incremental customization. The trade-off is that adding a new shared dependency requires modifying Configuration and updating all callers.

  3. Storage driver strategy pattern. driver.Driver is an interface with four implementations (Secrets, ConfigMaps, Memory, SQL). The selection happens at startup via a switch on HELM_DRIVER. The SQL backend is new in v4 (PostgreSQL via jmoiron/sqlx). This cleanly isolates release persistence from all action logic — actions call cfg.Releases.Get/Create/Update, never touching Kubernetes directly for storage.

  4. kube.Interface for full testability. All Kubernetes cluster operations go through kube.Interface, which has a fake implementation in pkg/kube/fake/. This means action tests can run entirely without a cluster. The real implementation (kube.Client) drives client-go with server-side apply support via fluxcd/cli-utils.

  5. Post-renderer extensibility via pipeline injection. The postrenderer.PostRenderer interface (Run(io.Reader) (*bytes.Buffer, error)) sits between template rendering and Kubernetes apply. Helm’s renderResources() serializes all rendered YAML into a merged stream, passes it through the post-renderer, then reconstructs the per-file map via filename annotations. This allows arbitrary YAML transformation (Kustomize, custom scripts) without modifying Helm internals. In v4, a WASM-based post-renderer is being introduced alongside the existing exec-based one.