Helm — Patterns#

Concurrency patterns#

Signal-to-context cancellation#

  • Usage: CLI commands that can be interrupted (install, upgrade)
  • Example: pkg/cmd/install.go:314context.WithCancel(ctx), then a goroutine listens on a buffered os.Signal channel and calls cancel() on SIGTERM/SIGINT
  • Assessment: Idiomatic. Buffered channel (make(chan os.Signal, 2)) avoids signal drops. Context cancellation propagates cleanly into the action layer.

Competing channels (racing goroutines)#

  • Usage: pkg/action/upgrade.go:405-420 — the upgrade operation and context-cancellation handler race on two channels
  • Example:
    rChan := make(chan resultMessage)
    ctxChan := make(chan resultMessage)
    doneChan := make(chan any)
    go u.releasingUpgrade(rChan, ...)
    go u.handleContext(ctx, doneChan, ctxChan, ...)
    select {
    case result := <-rChan:   return result.r, result.e
    case result := <-ctxChan: return result.r, result.e
    }
  • Assessment: Effective, though a bit complex. A doneChan signals the context watcher to exit when the upgrade wins. The mutex (u.Lock) ensures rollback-on-failure completes before reporting. This is a hand-rolled context-aware race — errgroup would not express this intent as cleanly.

Fan-out with WaitGroup + result channel#

  • Usage: pkg/cmd/repo_update.go:119-150 — parallel chart repository index downloads
  • Example: One goroutine per ChartRepository, guarded by sync.WaitGroup; errors collected into a buffered channel; a coordinator goroutine closes the channel after wg.Wait(), allowing a range loop to drain results.
  • Assessment: Idiomatic fan-out/collect pattern. A write mutex (sync.Mutex{}) serializes terminal output within goroutines, which is correct but slightly noisy — a single goroutine collecting from an output channel would be cleaner.

Lazy initialization with sync.Once#

  • Usage: pkg/action/lazyclient.go — Kubernetes client construction is deferred until first use
  • Example: lazyClient struct holds a sync.Once field (initClient) and a factory function (clientFn). init() calls s.initClient.Do(func() { s.client, s.clientErr = s.clientFn() }) so construction happens exactly once.
  • Assessment: Textbook sync.Once lazy init. Avoids requiring a live cluster at startup and survives concurrent callers safely.

Mutex-protected configuration state#

  • Usage: pkg/action/action.go:114Configuration embeds sync.Mutex; pkg/action/install.go:134 and upgrade.go:129 embed sync.Mutex in action structs
  • Assessment: Straightforward mutual exclusion for shared mutable state. No sync.RWMutex for read-heavy paths, which is a minor inefficiency in pkg/storage/driver/memory.go that does use sync.RWMutex correctly.

Concurrency summary#

  • go func count: 21 (low — Helm is CLI-first, not a server)
  • select {} count: 5
  • errgroup: not used — Helm predates idiomatic errgroup adoption and uses manual WaitGroup + channel patterns throughout
  • Context cancellation: 90 context.Context occurrences; well-propagated from CLI to action layer to kube client

Error handling#

  • Style: Mixed — sentinel errors, structured error types with Unwrap(), and fmt.Errorf %w wrapping all coexist
  • Error types defined:
    • driver.StorageDriverError{ReleaseName, Err} with Unwrap() — wraps storage-layer errors with release context
    • engine.TraceableError — template rendering errors with file/line attribution
    • chart.ValidationError (string type) — schema/lint validation messages
    • cmd.CommandError — carries exit code for CLI use
    • repo.ChartNotFoundError, jsonschema.JSONSchemaValidationError, kube.kubernetesError, plugin.InvokeExecError
  • Sentinel errors: driver.ErrReleaseNotFound, driver.ErrReleaseExists, kube.ErrNoObjectsVisited, downloader.ErrNoOwnerRepo, plugin.ErrMissingMetadata, output.ErrInvalidFormatType — all declared as package-level var via errors.New
  • Wrapping approach: fmt.Errorf("…: %w", err) is the dominant style throughout. errors.Is and errors.As used at call sites (e.g., cmd/helm/helm.go:46 dispatches on CommandError via errors.As). No github.com/pkg/errors — stdlib errors package only.
  • Examples:
    • pkg/cli/values/options.go:56: fmt.Errorf("failed to parse %s: %w", filePath, err) — wrapping with context
    • pkg/storage/driver/driver.go:47: func (e *StorageDriverError) Unwrap() error { return e.Err } — chain-compatible custom type
    • pkg/cmd/root.go:483: type switch over release.Releaser interface with a default arm returning fmt.Errorf("unsupported release type: %T", rel) — type-safe error for unexpected variants

