Configuration
Every setting of the wrapper, the web front, the results proxy, the
converter, the htrflow-batch chart and the frontend build, generated from
the config models, the chart's values.yaml and the frontend's config.ts. The htrflow-devstack chart — the development support
stack (RustFS, an in-cluster registry, devStack.insecureDefaults,
rustfs.accessKey/secretKey) — is a separate surface, documented in its own
README.
The wrapper's Config is not all it reads from the environment: the few
names read directly are listed under "Also read from the environment", and
described in Wrapper. Where the web front falls back to a
computed value rather than the model's own default, the Default column says
so.
Read outside these models. htrflow-campaigns apply reads
HTRFLOW_APPLIED_BY, stamped as applied-by on each campaign it applies;
unset, it uses the OS user (htrflow-campaigns CLI).
What the browser is told comes from /config.js, which the web front
writes from HTRFLOW_RESULTS_URL; there is no second copy to keep in
step (Web front & read API).
Two caps. FETCH_MAX_BYTES bounds an image on the wire and
MAX_IMAGE_PIXELS bounds what decoding it costs; a page over either fails
without a retry.
Security in four points
- Credentials are mounted files, with one scoped exception: S3
credentials reach a pod as a mounted Secret file
(
AWS_SHARED_CREDENTIALS_FILE,/secrets/s3/credentials), andpackages/wrapper/tests/test_config.pyfails if any field of these models, or any literal env read in the wrapper, is ever named like one. The exception isHF_TOKEN, whichhuggingface_hubreads from the environment:converter.yaml'shf_token_secretnames a Secret whosetokenkey the converter renders asHF_TOKENinto the warm-up pod alone — the short-lived pod that downloads models and holds nothing else. Campaign pods get neither the env nor the Secret name, and the test's exemption is per file, so reading a token anywhere but the warm-up entrypoint still fails. The devstack's own S3 store refuses to render on credentials nobody chose (devStack.insecureDefaults). - The read API needs a login, the site does not:
GET /api/v1/jobs[/…]answers401without a session on the results store, but the campaign browser's page, which asks for the login, is served to anyone who can reach the port. The network decides who gets that far: on a NodePort,network.web.ingressCidrs(the clients' own addresses); behind an ingress controller (web.ingress),network.web.ingressFromnames the controller and the controller's own allow-list is what keeps browsers out. - The read API's RBAC is get and list, plus one write:
createandpatchon ConfigMaps in its own namespace, for the per-campaign status ConfigMap it writes from what it observes (The record a campaign leaves). It is a namespacedRole; it grants nowatchordeleteand no write to a Job or a Pod, andtest_chart_agreement.pyholds the rendered Role to exactly that. Withsecurity.policies.enabled, a Kyverno rule also holds its ConfigMap writes to names of the formcampaign-<name>-status. - The results bucket is private: nothing in it is anonymous. The
results proxy reads it with each logged-in person's own store keys,
sealed in an
HttpOnlycookie only the proxy can open, and no pod holds a credential to read results. See the results boundary.
The Set by column says who can set each key in a deployment, read off
what really sets it: the converter's Job skeleton and the job-shape
policy's list of the env a Job may carry, the chart's web Deployment, the
images' own ENV and the compose stack. A local run only means exactly
that: nothing in a deployment can set it — no converter key renders it,
and with security.policies on, job-shape refuses a Job that sets it — so
it is for a hand run of the image, or the compose stack. The chart sets
HTRFLOW_NAMESPACES to its own release namespace and offers no value for
it: the read API parses a list, but one release reads one namespace.
The Security column below reads what the key exposes — who enforces it:
cluster = the API server or an admission policy, render = helm
template refuses it, nobody = convention only.
wrapper — the batch Job's container
Each key is an environment variable of the container.
| Key | Set by | Default | Must agree with | Security |
|---|---|---|---|---|
VOLUME_REF |
the campaign file: its volume list, one entry per index | required | — | no secret — nobody |
PIPELINE_PATH |
the converter, fixed: /config/pipeline.yaml |
required | — | no secret — nobody |
PIPELINE_ID |
the pipeline file's id |
required | — | no secret — nobody |
S3_ENDPOINT |
the S3 Secret (s3_secret), its S3_ENDPOINT key |
(empty) | — | from the S3 Secret (secretKeyRef) — cluster |
S3_VERIFY_TLS |
the S3 Secret (s3_secret), its S3_VERIFY_TLS key |
true |
— | false skips the S3 certificate check, so the pod would send its S3 keys (a logged-in user's own, for the results proxy) to whatever answers at S3_ENDPOINT; set by whoever writes the S3 Secret — cluster |
S3_BUCKET |
the S3 Secret (s3_secret), its S3_BUCKET key |
required | — | from the S3 Secret (secretKeyRef) — cluster |
RESULTS_URL |
converter.yaml results_url |
required | chart resultsUrl, converter results_url, web HTRFLOW_RESULTS_URL |
the results URL — nobody |
IIIF_MANIFEST_URL |
the campaign file: its volume list, one entry per index | (empty) | — | no secret — nobody |
IMAGES |
the campaign file: its volume list, one entry per index | (empty) | — | no secret — nobody |
S3_PREFIX |
converter.yaml namespace |
(empty) | — | no secret — nobody |
MAX_IMAGE_WIDTH |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | 2500 |
— | no secret — nobody |
RESUME |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | true |
— | no secret — nobody |
LOOKAHEAD_PAGES |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | 64 |
— | no secret — nobody |
LOOKAHEAD_BYTES |
half the pipeline's size's workdir (converter.yaml sizes); no size, the default |
1073741824 |
— | no secret — nobody |
MAX_PAGES |
a local run only (the compose stack sets it): no converter key renders it, and job-shape refuses a Job that sets it | 0 |
— | no secret — nobody |
WORKDIR_PATH |
the converter, fixed: /work |
/work |
— | no secret — nobody |
DOWNLOAD_CONCURRENCY |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | 12 |
— | no secret — nobody |
LOG_SHIP_SECONDS |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | 15.0 |
— | no secret — nobody |
MANIFEST_MAX_BYTES |
converter.yaml manifest_max_bytes |
16777216 |
— | no secret — nobody |
FETCH_MAX_BYTES |
converter.yaml fetch_max_bytes |
67108864 |
— | no secret — nobody |
IMAGE_CACHE_BUCKET |
converter.yaml image_cache.bucket; no image_cache, never set |
(empty) | — | no secret — nobody |
DOWNLOAD_DEADLINE_SECONDS |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | 300.0 |
— | no secret — nobody |
MAX_IMAGE_PIXELS |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | 100000000 |
— | no secret — nobody |
PAGE_TIMEOUT_SECONDS |
a local run only: no converter key renders it, and job-shape refuses a Job that sets it | 600.0 |
— | no secret — nobody |
IMAGE_DIGEST |
the pipeline file's image |
unknown |
— | no secret — nobody |
HTRFLOW_BASE_REVISION |
the image build (ENV): the htrflow revision the image was built from |
unknown |
— | no secret — nobody |
INDEX_FAILURE_COUNT |
the Job controller, per attempt (downward API) | 0 |
— | no secret — nobody |
BACKOFF_LIMIT_PER_INDEX |
the converter, fixed: the Job's backoffLimitPerIndex |
-1 |
— | no secret — nobody |
Config is not the whole wrapper env: these 6
names are read directly, by the warm-up entrypoint or by the Job
skeleton, never as a campaign setting.
Also read from the environment
| Key | Read by | Why not Config |
|---|---|---|
TERMINATION_LOG_PATH |
main.py |
the path Kubernetes sets, not chosen here |
HF_HUB_OFFLINE |
warmup.py |
the warm-up entrypoint's own contract |
HF_TOKEN |
warmup.py |
from hf_token_secret; read only to log it is set |
HF_HOME |
warmup.py |
the warm-up entrypoint's own contract |
PIPELINE_ID |
warmup.py |
the warm-up entrypoint's own contract |
PIPELINE_PATH |
warmup.py |
the warm-up entrypoint's own contract |
web — the read API and campaign browser
Each key is an environment variable of the container.
| Key | Set by | Default | Must agree with | Security |
|---|---|---|---|---|
HTRFLOW_RESULTS_URL |
the chart: resultsUrl |
required unless HTRFLOW_WEB_SITE_ONLY |
chart resultsUrl, converter results_url, wrapper RESULTS_URL |
the results URL — nobody |
HTRFLOW_INTERNAL_RESULTS_BASE |
the chart, fixed: http://htrflow-results:8082/results |
HTRFLOW_RESULTS_PROXY |
— | no secret — nobody |
HTRFLOW_RESULTS_PROXY |
the chart, fixed: http://htrflow-results:8082/results |
required unless HTRFLOW_WEB_SITE_ONLY |
— | no secret — nobody |
HTRFLOW_NAMESPACES |
the chart, fixed: the release namespace (no value sets it) | the pod's own namespace, else htr-batch |
— | no secret — nobody |
HTRFLOW_WEB_STATIC |
the image build (ENV): where the image puts the site |
/app/static |
— | no secret — nobody |
HTRFLOW_WEB_SITE_ONLY |
a local run only (the compose stack sets it); no chart value | false |
— | no secret — nobody |
HTRFLOW_BATCH_VERSION |
the image build (ENV): the tag the image is published under |
dev |
— | no secret — nobody |
results — the results proxy (htrflow-results)
Each key is an environment variable of the container.
| Key | Set by | Default | Must agree with | Security |
|---|---|---|---|---|
HTRFLOW_RESULTS_NAMESPACE |
the chart, fixed: the release namespace (downward API) | required | — | no secret — nobody |
S3_ENDPOINT |
the chart: the S3 Secret (s3.existingSecret), its S3_ENDPOINT key |
(empty) | — | from the S3 Secret (secretKeyRef) — cluster |
S3_BUCKET |
the chart: the S3 Secret (s3.existingSecret), its S3_BUCKET key |
required | — | from the S3 Secret (secretKeyRef) — cluster |
S3_VERIFY_TLS |
the chart: the S3 Secret (s3.existingSecret), its S3_VERIFY_TLS key |
true |
— | false skips the S3 certificate check, so the pod would send its S3 keys (a logged-in user's own, for the results proxy) to whatever answers at S3_ENDPOINT; set by whoever writes the S3 Secret — cluster |
HTRFLOW_SESSION_KEY_FILE |
the chart, fixed: /secrets/session/key, the key of the results.sessionSecret Secret |
/secrets/session/key |
— | the file holding the key that seals every login session; whoever reads it can forge a session — cluster |
HTRFLOW_SESSION_HOURS |
the chart: results.sessionHours |
8.0 |
— | no secret — nobody |
HTRFLOW_KEY_DERIVATION |
the chart: results.keyDerivation |
hcp |
— | how a login's password becomes S3 keys; a wrong value only makes every login fail — nobody |
HTRFLOW_TRUSTED_HOPS |
the chart, fixed: 2 on a ClusterIP Service with web.ingress.enabled or network.web.ingressFrom, else 1 |
1 |
— | no secret — nobody |
converter — a campaigns repo
Each key is a key of converter.yaml.
| Key | Set by | Default | Must agree with | Security |
|---|---|---|---|---|
namespace |
converter.yaml |
htr-batch |
— | no secret — nobody |
queue |
converter.yaml |
htr-batch |
chart queue.name |
no secret — nobody |
window |
converter.yaml; a campaign's own window: may lower it |
20 |
— | no secret — nobody |
s3_secret |
converter.yaml |
htr-batch-s3 |
chart s3.existingSecret |
names the Secret mounted at /secrets/s3; job-shape admits only s3.existingSecret — cluster |
data_pvc |
converter.yaml |
htr-test-data |
chart modelCache.name |
the model-cache PVC; job-shape admits only modelCache.name — cluster |
runtime_class |
converter.yaml |
nvidia |
— | no secret — nobody |
node_selector |
converter.yaml |
(empty) | — | no secret — nobody |
tolerations |
converter.yaml |
(empty) | — | no secret — nobody |
results_url |
converter.yaml |
required | chart resultsUrl, web HTRFLOW_RESULTS_URL, wrapper RESULTS_URL |
the results URL — nobody |
source_template |
converter.yaml |
(empty) | — | no secret — nobody |
max_seconds |
converter.yaml; a pipeline's own max_seconds: overrides it |
21600 |
— | no secret — nobody |
warmup_wait_seconds |
converter.yaml |
900 |
— | no secret — nobody |
ttl_seconds_after_finished |
converter.yaml; a pipeline's own ttl_seconds_after_finished: overrides it |
604800 |
— | no secret — nobody |
hf_token_secret |
converter.yaml |
(empty) | chart hfToken.existingSecret |
names the Secret the warm-up reads HF_TOKEN from; job-shape admits only hfToken.existingSecret — cluster |
manifest_max_bytes |
converter.yaml |
16777216 |
— | no secret — nobody |
fetch_max_bytes |
converter.yaml |
67108864 |
— | no secret — nobody |
image_cache |
converter.yaml |
(empty) | — | no secret — nobody |
priority_classes |
converter.yaml |
[htr-interactive, htr-bulk, htr-idle] |
chart queue.priorityClasses[].name |
no secret — nobody |
flavors |
converter.yaml |
(empty) | chart queue.flavors[].name, .nodeLabels |
no secret — nobody |
sizes |
converter.yaml |
(empty) | — | no secret — nobody |
default_size |
converter.yaml |
(empty) | — | no secret — nobody |
chart — charts/htrflow-batch
Each key is a key of its values.yaml.
| Key | Set by | Default | Must agree with | Security |
|---|---|---|---|---|
s3.existingSecret |
values.yaml |
htr-batch-s3 |
converter s3_secret |
names that Secret; no template creates it — nobody |
hfToken.existingSecret |
values.yaml |
(empty) | converter hf_token_secret |
the one Secret a warm-up may read (job-shape) — cluster |
resultsUrl |
values.yaml |
(empty) | converter results_url, web HTRFLOW_RESULTS_URL, wrapper RESULTS_URL |
the results URL; required — render |
modelCache.create |
values.yaml |
true |
— | no secret — nobody |
modelCache.name |
values.yaml |
htr-test-data |
converter data_pvc |
no secret — nobody |
modelCache.size |
values.yaml |
30Gi |
— | no secret — nobody |
modelCache.storageClass |
values.yaml |
(empty) | — | no secret — nobody |
modelCache.accessModes |
values.yaml |
[ReadWriteOnce] |
— | no secret — nobody |
queue.name |
values.yaml |
htr-batch |
converter queue |
no secret — nobody |
queue.flavor |
values.yaml |
default-flavor |
— | no secret — nobody |
queue.createFlavor |
values.yaml |
true |
— | no secret — nobody |
queue.clusterQueueName |
values.yaml |
(empty) | — | no secret — nobody |
queue.createClusterQueue |
values.yaml |
true |
— | no secret — nobody |
queue.resources |
values.yaml |
[{name: cpu, quota: 4}, {name: memory, quota: 8Gi}, {name: … |
— | no secret — nobody |
queue.flavors |
values.yaml |
(empty) | converter flavors |
no secret — nobody |
queue.createPriorityClasses |
values.yaml |
true |
— | no secret — nobody |
queue.priorityClasses |
values.yaml |
[{description: a handful of volumes someone is waiting for,… |
converter priority_classes |
no secret — nobody |
web.image |
values.yaml |
docker.io/riksarkivet/htrflow-web@sha256:57078aaace55336829… |
— | digest-pinned unless security.allowTagImages — render |
web.nodePort |
values.yaml |
30800 |
— | no secret — nobody |
web.resources.requests.cpu |
values.yaml |
50m |
— | no secret — nobody |
web.resources.requests.memory |
values.yaml |
128Mi |
— | no secret — nobody |
web.resources.limits.cpu |
values.yaml |
500m |
— | no secret — nobody |
web.resources.limits.memory |
values.yaml |
256Mi |
— | no secret — nobody |
web.service.type |
values.yaml |
NodePort |
— | no secret — nobody |
web.ingress.enabled |
values.yaml |
false |
— | no secret — nobody |
web.ingress.className |
values.yaml |
(empty) | — | no secret — nobody |
web.ingress.host |
values.yaml |
(empty) | — | no secret — nobody |
web.ingress.tlsSecretName |
values.yaml |
(empty) | — | no secret — nobody |
web.ingress.annotations |
values.yaml |
(empty) | — | no secret — nobody |
results.replicas |
values.yaml |
1 |
— | no secret — nobody |
results.sessionSecret |
values.yaml |
(empty) | — | names the Secret that seals login sessions; required — render |
results.sessionHours |
values.yaml |
8 |
— | no secret — nobody |
results.keyDerivation |
values.yaml |
hcp |
— | no secret — nobody |
results.resources.requests.cpu |
values.yaml |
50m |
— | no secret — nobody |
results.resources.requests.memory |
values.yaml |
128Mi |
— | no secret — nobody |
results.resources.limits.cpu |
values.yaml |
500m |
— | no secret — nobody |
results.resources.limits.memory |
values.yaml |
256Mi |
— | no secret — nobody |
apply.rbac.enabled |
values.yaml |
false |
— | no secret — nobody |
apply.gitCidrs |
values.yaml |
(empty) | — | no secret — nobody |
apply.gitPorts |
values.yaml |
[443] |
— | no secret — nobody |
security.allowedImageRepos |
values.yaml |
(empty) | — | enforced by a Kyverno ClusterPolicy — cluster |
security.jobImageRepos |
values.yaml |
(empty) | — | what a campaign or warm-up Job may run — cluster |
security.requireModelRevision |
values.yaml |
false |
— | enforced by a Kyverno ClusterPolicy — cluster |
security.policies.enabled |
values.yaml |
false |
— | enforced by a Kyverno ClusterPolicy — cluster |
security.policies.allowDisabled |
values.yaml |
false |
— | no admission policy at all — render |
security.psaEnforce |
values.yaml |
baseline |
— | Pod Security Admission label — cluster |
security.allowTagImages |
values.yaml |
false |
— | opens that digest gate — render |
security.verifyImages.enabled |
values.yaml |
false |
— | enforced by a Kyverno ClusterPolicy — cluster |
security.verifyImages.issuer |
values.yaml |
(empty) | — | enforced by a Kyverno ClusterPolicy — cluster |
security.verifyImages.subject |
values.yaml |
(empty) | — | enforced by a Kyverno ClusterPolicy — cluster |
security.verifyImages.imageReferences |
values.yaml |
(empty) | — | enforced by a Kyverno ClusterPolicy — cluster |
security.verifyImages.rekorUrl |
values.yaml |
https://rekor.sigstore.dev |
— | enforced by a Kyverno ClusterPolicy — cluster |
network.enabled |
values.yaml |
true |
— | no secret — nobody |
network.defaultDeny |
values.yaml |
true |
— | no secret — nobody |
network.iiifCidrs |
values.yaml |
(empty) | — | no secret — nobody |
network.s3Cidrs |
values.yaml |
(empty) | — | no secret — nobody |
network.s3InNamespace |
values.yaml |
true |
— | no secret — nobody |
network.s3Ports |
values.yaml |
[443] |
— | no secret — nobody |
network.clusterCidrs |
values.yaml |
[10.42.0.0/16, 10.43.0.0/16] |
— | no secret — nobody |
network.nodeCidrs |
values.yaml |
(empty) | — | no secret — nobody |
network.privateCidrs |
values.yaml |
[10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10] |
— | no secret — nobody |
network.apiServer.cidr |
values.yaml |
(empty) | — | no secret — nobody |
network.apiServer.cidrs |
values.yaml |
(empty) | — | no secret — nobody |
network.apiServer.port |
values.yaml |
6443 |
— | no secret — nobody |
network.web.ingressCidrs |
values.yaml |
[0.0.0.0/0] |
— | the read API's only gate on a NodePort — cluster |
network.web.allowPublicIngress |
values.yaml |
false |
— | no secret — nobody |
network.web.ingressFrom |
values.yaml |
(empty) | — | who may reach the read API behind an ingress; the controller's allow-list keeps browsers out — cluster |
frontend — the campaign browser's build
Each key is an environment variable of bun run build, baked into
the bundle: the published image builds with none set.
| Key | Default | What |
|---|---|---|
VITE_API_BASE |
/api/v1 |
the read API's base; /config.js overrides it at run time |
VITE_RESULTS_BASE |
(empty) | the results base; /config.js overrides it at run time |
VITE_RELOAD_MS |
60000 |
how often the campaign list is fetched again, in ms |
VITE_LIVE_MS |
15000 |
how often a live run log is fetched again, in ms |
One-sided keys
namespace is the release namespace (a helm -n argument, not a chart
value), and runtime_class has no chart key at all, so neither can be
checked mechanically. The Hub-token Secret, like the S3 one, is the
operator's own object: the chart names it only to allow it. The
chart's queue quotas, NetworkPolicy CIDRs and image settings have no
converter counterpart. Prose lives in
Chart Values, Campaign & Pipeline YAML and
Wrapper.