Skip to content

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:

  1. Set migration.enabled and migration.source_domain.
  2. Restart the Hub and operator.
  3. Update each existing Site through the Hub form or a full API update (PUT/PATCH): that path rebuilds the application list with set_applications, which adds the essential site-mirror entry, 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.
  4. 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. The jq filter accepts the entry list with or without an envelope, and converts mtime timestamps with any UTC offset (Z or ±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 remote content checksum request; 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 -f moves it into the content path, so a partial file never appears in the content directory. Total attempts are MIRROR_DOWNLOAD_RETRIES + 1 with the configured delay between them (delay 0 skips sleeping). An exhausted download logs the failure, keeps the old file, counts failed, 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 ERROR and the cycle continues; it is not counted in the failed total.
  • 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. If stale * 100 / local exceeds MIRROR_DELETE_MAX_PERCENT, all deletion is skipped for the cycle. After file deletes, directories not known to the source and empty are removed with rmdir, deepest first.
  • Reporting: each cycle logs cycle finished: downloaded=... skipped=... deleted=... failed=.... The cycle exit status is 0 only when nothing failed; MIRROR_ONCE=true runs 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-RUN actions 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_INTERVAL seconds (or the 3600 s default when the interval itself is invalid) without mirroring. SIGTERM/SIGINT exit cleanly; a restart picks up the fixed values. With MIRROR_ONCE=true the 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 space and name path 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: true logs the planned downloads and deletes.

Example usage

The platform administrator sets the migration settings; the mirror then follows the source:

migration:
  enabled: true
  source_domain: sites.example.org
  interval_seconds: 3600
  dry_run: true

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"]