Skip to content
Docs menu / Helm

Helm

konfig.ts does not replace Helm. Helm.release in @konfig.ts/core shells out to the helm binary, but it pins what gets rendered: an exact chart version, a digest of the tarball, and typed values. The templated documents come back as ordinary manifests, so they land in the same per-app directory as everything else and go through the same stable YAML and diff pipeline.

Helm.release

examples/full-stack/infra/modules/sops-operator.ts
export const defineSopsOperator = Application.module({
  namespace: "sops",
  annotations: Sync.wave(-2),
  build: ({ namespace }, opts: Record<never, never>) => {
    const ns = Namespace.make({ name: namespace })
    const release = Helm.release({
      repo: "https://isindir.github.io/sops-secrets-operator/",
      chart: "sops-secrets-operator",
      version: "0.19.0",
      digest: "sha256:e2a1cd7ef2c6fd53aad8fa49a1080d425c3648177a87fc20d5f9f6133cbb8e54",
      namespace,
      extraOpts: ["--include-crds"],
      values: {
        secretsAsFiles: [
          { name: "age-key", mountPath: "/etc/sops-age", secretName: "sops-age" }
        ],
        extraEnv: [{ name: "SOPS_AGE_KEY_FILE", value: "/etc/sops-age/age.key" }]
      }
    })
    return [ns, release]
  }
})

Options:

OptionMeaning
repoChart repository URL passed to helm pull --repo.
chartChart name inside the repository.
versionExact chart version. Required; there is no “latest”.
digestsha256:<hex> (the prefix is optional) of the chart tarball. Required.
releaseName?Name given to helm template; defaults to chart.
namespace?Passed as --namespace, and stamped onto every document that arrived without a metadata.namespace (missing or empty). Kinds on the built-in cluster-scoped list (Namespace, ClusterRole, ClusterRoleBinding, CustomResourceDefinition, PersistentVolume, StorageClass, IngressClass, webhook configurations, and others) are left alone.
valuesPlain object written to a temporary values.yaml.
extraOpts?Extra helm template arguments, for example ["--include-crds"].
minVersion?When set, runs helm version --short first and fails with HelmVersionTooLow { required, found } if the binary is older or cannot be run (found: "not found").

The result is a manifest producing one raw YAML document per document in the template output; each records its origin as helm:<chart>@<version>. The output is split with a real YAML multi-document parser, so a --- inside a block scalar does not split a document in two; each document is re-stringified with a leading ---. When the CLI writes files, each document is parsed again, serialised through the stable YAML writer, and named <Kind>-<name>.yaml like every other manifest. Documents without a string kind and metadata.name are dropped.

Digest verification

Chart tarballs are cached on disk so they are pulled once. The cache directory is resolved from KONFIG_HELM_CACHE, then the helm.cacheDir field in konfig.json, then the built-in default .konfig/helm-cache (see konfig.json for the precedence rule). During build, validate, and diff the resolved directory is passed through as KONFIG_HELM_CACHE to every Helm.release() call the render makes. Each entry is named <chart>-<version>-<first 12 hex chars of digest>.tgz.

On a cache miss, helm pull --repo <repo> <chart> --version <version> runs into a private temporary directory inside the cache dir (prefix .konfig-helm-pull-). Exactly one <chart>*.tgz must appear there (otherwise HelmRenderError), and it is renamed into place, so concurrent renders of the same chart cannot corrupt each other. The file is then hashed with SHA-256 and compared with digest.

On a cache hit the file is hashed again before use, so a tampered cache directory cannot slip a different chart into your manifests. A mismatch in either path removes the cached file and fails with HelmDigestMismatch { chart, version, expected, actual }, whose message reads Helm chart <chart>@<version> digest mismatch: expected sha256:..., got sha256:....

Every Helm.release failure except HelmVersionTooLow is reported as HelmRenderError { chart, version, cause }. A digest mismatch, a failed pull, or a failed template therefore arrives as the cause of a HelmRenderError. Both tags are members of the AnyRenderError union.

Cache pre-fetch and version floor

konfig helm fetch --all pulls every chart in the chart registry into the cache directory ahead of time, so a later render or CI job never has to pull. The registry is the set of .ts files (not starting with _) in the charts directory. From each file the first export marked _konfigHelmRelease: true is read; it must carry repo, chart, and version, and may carry digest (defaults to "") and id (defaults to the file name without .ts). The command first checks the helm binary against the minimum version and fails with HelmVersionTooLow if it is older, then refuses to run without --all (MissingAllFlag).

When a registry entry has a recorded digest, the pre-fetched file is cached under <chart>-<version>-<digest12>.tgz, the same name Helm.release and konfig crd extract look for. A helm fetch --all therefore warms exactly the cache slot a later render or CRD extraction reads from. Entries with no recorded digest still cache under the plain <chart>-<version>.tgz, which the digest-suffixed lookup cannot reuse; helm fetch prints a warning for those.

Paths for the helm and crd commands resolve through a shared precedence chain: an environment variable if set, otherwise the matching konfig.json field (resolved relative to konfig.json’s own directory), otherwise a built-in default (resolved against the working directory).

Environment variablekonfig.json fieldDefault
KONFIG_HELM_CACHEhelm.cacheDir.konfig/helm-cache
KONFIG_CHARTS_DIRchartsinfra/k8s-konfig/charts
KONFIG_CRD_OUT_DIRcrd.outDir.generated/crd
KONFIG_HELM_MIN_VERSIONhelm.minVersion3.16.0

See konfig.json for the full table. Helm.release’s own minVersion option is unaffected by helm.minVersion in konfig.json; it stays a plain opt-in per-call option.

CRD codegen

Charts that install CRDs (--include-crds) often need matching custom resources from your own modules. Rather than writing those by hand against an untyped object, konfig.ts can generate TypeScript types from the chart’s CRD schemas.

konfig crd extract --release <id> (or --all) does that in five steps:

  1. Checks the helm version.
  2. Untars the chart into a temp dir to read its crds/ directory.
  3. Templates the cached tarball with --include-crds --no-hooks.
  4. Dedupes the CustomResourceDefinition documents by name.
  5. Compiles the first version’s openAPIV3Schema of each CRD into a TypeScript type named <Resource>Input (for example sopssecrets.isindir.github.com becomes SopssecretsInput).

Output is one file per release at $KONFIG_CRD_OUT_DIR/<id>.ts, starting with a // Generated by @konfig.ts/cli ... header line. A chart without CRDs gets a stub that exports nothing.

konfig crd verify guards against drift: it re-extracts every registered release into a temp dir and reports files that differ from the committed ones (or lack the generated header). A chart bump that changes a CRD schema therefore fails CI until the types are regenerated. Failures in this pipeline are CrdExtractError { chart, cause }, except registry entries that fail the input schema, which are CrdInputDecodeError.

Use the generated Input type with Manifest.make to build a resource for that CRD, then return it from a module like any other manifest.

Next: Rendering.