> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy a Helm chart

> Manage a Helm release as a single formae resource: formae owns the release, Helm owns the objects, and hooks, CRD ordering and revision history all work.

Install a Helm chart from a forma, alongside the rest of your Kubernetes
resources, with no separate `helm install` step and no Helm CLI on the machine.
`K8S::Helm::Release` hands the chart to Helm and manages the resulting release as
one formae resource.

```mermaid theme={"languages":{"custom":["/languages/pkl.json"]}}
flowchart LR
  target["k8s-target"]:::tgt

  subgraph stack["flux"]
    direction LR
    ns["Namespace"]:::res
    rel["Helm::Release"]:::res
    ns --> rel
  end

  subgraph helm["owned by Helm"]
    direction LR
    objs["Deployments, Services,<br/>CRDs, hooks"]:::ext
  end

  target --> stack
  rel -.installs.-> helm

  classDef tgt fill:#FF8201,stroke:#B25900,color:#ffffff
  classDef res fill:#FFF3E6,stroke:#FF8201,color:#02024B
  classDef ext fill:#F4F4F8,stroke:#9A9AB0,color:#02024B
  style stack fill:#ffffff,stroke:#02024B,stroke-width:2px,stroke-dasharray:6 4,color:#02024B
  style helm fill:#ffffff,stroke:#9A9AB0,stroke-width:1px,stroke-dasharray:3 3,color:#02024B
```

## The model

formae manages the **release**. Helm manages the **objects the chart renders**.

That split is the whole point. Hooks, hook weights, hook delete policies, CRD
install ordering and revision history are Helm behaviours, and the plugin gets
all of them by calling Helm rather than reimplementing them. A chart with a
`pre-install` Job runs that Job, at the right moment, once.

The objects a release renders are hidden from discovery: the release stands in
for them, and lists what it owns on `resourceNames`. Objects created *downstream*
of the chart by controllers (the Pods behind a Deployment, the Jobs behind a
CronJob) are not in the rendered manifest and are still discovered individually.

## Prerequisites

A reachable cluster. That is all.

The plugin embeds the Helm SDK, so there is no `helm` binary to install, no
`helm repo add`, and no Pkl reader on your `PATH`. Chart repositories are
resolved from `repoURL` at apply time.

## The forma

This installs Flux (controllers plus 14 CRDs) as one release. Helm installs the
CRDs before the controllers that need them, which is exactly the ordering a
client-side render could not express.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
amends "@formae/forma.pkl"

import "@formae/formae.pkl"
import "@k8s/v1.33/k8s.pkl" as k8s
import "@k8s/v1.33/core/Namespace.pkl" as ns
import "@k8s/helm/Release.pkl" as helm

forma {
  new formae.Stack {
    label = "flux"
    description = "Flux installed via Helm (controllers + 14 CRDs, one release)"
  }

  new formae.Target {
    label = "k8s-target"
    config = new k8s.Config {
      kubernetesVersion = "1.33"
      auth = new k8s.KubeconfigAuth {}
    }
  }

  local fluxNs = new ns.Namespace {
    label = "flux-system-namespace"
    metadata = new ns.NamespaceMetadata { name = "flux-system" }
  }
  fluxNs

  new helm.Release {
    label = "flux"
    metadata {
      name = "flux"
      namespace = fluxNs.res.name
    }
    chart = "flux2"
    repoURL = "https://fluxcd-community.github.io/helm-charts"
    version = "2.14.0"
  }
}
```

Apply it with one command:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
pkl project resolve examples/flux
formae apply --mode reconcile --yes examples/flux/flux-helm.pkl
```

`namespace = fluxNs.res.name` is a reference, so formae orders the namespace
before the release without any explicit dependency declaration.

## Release fields

| Field                | Type                   | Purpose                                                |
| -------------------- | ---------------------- | ------------------------------------------------------ |
| `metadata.name`      | `String`               | Helm release name                                      |
| `metadata.namespace` | `String` or resolvable | Namespace the release installs into                    |
| `chart`              | `String?`              | Chart name, or an `oci://` reference                   |
| `repoURL`            | `String?`              | Chart repository. Required alongside a bare chart name |
| `version`            | `String?`              | Chart version                                          |
| `values`             | `Dynamic?`             | Values overrides, as `new Dynamic { ... }`             |
| `createNamespace`    | `Boolean?`             | Let Helm create the namespace                          |
| `skipCrds`           | `Boolean?`             | Skip the chart's `crds/` directory                     |
| `disableHooks`       | `Boolean?`             | Turn hooks off                                         |
| `atomic`             | `Boolean?`             | Roll back automatically on a failed install or upgrade |
| `timeoutSeconds`     | `Int?`                 | Bound on the operation, and on uninstall               |

`revision`, `status`, `appVersion` and `resourceNames` are reported by the
provider, not set by you.

A bare `chart` with no `repoURL` is rejected up front, naming the fix, rather
than failing later against the cluster.

## What is not modelled

* **`helm rollback`.** Revert the values in your forma and re-apply. Note this
  fires `pre-upgrade` hooks, not `pre-rollback` hooks.
* **`helm test`.** A CI verb, not desired state.
* **Drift inside the release.** Editing a chart-owned Deployment by hand is
  invisible here, exactly as it is invisible to Helm itself.
* **Uninstall residue.** Objects from the chart's `crds/` directory, and any
  object annotated `helm.sh/resource-policy: keep`, outlive a delete and
  correctly reappear as unmanaged once no release claims them.

## Migrating from `HelmChart`

`HelmChart.pkl` and its per-version wrapper trees were removed in the Kubernetes
plugin 0.1.11. If you import `@formae-helm/v<X.Y>/HelmChart.pkl` or
`@k8s/helm/v<X.Y>/HelmChart.pkl`, that import no longer resolves.

The old model rendered a chart client-side with `helm template` and decomposed it
into individual typed formae resources. It gave you per-object state, but it
could not honour hooks at all: a `pre-install` Job became a permanent resource
that never re-ran, `hook-weight` was ignored, finished hooks accumulated, and
`test` hooks were applied on every reconcile. Charts that relied on hooks applied
silently wrong, which is a worse failure than not being supported. It was removed
rather than deprecated for that reason.

There is no mechanical rewrite. `HelmChart` produced N resources in formae state
and `Release` produces one, so the release **adopts** what the chart already
installed rather than inheriting per-object state:

1. Replace the `HelmChart` block with a `helm.Release` naming the same chart,
   version and values.
2. Drop the `@formae-helm/*` import, the `pkl-readers/helm` package dependency
   and the `pkl-reader-helm` external-reader declaration. Nothing renders a chart
   at Pkl-eval time any more.
3. Apply. The release adopts the existing objects; the per-object rows leave
   formae state.

A release this plugin did not install is refused rather than adopted silently, so
an accidental name collision fails loudly.

<Note>
  This page is about using formae to manage **other** Helm charts. Installing
  **formae itself** through a Helm chart is a different task, covered in
  [Install the agent with Helm](/documentation/guides/install-agent-helm).
</Note>

## Source

`K8S::Helm::Release` ships at the root of the Kubernetes plugin's Pkl package as
`@k8s/helm/Release.pkl`. See
[formae-plugin-kubernetes](https://github.com/platform-engineering-labs/formae-plugin-kubernetes).