Configuration pattern#

  • Approach: Functional options + env-var-backed settings struct (no config file, no Viper)
  • ConfigurationOption pattern:
    type ConfigurationOption func(c *Configuration)
    func ConfigurationSetLogger(h slog.Handler) ConfigurationOption {  }
    func NewConfiguration(options ...ConfigurationOption) *Configuration {  }
    Used in pkg/action/action.go. Options modify a freshly allocated Configuration before it is used.
  • ClientOption pattern in pkg/registry/client.go: type ClientOption func(*Client) — identical idiom for the OCI registry client, with ~10 With* constructors (WithInsecureSkipTLSVerify, WithTLSClientConfig, WithPlainHTTP, etc.)
  • Option pattern in pkg/pusher/pusher.go: same idiom, four options
  • LinterOption in pkg/chart/v2/lint/lint.go: WithKubeVersion, WithSkipSchemaValidation
  • Assessment: The functional-options idiom is applied consistently across at least five packages. This is an idiomatic, zero-breaking-change extension mechanism used for optional/advanced configuration.

Dependency injection#

  • Approach: Manual constructor injection via *Configuration struct — no DI framework (no Wire, Dig, or Fx)
  • Evidence:
    • pkg/action/action.go defines Configuration as the single dependency container holding kube client getter, storage, engine, registry client, capabilities cache, and logger
    • Every action type (Install, Upgrade, Rollback, …) holds a *Configuration pointer set at construction: NewInstall(cfg *Configuration) *Install
    • cobra.OnInitialize defers actionConfig.Init(getter, namespace, driver) until command execution — lazy wiring avoids requiring a Kubernetes cluster for --help calls
  • Assessment: Explicit, readable, and debuggable. The trade-off is that adding a new shared dependency requires touching Configuration and every test that constructs it. Acceptable given Helm’s scope as a CLI tool, but would become unwieldy in a larger server application.

Other notable patterns#

Interface segregation (ISP in the storage driver)#

pkg/storage/driver/driver.go decomposes the Driver interface into four single-method role interfaces that are then composed:

type Creator  interface { Create(key string, rls release.Releaser) error }
type Updator  interface { Update(key string, rls release.Releaser) error }
type Deletor  interface { Delete(key string) (release.Releaser, error) }
type Queryor  interface { Query(labels map[string]string) ([]release.Releaser, error) }
type Driver   interface { Creator; Updator; Deletor; Queryor; Name() string }

This is textbook interface segregation — callers that only need to read can accept Queryor rather than the full Driver. Exemplary use of interface composition.

Lazy initialization (sync.Once + factory function)#

pkg/action/lazyclient.go wraps a sync.Once and a factory func() to construct a kubernetes.Interface on first use. The zero-value lazyClient with a clientFn is a clean separation of “how to make the client” from “when to make the client”.

Type switch for interface variant dispatch#

Type switches on interfaces are used in several places to handle the version-transition between v3 and v4 types (the release.Releaser interface):

  • pkg/storage/driver/driver.go: switch r := rel.(type) { case rspb.Release: … case *rspb.Release: … default: fmt.Errorf("unsupported release type: %T", rel) }
  • pkg/release/common.go:34,49: dispatching on Releaser and Hooker interfaces
  • pkg/registry/client.go:298-306: nested type switches to find the concrete HTTP transport for credential injection

Table-driven tests#

  • Prevalence: Heavy (267 occurrences of table-test identifiers in *_test.go files)
  • Style: Anonymous struct slices with name string field + t.Run(tt.name, …) — the idiomatic Go style
  • Example: pkg/engine/engine_test.go, pkg/chart/v2/lint/rules/ — every linting rule has an exhaustive table of valid/invalid chart inputs

Plugin type registry#

internal/plugin/plugin_type_registry.go implements a string-keyed registry (pluginTypesIndex map[string]pluginTypeMetadata) mapping plugin type strings ("cli/v1", "getter/v1", "postrenderer/v1") to Go reflect type descriptors for input/output/config message types. Used to dynamically instantiate the correct message type for each plugin invocation without a large type switch. A compact runtime registry pattern.

PostRenderer as pipeline injection point#

pkg/postrenderer/postrenderer.go defines:

type PostRenderer interface {
    Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error)
}

A single-method interface representing a YAML-transformation step injected between template rendering and Kubernetes apply. The action layer serializes all rendered YAML into a single buffer, passes it through the post-renderer, then reconstructs per-file structure. Any external tool (Kustomize, custom script, WASM) can satisfy this interface.

Functional options — With* naming convention#

Across all packages using functional options, Helm consistently names option constructors With<Thing>. 20+ With* functions identified. This is the project-wide convention for optional configuration, making the API immediately recognizable.

Generics (minimal)#

Only 2 usages:

  • pkg/registry/transport.go:55: type cloner[T any] interface {} — local unexported generic interface
  • internal/plugin/runtime.go:42: func remarshalRuntimeConfig[T RuntimeConfig](runtimeData map[string]any) (RuntimeConfig, error) — generic helper for config unmarshalling

Generics are used sparingly and only in internal/utility code — not in the public API surface. Helm targets Go 1.23+ but hasn’t broadly adopted generics, preferring explicit interface dispatch.

Builder pattern (dependency management)#

pkg/downloader.Manager.Build() and pkg/kube.Interface.Build() follow a builder-style method that constructs a derived artifact (downloaded chart dependencies, a ResourceList) from configuration accumulated on the receiver. Not a fluent builder — more of a “build from accumulated state” pattern.