Rendering a Baseline from git
A Baseline either declares its services inline in spec.services or points at a
repository in spec.source. With a source, the operator clones the repository, renders
the deployment definition and derives the service list. Inline entries then act as
per-service overrides by name.
spec:
application: shop
source:
url: https://git.bank.internal/retail/shop.git
ref: release/2026.09 # branch, tag or commit sha
path: deploy/chart
kind: helm # helm | kustomize | manifests | compose
secretRef: {name: shop-git}
helm:
valuesFiles: [values-prod.yaml]
values: {logLevel: info}
pollInterval: 5m # optional; re-resolve the ref this often
services:
- name: shop-postgres # override: rendered as StatefulSet, keep it shared
shared: true
Credentials Secret
| Key | Use |
|---|---|
token |
HTTPS token or password. username optional, defaults to nezal. |
ssh-privatekey |
SSH key. known_hosts is required; passphrase optional. |
ca.crt |
PEM bundle for a private certificate authority on the git server. |
The operator contacts nothing but this git server.
Formats
- helm: chart at
path. Dependencies must be vendored undercharts/(helm dependency build, then commit). Hooks, tests and NOTES are skipped with a warning. - kustomize: kustomization at
path. Bases must be inside the repository; URL references andhelmChartsare rejected. - manifests: every
.yaml,.ymland.jsonunderpath, recursively. - compose:
compose.yamlordocker-compose.ymlatpath. Each service becomes a Deployment plus a Service.build:is ignored: push the image to your registry and setimage:. Unsupported keys are listed per service in the warnings.
How Kubernetes objects become services
Each Service that selects a Deployment or StatefulSet becomes one nezal service, named
after the Service. Image, command, args, env, resources, replicas, working directory and
the numeric uid come from the first container. Port protocol comes from appProtocol or the
nezal.dev/protocol annotation, otherwise http, except well-known database ports which
default to tcp. The annotation nezal.dev/shared: "true", or being a StatefulSet, marks a
shared dependency. In compose, use the labels nezal.dev/protocol and nezal.dev/shared.
Health checks
A readiness probe in the source is carried over. A compose healthcheck becomes one too:
CMD runs the command directly, CMD-SHELL and the bare string form run through /bin/sh,
and disable: true or NONE means no probe at all.
Nothing is ever rendered as a liveness probe. A liveness probe restarts the pod, and a check written for production, or for a laptop, will restart a half-built preview environment forever. A readiness probe only decides whether traffic is sent, which is recoverable. Where a workload declares only a liveness probe, it is used as the readiness check and a warning says so.
With no check declared, nezal infers a TCP probe on the service's first port, so the first
request after nezal up does not reach a process that has not started listening yet.
Volumes and mounted files
| In the source | Becomes | Note |
|---|---|---|
emptyDir, compose named or anonymous volume |
emptyDir |
Ephemeral. Nothing survives a restart. |
configMap, secret, compose configs, compose secrets |
file mount | The object must already exist in the namespace. |
persistentVolumeClaim |
emptyDir, with a warning |
Mark the service shared if it owns real data. |
compose bind mount (./src:/app) |
dropped, with a warning | A host path does not exist in the cluster. |
Two rules behind that table. A preview environment is rebuilt from the repository, so state
that survived a rebuild would make it irreproducible, and nezal never provisions persistent
storage for one. And nezal never copies secret material out of a git repository into the
cluster: a compose secrets: entry names a Secret that an admin creates, it does not upload
the file compose would have read from disk.
A ConfigMap or Secret mounted as a whole directory is not carried over, because its keys are
only known at deploy time. List them under items in the source, or use subPath.
Every rendered pod has a read-only root filesystem, so a service that writes outside a
declared scratch path will fail. Add an emptyDir mount in the source for the paths it needs.
Ordering
depends_on is not an ordering guarantee in Kubernetes. Everything starts at once and a
service restarts until its dependencies answer, which usually converges within seconds. A
service that reads its database once at startup and exits rather than retrying will
crash-loop instead, so make startup connections retry.
What to look at
status.source: commit, when it was resolved and rendered, service count.status.resolvedServices: the effective list after overrides.status.renderWarnings: what was dropped or guessed.- Conditions
SourceResolvedandRenderedwith reasonsResolving,Rendering,Rendered,RenderedWithWarnings,ResolveFailed,RenderFailed.
Failures retry every two minutes. Rendered output is cached by commit, so restarting the operator or re-applying the same Baseline does not re-render.