Skip to content

Contracts, needs, and providers

podule replaces a chart’s hardcoded dependencies with a named contract and a binding the cluster controls. An app says what it needs; the cluster says who provides it.

A chart wires an app to one specific implementation. The Traefik ingress annotations live in the app’s values. The Postgres host is a literal postgresql.postgresql.svc.cluster.local. Moving the database, or swapping the ingress controller, means editing every app that depended on it.

podule splits that into three roles.

  • Contract. A named kind of resource an app can ask for: http, database, objectStorage, volume, and so on. A contract defines the options a consumer sets and the facts a provider must write back.
  • Need. An app declares the contracts it uses. needs.http means “expose me over HTTP”; needs.database means “give me a database”.
  • Provider. A module says which contracts it can provide. Traefik provides http; Garage provides objectStorage.

The app never names a provider. The cluster binds one.

A cluster sets a default provider for each contract, and an app can override it for one need.

providers = {
http = apps.traefik.main;
database = apps.db-operator.main;
objectStorage = apps.garage.main;
};
# Every app that needs objectStorage now uses Garage ...
apps.nextcloud.main = { };
apps.paperless.docs = { };
# ... except this one, which points at MinIO instead.
apps.media.main.needs.objectStorage.provider = apps.minio.main;

Because the binding is a reference, not a string, three things follow:

  • The value is the object itself, so a typo is an evaluation error, not a silent misconfiguration. providers.http = "traefik" is rejected.
  • A rename touches the reference. Move apps.traefik.main and the compiler tells you what broke.
  • The bound object must provide the contract, or evaluation fails.

Binding an app to a provider does not just pass a name. The provider writes the facts the app reads:

  • database writes host, port, user, and a password secretRef.
  • objectStorage writes endpoint, region, bucket, and the access secretRefs.
  • http reads the bound dns zone, writes host and publicUrl, and enqueues an Ingress.

The app reads those facts, never another app’s service name. Paperless asks for needs.database.host; it does not know whether that is a Database CR on a local Postgres or a managed instance.

Each contract has options in two directions. Knowing which is which explains most of the reference.

  • You set options the consumer chooses: needs.database.engine, needs.volume.data.size, needs.http.host.
  • The provider writes facts the consumer reads: needs.database.host, needs.objectStorage.endpoint, needs.http.publicUrl.

An option like needs.http.host is both: you may set it to override the default, and if you leave it unset the provider fills it in.

Some contracts have several interchangeable providers. providers.runtime accepts a preference-ordered list, and the first entry the workload supports is used.

providers.runtime = [
impl.runtime.nixkube
impl.runtime.nix-oci
impl.runtime.oci
];

A module that needs runtime runs on the first of these it can run on. Pin one for a single app with needs.runtime.provider.