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.
The problem podule removes
Section titled “The problem podule removes”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.httpmeans “expose me over HTTP”;needs.databasemeans “give me a database”. - Provider. A module says which contracts it can provide. Traefik provides
http; Garage providesobjectStorage.
The app never names a provider. The cluster binds one.
Binding
Section titled “Binding”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.mainand the compiler tells you what broke. - The bound object must provide the contract, or evaluation fails.
Fulfilment
Section titled “Fulfilment”Binding an app to a provider does not just pass a name. The provider writes the facts the app reads:
databasewriteshost,port,user, and a passwordsecretRef.objectStoragewritesendpoint,region,bucket, and the accesssecretRefs.httpreads the bounddnszone, writeshostandpublicUrl, 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.
Two kinds of option
Section titled “Two kinds of option”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.
Preference lists
Section titled “Preference lists”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.