Skip to content

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), and packages/wrapper/tests/test_config.py fails if any field of these models, or any literal env read in the wrapper, is ever named like one. The exception is HF_TOKEN, which huggingface_hub reads from the environment: converter.yaml's hf_token_secret names a Secret whose token key the converter renders as HF_TOKEN into 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[/…] answers 401 without 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.ingressFrom names 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: create and patch on 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 namespaced Role; it grants no watch or delete and no write to a Job or a Pod, and test_chart_agreement.py holds the rendered Role to exactly that. With security.policies.enabled, a Kyverno rule also holds its ConfigMap writes to names of the form campaign-<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 HttpOnly cookie 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.