---
title: Authoring ResourceTypes
---

# Authoring ResourceTypes

This guide covers how to create custom [ResourceTypes](https://openchoreo.dev/docs/reference/api/platform/resourcetype.md) and [ClusterResourceTypes](https://openchoreo.dev/docs/reference/api/platform/clusterresourcetype.md) in OpenChoreo. A ResourceType is the platform-engineer-defined template that governs how a managed-infrastructure resource (database, queue, cache, object store) is provisioned on the data plane and exposed to consumers.

## What is a ResourceType?

A ResourceType plays the same role for managed infrastructure that a ComponentType plays for code components: it captures the manifests the platform emits, the parameters developers can supply, the environment-specific overrides bindings can apply, and the named outputs consumers wire into their containers.

Platform engineers use ResourceTypes to:

- Publish reusable provisioning templates (Postgres, NATS, Valkey, S3 buckets, Crossplane claims)
- Define what developers can configure through a schema
- Define what consumers can wire into their workloads through declared outputs
- Set retention defaults so accidental deletes do not destroy stateful data

Developers reference a ResourceType from `Resource.spec.type` and supply parameter values. Each `ResourceReleaseBinding` then renders the template per environment, applies the rendered manifests to the data plane, and surfaces the resulting outputs back through `status.outputs`.

OpenChoreo ships example ClusterResourceTypes (`postgres`, `valkey`, `nats`) under `samples/getting-started/cluster-resource-types/` for local development and to demonstrate the pattern. These samples back stateful infrastructure with in-cluster StatefulSets and are not intended for production use; platform engineers should author their own templates targeting production-grade provisioners (Crossplane, ACK, native cloud operators).

### ClusterResourceType

A **ClusterResourceType** is the cluster-scoped variant of ResourceType. Use it for templates intended to be shared platform-wide; namespace-scoped ResourceTypes are available when a template should be available only within a specific namespace.

ClusterResourceTypes share the same spec structure as ResourceTypes; only scope differs.

**Key concepts:**

- `parameters` / `environmentConfigs` — Define what developers can configure on a Resource, and what bindings can override per environment
- `resources` — Kubernetes manifest templates the provisioner emits on the data plane (rendered through CEL)
- `outputs` — Named values that consuming workloads bind to environment variables and file mounts
- `retainPolicy` — Default deletion behavior (`Delete` or `Retain`) for bindings of this type

## ResourceType Example

The example below defines a simple Valkey (Redis-protocol) cache backed by a StatefulSet. It exposes three outputs—`host`, `port`, and `password`—that workloads can consume through `dependencies.resources[]`.

```yaml
apiVersion: openchoreo.dev/v1alpha1
kind: ResourceType
metadata:
  name: valkey-cache
  namespace: default
spec:
  # Developer-facing parameters; captured in the ResourceRelease snapshot.
  parameters:
    openAPIV3Schema:
      type: object
      properties:
        version:
          type: string
          enum: ["7", "8"]
          default: "8"

  # Per-environment overrides applied through ResourceReleaseBinding.
  environmentConfigs:
    openAPIV3Schema:
      type: object
      properties:
        memory:
          type: string
          default: "128Mi"

  # Default retention. Per-environment override available on bindings.
  retainPolicy: Delete

  # Named outputs that consuming workloads wire into containers.
  outputs:
    - name: host
      value: "${metadata.name}.${metadata.namespace}.svc.cluster.local"
    - name: port
      value: "6379"
    - name: password
      secretKeyRef:
        name: "${metadata.name}-creds"
        key: password

  # Kubernetes manifests rendered onto the data plane.
  resources:
    - id: password-secret
      template:
        apiVersion: v1
        kind: Secret
        metadata:
          name: ${metadata.name}-creds
          namespace: ${metadata.namespace}
          labels: ${metadata.labels}
        type: Opaque
        stringData:
          # Real templates generate this on the data plane (for example via
          # an ExternalSecret + Password generator) so the literal never
          # transits the control plane. See the example `valkey` sample for
          # the full pattern.
          password: "change-me"

    - id: service
      template:
        apiVersion: v1
        kind: Service
        metadata:
          name: ${metadata.name}
          namespace: ${metadata.namespace}
          labels: ${metadata.labels}
        spec:
          selector:
            app: ${metadata.name}
          ports:
            - name: valkey
              port: 6379
              targetPort: 6379

    - id: statefulset
      readyWhen: "${applied.statefulset.status.readyReplicas == applied.statefulset.status.replicas && applied.statefulset.status.replicas > 0}"
      template:
        apiVersion: apps/v1
        kind: StatefulSet
        metadata:
          name: ${metadata.name}
          namespace: ${metadata.namespace}
          labels: ${metadata.labels}
        spec:
          serviceName: ${metadata.name}
          replicas: 1
          selector:
            matchLabels:
              app: ${metadata.name}
          template:
            metadata:
              labels:
                app: ${metadata.name}
            spec:
              containers:
                - name: valkey
                  image: valkey/valkey:${parameters.version}-alpine
                  resources:
                    limits:
                      memory: ${environmentConfigs.memory}
                  env:
                    - name: VALKEY_PASSWORD
                      valueFrom:
                        secretKeyRef:
                          name: ${metadata.name}-creds
                          key: password
                  args:
                    - --requirepass
                    - $(VALKEY_PASSWORD)
                  ports:
                    - containerPort: 6379
                      name: valkey
```

Real-world templates also generate credentials on the data plane (for example through an ExternalSecret backed by a Password generator) so secret material never reaches the control plane. The example templates under `samples/getting-started/cluster-resource-types/` (`postgres`, `valkey`, `nats`) demonstrate the full pattern. They use in-cluster StatefulSets for the underlying infrastructure and are intended for local development; production templates typically target a managed-provisioner abstraction (Crossplane, ACK, native cloud operator) instead.

## Outputs

Outputs are the contract between the ResourceType and the workloads that consume it. Each output is identified by a unique `name` and picks exactly one of three source kinds:

| Source kind       | When to use                                                                                           | What transits to the control plane                                                             |
| ----------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `value`           | Non-sensitive data (host, port, region, database name, composed connection URLs)                      | The resolved literal value. Stored on the binding's `status.outputs[].value`.                  |
| `secretKeyRef`    | Sensitive credentials (passwords, tokens, private keys)                                               | Only `{name, key}` of the data-plane Secret. The underlying value never leaves the data plane. |
| `configMapKeyRef` | Non-sensitive runtime configuration sourced from a data-plane ConfigMap (CA bundles, locale settings) | Only `{name, key}` of the data-plane ConfigMap.                                                |

`value`, `secretKeyRef.name`, `secretKeyRef.key`, `configMapKeyRef.name`, and `configMapKeyRef.key` all support `${...}` CEL templating. The CEL context includes `applied.<id>.status.*`—use this to surface fields populated by the provisioner (for example a Crossplane claim's `status.connectionDetails`).

Consumers reference outputs by name through `Workload.spec.dependencies.resources[].envBindings` and `fileBindings` (see the [Resource Dependencies developer guide](https://openchoreo.dev/docs/developer-guide/dependencies/resources.md)). Outputs declared on the ResourceType but not requested by a consumer are simply unused; outputs requested by a consumer but missing on the ResourceType surface as a `ResourceDependenciesPending` reason on the consuming `ReleaseBinding`.

## Local Development Addresses

A ResourceType can mark which of its outputs form a **dialable network address**. That is what makes a resource reachable through [`occ remote`](https://openchoreo.dev/docs/developer-guide/local-development.md): the control plane opens one tunnel per declared address and re-points the consumer's bindings for that address's host and port at it. A type that marks none still delivers its configuration. It has nothing to dial.

This is declared with an **annotation**, not a spec field, and it is stamped onto every `ResourceRelease` cut from the type:

```yaml
apiVersion: openchoreo.dev/v1alpha1
kind: ClusterResourceType
metadata:
  name: postgres
  annotations:
    openchoreo.dev/local-dev-addresses: client=outputs.host:outputs.port
spec:
  outputs:
    - name: host
      value: ${"postgres-" + params.name + "." + context.namespace + ".svc.cluster.local"}
    - name: port
      value: "5432"
    - name: password
      secretKeyRef:
        name: ${params.name + "-credentials"}
        key: password
```

### Syntax

Comma-separated entries, each naming an address and the two outputs carrying its halves:

```text
<name>=outputs.<host output>:outputs.<port output>
```

```yaml
openchoreo.dev/local-dev-addresses: >-
  database=outputs.host:outputs.port,adminEndpoint=outputs.host:outputs.adminPort
```

| Part                    | Rules                                                                                                                                                     |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<name>`                | Up to 63 characters of alphanumerics, `-`, `_` and `.`, starting and ending alphanumeric. `/` is excluded because it separates tunnel target-key segments |
| `outputs.<host output>` | Must name an output declared in `spec.outputs`                                                                                                            |
| `outputs.<port output>` | Same, and no two addresses may name the same port output                                                                                                  |

**Both halves are required.** There is no way to declare a literal host or port here. An address is always expressed as a pair of outputs, because naming outputs is precisely what lets a consumer redirect _their_ env bindings along with the address. That is how a developer's `DB_HOST`/`DB_PORT` come to point at a tunnel.

Sharing a **host** output between addresses is fine, and expected for a client port and a monitoring port on the same service. Sharing a **port** output is rejected: one environment variable cannot carry a redirected address for two of them.

### What is checked, and when

At **admission**, on the ResourceType and on any ResourceRelease cut from it:

- the annotation parses, and declares at most 10 addresses
- no name is repeated, and no port output is claimed twice
- every `outputs.<name>` reference names an output declared in `spec.outputs`

A malformed annotation is rejected **as a whole** rather than having the bad entry dropped, so a typo shows up at admission instead of costing you a tunnel.

Everything else is checked when a developer runs `occ remote`. The control plane reads the annotation from the `ResourceRelease` that the consumer's `ResourceReleaseBinding` is pinned to, and evaluates it against that binding's `status.outputs`. An address that cannot yield something dialable is reported against the address it belongs to, and the resource's other addresses and bindings still resolve:

| Situation                                              | Reported to the developer as                                                                               |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| Either named output is backed by a Secret or ConfigMap | `address is published only through a Secret or ConfigMap reference, which the control plane does not read` |
| A named output has not rendered a value yet            | `output "host" has no value yet; retry once the resource reports it`                                       |
| The port output does not render a number in 1-65535    | `output "port": port "abc" is not numeric`                                                                 |

:::note
An address whose host or port comes from a `secretKeyRef` or `configMapKeyRef` output has nothing the control plane can dial, so `occ remote` reports it as unavailable rather than tunnelling it. Publish the host and port as plain `value` outputs to make an address tunnellable. A credential belongs in a separate output.
:::

Addresses add no field or condition to the binding. `ResourceReleaseBinding.status` carries only `conditions` and `outputs`, and its conditions (`Synced`, `ResourcesReady`, `OutputsResolved`, `Ready`) are unaffected by this annotation, and a resource whose addresses cannot be dialed is still `Ready`. Because resolution happens per `occ remote` invocation, an output that renders later needs nothing more than a re-run.

:::info
The annotation is named `local-dev-addresses` and this guide calls these **addresses**, but the word _endpoint_ still surfaces in two places: admission error messages (`endpoint "client": host output "hostname" is not declared in spec.outputs`) and `occ remote`'s per-tunnel label (`res/my-postgres/client -> (resource/client)`). They refer to the same thing.
:::

`occ remote` also re-points a **composed** output, such as a connection URL or driver connection string, whose text embeds a declared address's `host:port`. Annotating a type benefits those outputs too, with no extra declaration.

## How Developers Consume a ResourceType

Developers create a **Resource** that references the ResourceType, providing parameter values that conform to the declared schema. They then declare a dependency from a Workload to the Resource and bind the outputs they need:

```yaml
apiVersion: openchoreo.dev/v1alpha1
kind: Resource
metadata:
  name: doclet-cache
  namespace: default
spec:
  owner:
    projectName: doclet
  type:
    kind: ResourceType
    name: valkey-cache
  parameters:
    version: "8"
---
apiVersion: openchoreo.dev/v1alpha1
kind: Workload
metadata:
  name: doclet-document
  namespace: default
spec:
  owner:
    projectName: doclet
    componentName: doclet-document
  container:
    image: ghcr.io/openchoreo/samples/doclet-document:latest
  dependencies:
    resources:
      - ref: doclet-cache
        envBindings:
          host: REDIS_HOST
          port: REDIS_PORT
          password: REDIS_PASSWORD
```

To deploy the Resource into an environment, a platform engineer or GitOps process creates a `ResourceReleaseBinding`. The binding pins a specific `ResourceRelease`, targets an Environment, supplies per-environment overrides through `resourceTypeEnvironmentConfigs`, and optionally overrides the `retainPolicy`:

```yaml
apiVersion: openchoreo.dev/v1alpha1
kind: ResourceReleaseBinding
metadata:
  name: doclet-cache-production
  namespace: default
spec:
  owner:
    projectName: doclet
    resourceName: doclet-cache
  environment: production
  resourceRelease: doclet-cache-abc12345 # advanced via `occ resource promote`
  retainPolicy: Retain
  resourceTypeEnvironmentConfigs:
    memory: "2Gi"
```

The binding controller renders the ResourceType template with the combined Resource parameters and environment overrides, produces a `RenderedRelease` applied to the data plane, and resolves declared outputs into `status.outputs` so consuming workloads can read them.

## CEL Surface for Resource Templates

Resource templates and output expressions have access to the following CEL context. The available bindings depend on which field is being evaluated:

| Context                | In resource `template` | In `includeWhen` | In `readyWhen` | In `outputs` | Description                                                                               |
| ---------------------- | :--------------------: | :--------------: | :------------: | :----------: | ----------------------------------------------------------------------------------------- |
| `metadata.*`           |          yes           |       yes        |      yes       |     yes      | Platform-injected naming, namespace, resource/project/env UIDs, labels, annotations       |
| `parameters.*`         |          yes           |       yes        |      yes       |     yes      | Values from `Resource.spec.parameters` after schema defaulting                            |
| `environmentConfigs.*` |          yes           |       yes        |      yes       |     yes      | Values from `ResourceReleaseBinding.spec.resourceTypeEnvironmentConfigs` after defaulting |
| `environment.*`        |          yes           |       yes        |      yes       |     yes      | Per-environment surface including the merged effective gateway for this environment       |
| `dataplane.*`          |          yes           |       yes        |      yes       |     yes      | Target DataPlane attributes (secret store, raw gateway, observability ref)                |
| `gateway.*`            |          yes           |       yes        |      yes       |     yes      | Effective gateway (Environment-level override merged onto DataPlane-level default)        |
| `applied.<id>.*`       |           no           |        no        |      yes       |     yes      | Status of resources that were applied to the data plane—reference by template `id`        |

`applied.<id>.*` is not available during rendering (the manifests have not been applied yet) but is available in `readyWhen` and `outputs` because both run after the data plane reports back. Use `applied.<id>.status.*` in outputs to surface provider-populated fields, such as a Crossplane claim's connection details or a StatefulSet's observed pod count.

The `${...}` wrapper is required on `includeWhen`, `readyWhen`, and `outputs[].value`. Inside resource templates, both `${...}` interpolation (which substitutes a value into a string) and `${...}` whole-field replacement (when the entire field value is a `${...}` expression) are supported—same shape as ComponentType templates.

## includeWhen and readyWhen

Each resource template entry supports two optional CEL fields that shape its lifecycle:

**`includeWhen`** is a boolean expression evaluated at render time. When it returns `false`, the entry is omitted from the rendered output and any previously-applied object is garbage-collected from the data plane. Common uses:

- `${parameters.tlsEnabled}` — conditionally emit a Certificate/Issuer for TLS
- `${environmentConfigs.adminEnabled && has(gateway.ingress.external)}` — only emit the admin UI when explicitly enabled and the environment has external ingress

**`readyWhen`** is a boolean expression evaluated after the rendered object has been applied. When it returns `true`, the entry contributes positively to the binding's `ResourcesReady` condition. When unset, the binding falls back to the per-Kind health heuristics in `RenderedRelease` (replica counts, condition probes). Use `readyWhen` when the default heuristic does not match your provisioner's signal:

- `${applied.claim.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')}` — a Crossplane claim's `Ready` condition
- `${applied.statefulset.status.readyReplicas == applied.statefulset.status.replicas && applied.statefulset.status.replicas > 0}` — explicit StatefulSet quorum

Both fields must evaluate to a boolean and must be wrapped in `${...}`.

## retainPolicy

`retainPolicy` on the ResourceType sets the default deletion behavior for bindings of that type. Two values:

- **`Delete`** (default) — When a `ResourceReleaseBinding` is deleted, the binding controller removes the emitted data-plane manifests as part of finalization.
- **`Retain`** — The binding's finalizer holds when deleted, preserving the underlying data-plane state until the policy is flipped back to `Delete`.

Per-environment bindings can override the type-level default through `ResourceReleaseBinding.spec.retainPolicy`. Production environments typically opt into `Retain` for non-recoverable infrastructure (databases, persistent volumes) while dev and staging keep the default `Delete`.

## Syntax Systems

ResourceTypes reuse the same syntax systems documented in the Component Types guide:

| Syntax                                               | Purpose                                        | Used In                                                                |
| ---------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------- |
| [Templating](https://openchoreo.dev/docs/platform-engineer-guide/component-types/templating-syntax.md) | Dynamic value generation using CEL expressions | `resources[].template`, `includeWhen`, `readyWhen`, `outputs[]` fields |
| [Schema](https://openchoreo.dev/docs/platform-engineer-guide/component-types/schema-syntax.md)         | Parameter validation and defaults              | `parameters.openAPIV3Schema` and `environmentConfigs.openAPIV3Schema`  |

Patching (Trait-only) and CEL-based validation rules do not apply to ResourceTypes.

## CEL Reference

- **[Context Variables](https://openchoreo.dev/docs/reference/cel/context-variables.md)** — `metadata`, `parameters`, `environmentConfigs`, `dataplane`, `gateway`, `applied`
- **[Built-in Functions](https://openchoreo.dev/docs/reference/cel/built-in-functions.md)** — `oc_omit()`, `oc_merge()`, `oc_generate_name()`, `oc_dns_label()`
- **[Configuration Helpers](https://openchoreo.dev/docs/reference/cel/helper-functions.md)** — helpers for working with configs and secrets

## Next Steps

- **[Resource Dependencies (developer guide)](https://openchoreo.dev/docs/developer-guide/dependencies/resources.md)** — How developers wire ResourceType outputs into containers

## Related Resources

- [ResourceType API Reference](https://openchoreo.dev/docs/reference/api/platform/resourcetype.md) — Full CRD specification
- [ClusterResourceType API Reference](https://openchoreo.dev/docs/reference/api/platform/clusterresourcetype.md) — Cluster-scoped variant
- [Resource API Reference](https://openchoreo.dev/docs/reference/api/application/resource.md) — Developer-facing CRD
- [ResourceReleaseBinding API Reference](https://openchoreo.dev/docs/reference/api/platform/resourcereleasebinding.md) — Per-environment binding
