Site application architecture¶
This page is the index for the applications that compose a Site pod. It defines the registry contract and the shared container contract: what each class must provide, and what the exporter renders from it.
The applications:
- built-in Site applications — sets, selection, Service behaviour, resource tiers, and build metadata
- site initializer
- site OAuth2
- site proxy
- site API
- site admin
- site WebDAV
- site S3
- custom applications
- content mirror
Path shortcuts: site/.../applications/ is
site/src/ecmwf/sites/website/applications/; Lua and nginx paths are relative
to site/.../applications/proxy/.
The model side of the registry — class flags, application selection from
spec.configuration.applications, image override, and upgrade behaviour — is
in domain model. This page covers
the deployment contract: containers, Service ports, and route paths.
Registry contract¶
Each application module under
site/src/ecmwf/sites/website/applications/<name>/app_config.py registers a
SiteApplicationConfiguration subclass in the singleton
SiteApplicationRegistry (core/src/ecmwf/sites/core/model/site_apps_registry.py).
Base-class attribute defaults matter: a new built-in application is a basic
application (IS_BASIC = True) unless it says otherwise.
| Attribute | Default | Meaning |
|---|---|---|
NAME |
— | Container name and application-entry key |
PORT |
— | Container listener port. The initializer and the mirror define none |
CONTAINER_IMAGE |
— | Built-in image: <registry>/<project>/<NAME>:<VERSION> |
VERSION |
site.version config |
Tag in the built-in image formula |
IMAGE_PULL_POLICY |
kubernetes.image_pull_policy config |
Default pull policy for every container |
IS_INIT |
False |
Render into the init-container list |
IS_ESSENTIAL |
False |
The normal Site path needs the application |
IS_BASIC |
True |
Include in the default application list for a new Site |
IS_OPTIONAL |
False |
Shown as removable in the interface |
RESOURCE_LIMIT |
base tiers (see 03) | Per tier MEMORY/CPU request (MIN) and limit (MAX) values; MAX: None means "no limit". Every current module overrides the base values; see built-in Site applications, custom applications, and content mirror |
Each application implements three abstract methods, and may override the rest:
| Method | Contract |
|---|---|
get_container() |
Return the Kubernetes container definition |
get_service_port() |
Return a V1ServicePort for the per-site Service, or None when the application must not be reachable cluster-wide |
get_ingress_path() |
Return a V1HTTPIngressPath for the public route, or None when the application gets no route path |
get_config_map_values(data) |
Receive the per-Site render data and return this application's own entries; the builder merges all applications' entries into the files ConfigMap <id>-files, never the environment ConfigMap <id> |
get_extra_env_var_list() |
Decode the application's extra_env entries into container environment variables |
is_to_expose_clusterwide() |
True when get_service_port() returns a port |
is_to_expose_worldwide() |
True when get_ingress_path() returns a path |
For each Site, ConfiguredSiteApplications aggregates the selected
instances:
get_init_containers()— containers of the init applications.get_containers()— containers of the selected normal applications.get_service_ports()— ports of the applications whereis_to_expose_clusterwide()is true.get_ingress_path()— the ingress path of the first application whereis_to_expose_worldwide()is true. Today onlysite-oauth2qualifies.
A new built-in application must register its class and provide the three
abstract methods. Display helpers (pretty_name, get_urls,
get_hub_view_url, to_dict) build on NAME, DESCRIPTION, and the
application path attribute.
Application matrix¶
| Application | Kind | Port | Own path (app.path) |
Service port | Route |
|---|---|---|---|---|---|
site-initializer |
Essential init container | — | Site path | — | — |
site-oauth2 |
Essential, basic | 4180 | /s/oauth2 |
yes | yes (Site path, owns the route) |
site-proxy |
Essential, basic | 8080 | Site path | — | — |
site-api |
Essential, basic | 10000 | <site>s/api/v2/ |
— | — |
site-admin |
Basic, optional | 9000 | <site>s/admin/ |
— | — |
site-webdav |
Optional | 7000 | <site>s/dav/ |
— | — |
site-s3 |
Optional | 6000 | <site>s/s3/ |
— | — |
site-custom |
Site-supplied | 8000 | Site path | — | — |
site-mirror |
Migration container (essential while migration.enabled) |
— | Site path | — | — |
The "own path" column is the path attribute each application class sets.
For site-api, site-admin, site-webdav, and site-s3 it is the location
the proxy serves under the Site URL (<site> is the Site path); those
applications are reached through the proxy, not through a route or Service
path of their own. Only site-oauth2 provides a route path, which is the
Site path itself. The proxy location configuration is in
Site proxy.
Shared container contract¶
SiteExporter renders one StatefulSet per Site. The Pod carries three
volumes: site-volume (the Site persistent volume claim (PVC), read-write), site-config (the files
ConfigMap <id>-files), and oidc-client-secret (only the
OIDC_CLIENT_SECRET key of oidc-secrets, as the file client-secret,
mode 0440). Seven applications build their container through the
shared _container() helper in
core/src/ecmwf/sites/core/model/site_app_config.py. What it sets:
- Configuration sources (
envFrom): the per-site Secret<id>and the per-site environment ConfigMap<id>. No container gets the platform Secretoidc-secretsthroughenvFrom. The chart createsoidc-secretsfromsecrets.oidcSecretsvalues (platform configuration). - Environment: the decoded
extra_enventries, thenPORT(the container port), then the variables that the application gives in theextra_envparameter of_container(), then the masked variables. Onlysite-proxygives such variables:OIDC_CLIENT_IDandOIDC_CLIENT_SECRET, each asecretKeyRefto one key ofoidc-secrets, andOIDC_EXTRA_AUDIENCES, a plain value fromoidc.extra_audiences(not a Secret). The Lua code of the proxy reads them. Because they come after the applicationextra_enventries, a site value with the same name does not replace them (site/tests/test_oidc_secret_scope.py). The masked variables (SITE_MASKED_ENVIRONMENT_NAMES,core/.../config/constants.py) areNAMESPACEand theKUBERNETES_*variables of thekubernetesService, each with an empty value. Kubelet always injects theKUBERNETES_*variables, also withenableServiceLinks: false. A pod cannot remove them, but a container variable with the same name replaces them. The masked variables come afterextra_env, so that anextra_enventry cannot set them. The initializer and the mirror build their own containers, and they also get the masked variables. - Volume mounts:
site-volumeat/www, plus each application's ownsite-configsubPathmounts (for example the proxy mountsadmin.conf; the OAuth2 application mounts its two configuration files and the error page). - OAuth2 Proxy secrets: only
site-oauth2mountsoidc-client-secret, read-only, at/opt/sites/sites/site/oauth2/secrets(clientSecretFileinalpha-oauth2.cfg). It also getsOAUTH2_PROXY_COOKIE_SECRETfrom the keycookie-secretof the Secret<id>-oauth2, as its last environment entry, so that anextra_enventry cannot replace it. The custom application gets neither. - Security context:
allowPrivilegeEscalation=false,runAsNonRoot=true,privileged=false. The helper acceptsread_only_root_filesystem(defaultfalse); no built-in application enables it. The Pod runs as user/group 65534 and mounts no ServiceAccount token (03). - Resources: memory limit always set, CPU request always set, CPU limit
only when the tier value sets it explicitly. This follows the
_limit_rangebest practice: CPU is request-only unless a limit is named. - Image pull policy: the class default
IMAGE_PULL_POLICYfromkubernetes.image_pull_policy.site-customoverrides it toAlways, because user images are mutable tags.
Liveness probes only: no application defines a readiness or startup probe.
| Application | Liveness probe | Timing (initial delay / period / failure threshold) |
|---|---|---|
site-oauth2 |
HTTP GET /ping on its port |
10 s / 60 s / 3 |
site-proxy |
TCP socket on its port | Kubernetes defaults |
site-api, site-admin, site-webdav, site-s3 |
TCP socket on their port | 10 s / 60 s / 3 |
site-custom |
TCP socket on its port | 30 s / 60 s / 10 (tolerant of slow user applications) |
The four Go applications (site-oauth2, site-api, site-admin,
site-webdav) also set GOGC=50 and GOMEMLIMIT to 90 % of the effective
tier memory maximum, so the garbage collector stops before the container
hits its cgroup limit.
Two applications bypass _container() and build the container directly:
site-initializer: environment sources are the Secret<id>and the ConfigMap<id>— no OpenID Connect (OIDC) Secret, no ports, no environment variables, no probes, and no security context (the Pod-level settings still apply). It mounts the Site volume.site-mirror: a normal container with no listener. It sets its own security context (the same three flags, root filesystem writable), mounts the Site volume, takes the Secret<id>and ConfigMap<id>throughenvFrom, and receives itsMIRROR_*settings as directenventries rendered from the platform migration settings (empty whenmigration.enabledis false). The same values are also added to the files ConfigMap byget_config_map_values; the mirror test asserts both copies agree. See content mirror.
flowchart LR
R["Ingress or HTTPRoute"] --> O["site-oauth2"]
O --> P["site-proxy"]
P --> A["site-api"]
P --> B["optional or custom backend"]
I["init containers"] --> V[("Site PVC /www")]
P --> V
The OAuth2 Proxy upstream is the proxy on 127.0.0.1:8080, rendered into
alpha-oauth2.cfg. Browser traffic therefore always passes authentication
(site-oauth2), then the proxy location and auth chain
(site-proxy), before it reaches a backend application.
Image and selection rules¶
Built-in images use:
An image in spec.configuration.applications overrides the built-in image.
A normal application is selected only when its entry supplies both a name
and a non-empty image (03).
An upgrade rewrites built-in image versions. It preserves the image of
site-custom because the platform cannot determine its compatible version.
The application list is part of the Site resource. Adding or removing an application is therefore a Site update and follows the operator update flow in site lifecycle and reconciliation.
Behaviour requirements¶
| ID | Requirement | Implemented in | Verified by |
|---|---|---|---|
| APP-01 | The registry creates init applications separately from normal containers and selects normal containers only when their effective image is available. | core/.../model/site_apps_registry.py |
site/tests/test_site_apps_registry.py (test_only_the_initializer_is_an_init_application, test_init_applications_are_configured_without_a_spec_entry, test_application_entry_with_image_selects_the_application, test_application_entry_without_image_does_not_select_the_application) |
| APP-02 | Browser traffic enters through OAuth2 Proxy and the Site proxy before it reaches a backend application. | site/.../applications/oauth2/oauth2-proxy/alpha-oauth2.cfg (upstream 127.0.0.1:8080) |
site/tests/test_oauth_app_config.py (test_get_config_map_values), site/tests/test_proxy_lua_auth_chain.py |
| APP-03 | The mirror has no listener, writes only to the local Site volume, and receives the source URL, interval, and deletion settings from the platform migration configuration. | site/.../applications/mirror/app_config.py |
site/tests/test_mirror_app_config.py (test_container_has_no_listener_or_probe, test_container_mounts_site_volume_read_write, test_container_receives_mirror_environment_when_enabled, test_container_has_no_mirror_environment_when_disabled) |
| APP-04 | Mirror downloads use a work directory and preserve the existing file when a download fails. | site/.../applications/mirror/bin/mirror.sh |
site/tests/test_mirror_script.py |
| APP-05 | Mirror deletion is disabled for an empty source or when the stale-file safety limit is exceeded. | site/.../applications/mirror/bin/mirror.sh |
site/tests/test_mirror_script.py |
| APP-06 | A custom application image must come from an allowed registry (prefix match; an empty allow-list permits any registry), and a public Site may carry a custom application only when site.allow_public_custom_application is true. |
core/.../form/utils.py (is_image_from_allowed_registry), core/.../form/forms.py |
core/tests/test_add_edit_site_form.py (test_custom_application_validation, test_custom_application_any_registry_by_default, test_unchanged_custom_image_skips_registry_check, test_custom_application_on_public_site) |
| APP-07 | Every normal Site container receives the shared volume, the required configuration sources, and a non-privileged security context; the port and namespace environment is added except for site-mirror and site-initializer. |
core/.../model/site_app_config.py (_container), core/.../model/site_exporter.py |
site/tests/test_mirror_app_config.py (test_container_reads_site_secret_and_config_map, test_container_runs_as_non_root) — the shared _container() builder itself has no direct test |
| APP-08 | Only site-oauth2 returns a Service port and a route path; every other application returns None for both, so the per-site Service has a single port and the route has a single backend. |
site/.../applications/*/app_config.py, core/.../model/site_apps_registry.py (get_service_ports, get_ingress_path) |
site/tests/test_site_apps_registry.py (test_only_oauth2_has_a_service_port_and_a_route_path, for every registered application), site/tests/test_oauth_app_config.py (test_get_service_port, test_get_ingress_path) |
| APP-09 | Application data reaches containers as files in the per-site files ConfigMap (<id>-files); the environment ConfigMap (<id>) carries only the fixed site keys. |
core/.../model/site_exporter.py (to_config_map, to_files_config_map) |
site/tests/test_oauth_app_config.py (test_get_config_map_values), site/tests/test_proxy_app_config.py (test_get_config_map_values), site/tests/test_mirror_app_config.py (test_environment_and_config_map_values_agree) |
| BIA-01 | The initializer runs before normal Site containers and has no Service port or route. | site/.../applications/initializer/app_config.py, core/.../model/site_exporter.py |
site/tests/test_site_apps_registry.py (test_only_the_initializer_is_an_init_application; test_only_oauth2_has_a_service_port_and_a_route_path: no Service port or route; the start order itself is not asserted) |
| BIA-02 | OAuth2 Proxy receives browser traffic, uses the rendered Site configuration, and forwards authenticated requests to the Site proxy. | site/.../applications/oauth2/app_config.py |
site/tests/test_oauth_app_config.py |
| BIA-03 | The admin, WebDAV, and S3 applications are selected from the Site application list and are reached through proxy-owned paths. | site/.../applications/admin/, webdav/, s3/, proxy/ |
site/tests/test_admin_app_config.py, site/tests/test_webdav_app_config.py, site/tests/test_s3_app_config.py |
| BIA-04 | The Site API is reachable through the loopback network and exposes no independent external route. | site/.../applications/api/app_config.py, site/.../applications/proxy/nginx/api.conf, site/.../applications/proxy/lua/, core/.../model/site_exporter.py |
site/tests/test_api_app_config.py (test_container_uses_port_10000, test_proxy_reaches_the_api_on_its_port) |
| BIA-05 | Every Site keeps the essential set (site-initializer, site-oauth2, site-api, site-proxy); the application list re-adds it on every change, and site-admin is the only removable default application. |
core/.../model/site.py (set_applications), core/.../model/site_apps_registry.py |
site/tests/test_site_apps_registry.py |
| BIA-06 | A normal application creates a container only when the Site specification lists it with an image. | core/.../model/site_apps_registry.py (image_available()) |
site/tests/test_site_apps_registry.py |
| BIA-07 | The proxy renders the admin, API, WebDAV, and S3 locations only when the application is configured for the Site. | site/.../applications/proxy/app_config.py (is_application_configured filter), proxy/nginx/*.conf |
— |
| BIA-08 | WebDAV BasicAuth accepts the master token (Hex64) or a Branca token only; on public Sites, read-only methods on directories without .htpasswd run as anonymous. |
proxy/lua/basic.lua |
site/tests/test_proxy_lua_basic.py (anonymous fallback and Hex64 path; the Branca branch is not asserted) |
| BIA-09 | The S3 container receives no authentication material: the proxy authenticates the X-Amz-Security-Token header and clears it and Authorization before forwarding. |
proxy/nginx/s3.conf, proxy/lua/s3_auth.lua |
— |
| BIA-10 | The Site Service exposes exactly one port, site-oauth2 on 4180, with sessionAffinity: ClientIP. |
core/.../model/site_exporter.py (to_service), per-app get_service_port() |
site/tests/test_oauth_app_config.py (port and route present), site/tests/test_site_apps_registry.py (test_only_oauth2_has_a_service_port_and_a_route_path: absent for all other applications); the Service object itself is not asserted |
| BIA-11 | Every Site image sets IMAGE_TAG from the mandatory TAG build argument; all images except the Go API also record /etc/build-date. |
site/*.Dockerfile |
site/tests/test_dockerfile_licences.py, utilities/tests/dockerfile-policy.test.sh |
| PRX-01 | The proxy renders NGINX and listing-template files from the Site and platform configuration. | site/.../applications/proxy/app_config.py, proxy/nginx/, proxy/html/ |
site/tests/test_proxy_app_config.py |
| PRX-02 | Invalid values that would become NGINX settings are removed before configuration is rendered. | proxy/app_config.py (remove_unsafe_nginx_settings) |
site/tests/test_proxy_app_config.py |
| PRX-03 | Public and private Sites use different request maps for .htpasswd, signed URLs, session URLs, and anonymous methods. |
proxy/nginx/, proxy/lua/ |
Lua tests (public_auth.lua itself is not covered) |
| PRX-04 | API, admin, WebDAV, S3, health, and metrics requests use separate locations and authentication handlers. | proxy/nginx/*.conf, proxy/lua/*.lua |
site/tests/test_proxy_app_config.py, Lua tests (per-location wiring is not asserted) |
| PRX-05 | The default content backend serves /www/html, while custom applications select proxy or FastCGI integration. |
proxy/nginx/location.conf, proxy/nginx/*-app.conf |
— |
| PRX-06 | The health location allows the pod loopback address and trusted_ips; the metrics location allows trusted_ips only, evaluated against the real IP; an empty list denies all metrics requests. |
proxy/nginx/health.conf, metrics.conf |
site/tests/test_proxy_app_config.py (health render) |
| PRX-07 | CORS, Content Security Policy (CSP), cache, robots, custom error, and retention settings change only the generated locations that use them. | proxy/nginx/*.conf, proxy/app_config.py |
site/tests/test_proxy_app_config.py (CORS/CSP and unsafe settings; the X-Robots-Tag effect is not asserted) |
| PRX-08 | The listing UI adds administrator-only Edit Mode, dropzone uploads to the Site API, and in-browser signed and session URL generation. | proxy/html/js/, proxy/html/template/ |
— |
| PRX-09 | Authentication failures answer as JSON on the /s/api/vN, /s/_m2m, and /s/s3 paths, and with the HTML or redirect treatment elsewhere. |
proxy/nginx/nginx.conf (map $want_json), proxy/lua/auth_utils.lua |
— |
| PRX-10 | Content authentication paths always set X-Site-Retention-Date-Expired; an expired retention date injects the branded banner via sub_filter; a missing banner or an invalid RETENTION_DATE logs and degrades without injecting. |
proxy/lua/retention_date_notification.lua, proxy/nginx/server.conf, proxy/nginx/location.conf |
site/tests/test_proxy_lua_retention_date.py |
| PRX-11 | When the metrics integration is active, the proxy collects counters in the log phase and serves /s/metrics from the shared dictionary; no metrics code is rendered when it is inactive. |
proxy/lua/metrics.lua, proxy/nginx/nginx.conf, server.conf, metrics.conf |
site/tests/test_proxy_app_config.py (render wiring on and off; metrics.lua logic is not covered, see 21) |
| CUS-01 | The site-custom container exists only when the Site configuration carries a site-custom entry with a non-empty image; the form creates the entry only when an image is set. |
core/.../model/site_apps_registry.py, core/.../model/site.py (SiteApplicationsBuilder.from_form) |
site/tests/test_site_apps_registry.py (test_application_entry_with_image_selects_the_application, test_application_entry_without_image_does_not_select_the_application) — the image-selection rule is exercised with site-s3; core/tests/test_site_applications_validation.py (TestSiteApplicationFromFormEntry) covers the form-side entry creation |
| CUS-02 | Custom image, proxy type, context root, and environment values use the Custom Resource Definition (CRD) and Hub validation rules; the form uses the same image regex as the CRD. | helm/.../crds/site-crd.yaml, core/.../form/forms.py, core/.../config/constants.py (IMAGES_REGEX) |
core/tests/test_add_edit_site_form.py (custom application validation), hub/tests/test_api_request_validation.py (empty fields), hub/tests/test_validation_constants.py (the browser constants equal IMAGES_REGEX, SITE_CUSTOM_ERROR_REGEX, and the extra_env name rule), core/tests/test_site_applications_validation.py (the model rejects an invalid enum) |
| CUS-03 | New or changed custom images follow the configured registry policy and public-site policy; an unchanged image on an already-public Site reverts to private, and the Hub API holds that revert until the caller confirms it. | core/.../form/forms.py, core/.../form/utils.py (is_image_from_allowed_registry), hub/.../app/api/common.py (edit_site) |
core/tests/test_add_edit_site_form.py (test_custom_application_validation, test_custom_application_any_registry_by_default, test_unchanged_custom_image_skips_registry_check, test_custom_application_on_public_site), hub/tests/test_edit_site_reverted.py (hold, confirm, raw-client behaviour) |
| CUS-04 | The custom container uses port 8000, TCP liveness and readiness probes, the Site volume, the Site configuration sources, the Always pull policy, and no OIDC Secret. |
site/.../applications/custom/app_config.py, core/.../model/site_app_config.py (_container) |
site/tests/test_custom_app_config.py (test_container_port_and_probe_and_pull_policy, test_container_has_a_tcp_readiness_probe_on_the_service_port, test_container_uses_the_site_configuration_sources_without_the_oidc_secret, test_container_mounts_the_site_volume, test_container_passes_the_custom_environment_variables) |
| CUS-05 | The proxy includes proxy-app.conf or fastcgi-app.conf according to proxy_type, defaults to the static default-app.conf without a custom image, and rewrites the upstream path only in proxy mode with site_context. |
proxy/nginx/location.conf, proxy-app.conf, fastcgi-app.conf, fastcgi.conf, default-app.conf |
— |
| CUS-06 | Site upgrades preserve the custom image while rewriting built-in application images. | core/.../model/site.py (from_json upgrade path) |
hub/tests/test_sites_upgrade.py (test_force_same_version_preserves_spec_and_changes_request_each_time) |
| CUS-07 | An environment entry needs a shell-style name, must not contain a comma in its value, and must not use a name the platform injects; the form lists every reserved name it finds. | core/.../config/utils.py (parse_extra_env, invalid_extra_env_entries), core/.../form/utils.py (ExtraEnvEntriesValid), core/.../model/site_exporter.py (SITE_PLATFORM_ENVIRONMENT_NAMES) |
core/tests/test_add_edit_site_form.py (test_custom_application_extra_env_*), core/tests/test_site_applications_validation.py (TestSitePlatformEnvironmentNames) |
| CUS-08 | The Hub sends no request to the registry of a custom application image; an image that cannot be pulled shows on the Settings tab status line after the restart. | hub/.../app/hub/custom_application.py (custom_application_status), hub/.../templates/hub/view-tab-settings.html |
hub/tests/test_custom_application_status.py |
| CUS-09 | The Settings tab reports the custom application restart state from the Site system status and lets an administrator restart the Site from the error line. |
hub/.../app/hub/custom_application.py (custom_application_status), hub/.../templates/hub/view-tab-settings.html |
hub/tests/test_custom_application_status.py |
| CUS-10 | The Settings tab page carries the environment variable names only; the dialog reads the values from an endpoint that requires Site administrator access, and leaves the field out of the submission until that read succeeds. | hub/.../template_utils.py, hub/.../app/api/common.py (custom_application_environment), hub/.../static/js/list-editor.js |
hub/tests/test_custom_application_environment.py |
| CUS-11 | The chart rejects an invalid custom-application configuration before install or upgrade. The registry list is the only control over where custom images come from, and kubernetes.private_registry does not restrict them. |
helm/ecmwf-sites/templates/_helpers.tpl (sites.customApplicationSettings.validate) |
utilities/tests/helm-custom-applications.test.sh |
| CUS-12 | The status line treats a waiting ContainerCreating or PodInitializing as preparing, gives a pull, crash, or out-of-memory problem a short label and a one-sentence advice, keeps an unknown reason as the raw error, and never shows the container text unescaped. |
hub/.../app/hub/custom_application.py (classify_custom_application_container), hub/.../templates/hub/view-tab-settings.html |
hub/tests/test_custom_application_status.py (TestCustomApplicationContainerClassification, the label and tooltip template tests) |
| CUS-13 | The site-custom container carries a TCP readiness probe on port 8000 (15 s initial delay, 30 s period, failure threshold 3). The readiness state only reports acceptance through the Pod condition: the site Service publishes not-ready addresses, so it never removes the pod from routing. |
site/.../applications/custom/app_config.py, core/.../model/site_exporter.py (to_service) |
site/tests/test_custom_app_config.py (test_container_has_a_tcp_readiness_probe_on_the_service_port), core/tests/test_site_exporter_service.py |
| CUS-14 | The Add control of a row-list editor creates a row only when every existing row holds a value in each of its required fields (an environment variable value is optional); otherwise it focuses the first required field left empty and flashes the reason in the hint, keeps one pending hint restore per editor, and never changes whether the rows may be submitted. | hub/.../static/js/list-editor.js (firstBlankSegmentIndex, firstIncompleteRow, the data-list-role="add" handler) |
hub/tests/test_list_editor.py (test_add_creates_a_row_only_when_the_others_are_complete, test_add_accepts_an_empty_environment_variable_value) |
| CUS-15 | A running site-custom container that fails its readiness check is recorded as NotReady in the Site status, and the status line keeps preparing with the label waiting for the application, an advice that says this is normal at start-up and lists the possible causes when it continues, and the Restart now link. A Site update deletes the pod that is not ready, so removing or changing the image does not leave the Site stuck (LIFE-15). |
core/.../kubernetes/utils.py (resolve_pod_status), hub/.../app/hub/custom_application.py (classify_custom_application_container), hub/.../templates/hub/view-tab-settings.html, operator/.../site_update.py (replace_not_ready_pods) |
core/tests/test_kubernetes_utils.py (test_running_not_ready_container_is_reported, test_ready_or_unknown_running_containers_add_no_entry), hub/tests/test_custom_application_status.py (TestCustomApplicationContainerClassification, the pending-label template test, test_not_ready_status_line_offers_restart), operator/tests/test_site_update.py::TestReplaceNotReadyPods |
| MIR-01 | The mirror is added only when migration is enabled and has no listener, probe, Service port, or route. | mirror/app_config.py, core/.../model/site_apps_registry.py |
site/tests/test_mirror_app_config.py (test_is_not_enabled_by_default, test_container_has_no_listener_or_probe) |
| MIR-02 | The source URL combines the platform scheme and the configured source domain; sitesctl is configured with it and with the Site master token. |
mirror/app_config.py (get_environment), mirror/bin/mirror.sh |
site/tests/test_mirror_script.py (test_sets_the_source_url), site/tests/test_mirror_app_config.py (test_container_receives_mirror_environment_when_enabled) |
| MIR-03 | Changed files are downloaded to a work directory and replace the existing file only after a successful download, with the source modification time restored. | mirror/bin/mirror.sh (download_file) |
site/tests/test_mirror_script.py (test_no_partial_file_is_left_in_the_local_directory, test_retry_exhaustion_keeps_old_file_and_continues, test_local_mtime_is_set_to_source_mtime) |
| MIR-04 | Retry, delay, dry-run, checksum, and deletion settings control the cycle behaviour. | mirror/bin/mirror.sh, mirror/app_config.py |
site/tests/test_mirror_script.py (test_download_retry_succeeds_on_third_attempt, test_zero_retries_make_one_attempt, test_zero_delay_does_not_sleep, test_dry_run_changes_nothing, test_verify_checksum_downloads_a_different_file, test_verify_checksum_skips_a_matching_file, test_delete_false_keeps_stale_files) |
| MIR-05 | An empty source, unreachable source, failed download, or unsafe stale-file percentage does not remove local content. | mirror/bin/mirror.sh (cycle) |
site/tests/test_mirror_script.py (test_unavailable_source_skips_the_cycle, test_delete_is_skipped_when_source_is_empty, test_delete_is_skipped_above_the_percent_limit, test_failed_download_is_retried_in_the_next_cycle) |
| MIR-06 | The operator renders the migration settings into the mirror container environment; the files ConfigMap copy agrees with it; both are absent when migration is disabled. | mirror/app_config.py (get_environment, get_config_map_values) |
site/tests/test_mirror_app_config.py (test_container_receives_mirror_environment_when_enabled, test_container_has_no_mirror_environment_when_disabled, test_environment_and_config_map_values_agree) |
| MIR-07 | A missing or invalid configuration setting is reported to stderr and never stops the mirror container; it keeps reporting until the configuration is fixed. | mirror/bin/mirror.sh (configuration-error loop) |
site/tests/test_mirror_script.py (test_invalid_environment_is_rejected, test_missing_environment_is_rejected, test_invalid_configuration_keeps_running_and_reports) |
| MIR-08 | The mirror performs only read operations against the source platform. | mirror/bin/mirror.sh (content list, content checksum, content download only) |
site/tests/test_mirror_script.py (test_script_only_reads_from_the_source) |
| MIR-09 | Change detection compares type, size, and modification time; source paths with spaces, UTC offsets, and fractional seconds parse correctly; symlinked source content arrives as regular files and directories. | mirror/bin/mirror.sh (source_list, comparison awk) |
site/tests/test_mirror_script.py (test_second_cycle_skips_up_to_date_files, test_download_when_size_differs, test_download_when_source_is_newer, test_no_download_when_local_is_newer_with_same_size, test_names_with_spaces, test_modification_time_with_any_utc_offset, test_fractional_seconds_in_modification_time, test_symlinked_source_content_is_copied_as_regular_files) |