Content mirror design¶
site-mirror is a migration container. It copies content from the same Site
path on a source Sites platform into the local Site content directory
(/www/html on the mounted Site persistent volume claim (PVC)). It has no listener, Service port,
route, or source-side write operation.
Configuration¶
| Setting | Default | Behaviour |
|---|---|---|
migration.enabled |
false |
Adds the mirror to the essential application set (evaluated at import; see "Activation and rollout"). |
migration.source_domain |
empty | Source host without scheme, port, or path. Validated as a DNS subdomain (max 253 characters). |
migration.interval_seconds |
3600 |
Delay between cycles; minimum 1 (ge=1). |
migration.delete |
true |
Allows removal of local entries absent from the source. |
migration.delete_max_percent |
99 |
Stops deletion when the stale-entry percentage is above this value (range 0..100). |
migration.dry_run |
false |
Logs changes without changing the PVC. |
migration.verify_checksum |
false |
Adds SHA-256 checks for files that appear unchanged. |
migration.download_retries |
3 |
Retries after the initial failed download (minimum 0). |
migration.download_retry_delay_seconds |
5 |
Delay between download retries (minimum 0). |
Loading the configuration fails when migration.enabled is true and
migration.source_domain is empty (cross-field validator in
core/.../config/schema/models.py).
The container environment is derived from these settings:
MIRROR_SOURCE_URL combines routing.scheme and migration.source_domain;
the script appends the Site path and selects the source Site by --space
and --name parsed from SITE_PATH. The mirror uses the source Site master
token: MASTER_TOKEN arrives through the per-site Secret, and sitesctl
authenticates with it. Source and destination Sites therefore need matching
master-token access for the duration of migration.
Container and image¶
SiteMirrorApplication builds its container without the shared
_container() helper (see
site application architecture):
no ports, no probes, no PORT environment, and no oidc-secrets
value. It gets the masked variables (NAMESPACE and KUBERNETES_*, with
empty values), as every site container does. It sets its own security context (non-root, not privileged, no
privilege escalation, writable root filesystem), mounts the Site volume at
/www, and takes the per-site Secret <id> and environment ConfigMap <id>
through envFrom. When migration.enabled is true it receives every
MIRROR_* setting as direct env entries; the same values are also written
into the files ConfigMap <id>-files by get_config_map_values(), which the
mirror does not mount (a test asserts both copies agree). Tiers match the
custom application:
| Tier | Memory (request / limit) | CPU (request / limit) |
|---|---|---|
normal |
512 Mi / 1 Gi | 100 m / 1000 m |
medium |
1 Gi / 2 Gi | 200 m / 2000 m |
high |
2 Gi / 3 Gi | 300 m / 3000 m |
The image (site/mirror.Dockerfile) is Alpine running as UID/GID 65534 with
coreutils and jq. It bakes sitesctl in at build time: the build downloads
the fixed sites-linux-amd64 build SITESCTL_VERSION from the Sites CLI
repository (get.ecmwf.int), stops when its SHA-256 is not
SITESCTL_SHA256, and records the version in /etc/sitesctl-version. To
use a new CLI build, change both arguments together, and change the same
values in .github/workflows/publish.yml. The
build requires a TAG build argument, which the banner line reports with
the image build date. For container-level debugging, the script also reads
MIRROR_ONCE, MIRROR_LOCAL_DIR (default /www/html),
MIRROR_STATE_DIR (default <parent of the content dir>/.mirror), and
SITESCTL, and LOG_LEVEL=debug enables shell tracing. These are not
platform settings.
Activation and rollout¶
IS_ESSENTIAL = sites_app.config.migration.enabled is evaluated at import
time, so the Hub and the operator read the setting at startup. Enabling it
takes effect through this sequence:
- Set
migration.enabledandmigration.source_domain. - Restart the Hub and operator.
- Update each existing Site through the Hub form or a full API update
(
PUT/PATCH): that path rebuilds the application list withset_applications, which adds the essentialsite-mirrorentry, and the operator then renders the new StatefulSet container. Upgrades never add applications: Force Upgrade All Sites only rewrites the image of the entries already in the resource. Site creation does not add it either — the create path starts from the basic set. - Check the mirror logs for a completed cycle with
failed=0.
To reverse it, reverse the setting and restart the Hub and operator.
Sites whose resources already contain a site-mirror application entry keep
that entry: upgrades keep and rewrite application entries, so the container
keeps rendering and its script reports a configuration error without
mirroring (see MIR-07). Remove the site-mirror entry from those Site
resources to complete the removal.
Cycle algorithm¶
The script configures sitesctl once at startup (base URL, auto-upgrade
off, strict HTTP errors on), then loops: cycle, sleep MIRROR_INTERVAL.
flowchart TD
A["List source with sitesctl"] --> B["Compare source entries and local files"]
B --> C{"Entry changed?"}
C -- no --> D["Keep local file"]
C -- yes --> E["Download to .mirror work path"]
E --> F{"Download succeeded?"}
F -- no --> G["Keep old file and report failure"]
F -- yes --> H["Move into content path and restore mtime"]
B --> I["Create missing directories"]
B --> J{"Deletion enabled and safe?"}
J -- yes --> K["Delete stale files and empty directories"]
J -- no --> L["Keep stale files"]
Details, as implemented in mirror/bin/mirror.sh:
- Listing:
sitesctl content list --path / --recursive -o json. The source API follows symbolic links, so a link appears as a regular file or directory entry. Thejqfilter accepts the entry list with or without an envelope, and convertsmtimetimestamps with any UTC offset (Zor±HH:MM) and optional fractional seconds. - Change detection: a source file is (re)downloaded when it is new, when
its size differs from the local file, or when its modification time is
newer. Local files that are newer with an equal size are kept. With
MIRROR_VERIFY_CHECKSUM=true, entries that look up to date get a remotecontent checksumrequest; a differing 64-hex digest triggers a download, and a missing remote digest keeps the local file. - Download: into a temporary directory under the state directory; the
result must be exactly one file. The modification time is restored on the
downloaded file before
mv -fmoves it into the content path, so a partial file never appears in the content directory. Total attempts areMIRROR_DOWNLOAD_RETRIES + 1with the configured delay between them (delay 0 skips sleeping). An exhausted download logs the failure, keeps the old file, countsfailed, and the next cycle retries it — one failed file never stops the other files. - Directories: source directories missing locally are created, including
empty ones. A failed creation logs
ERRORand the cycle continues; it is not counted in thefailedtotal. - Deletion (only with
MIRROR_DELETE=true): stale files are local files not present in the source list. If the source has no files at all, no deletion happens. Ifstale * 100 / localexceedsMIRROR_DELETE_MAX_PERCENT, all deletion is skipped for the cycle. After file deletes, directories not known to the source and empty are removed withrmdir, deepest first. - Reporting: each cycle logs
cycle finished: downloaded=... skipped=... deleted=... failed=.... The cycle exit status is 0 only when nothing failed;MIRROR_ONCE=trueruns one cycle and exits with that status.
Safety behaviour¶
- An empty or unreachable source does not delete local content; an unreachable source skips the whole cycle.
- Deletion stops when the stale-entry percentage exceeds
migration.delete_max_percent. - Dry-run mode performs comparisons and logs
DRY-RUNactions only. - A failed download keeps the existing file and is retried in the next cycle.
- A failed file does not stop processing of other files.
- Source symbolic links arrive as regular files and directories (resolved by the source API).
- The mirror only performs read operations against the source
(
content list,content checksum,content download); it never writes to the source platform and does not copy Site resources. - A missing or invalid environment value never stops the container. The
script reports every problem to stderr, states that mirroring will not
work until the configuration is fixed, and repeats the report every
MIRROR_INTERVALseconds (or the 3600 s default when the interval itself is invalid) without mirroring.SIGTERM/SIGINTexit cleanly; a restart picks up the fixed values. WithMIRROR_ONCE=truethe same problems exit with status 1. - Operational failures outside environment validation (cannot create the
content or state directory, cannot configure
sitesctl) exit and rely on the container restart.
During migration, local content can be overwritten or deleted. Users should not edit destination content until the mirror has completed its final cycle.
Use cases¶
- Move Sites from a source platform to this one, with the same
spaceandnamepath on both. - Keep a local copy of a Site in step while the source is still in use.
- Prove a migration before it changes anything:
migration.dry_run: truelogs the planned downloads and deletes.
Example usage¶
The platform administrator sets the migration settings; the mirror then follows the source:
Each cycle mirrors one Site path. Wait for a complete cycle, then set
migration.enabled: false; the mirror stops and the Site serves the copied
content
(migration settings).
Cycle¶
flowchart LR
S["Source platform (sites.example.org)"] --> M["site-mirror (sitesctl)"]
M --> V[("Site PVC /www/html")]
M -- "deletes and failures" --> L["container log"]