Domain model¶
This page describes the Site custom resource, the applications registry, and what the platform derives from the model. The lifecycle the model drives is in 09; the exporter output detail is in 13.
The Site resource¶
| Property | Value |
|---|---|
| Group / version / kind | operators.ecmwf.int / v2 / Site |
| Plural, singular, short names | sites, site, s and ss |
| Scope | Namespaced |
| Status subresource | v2 only |
| Schema source | helm/ecmwf-sites/crds/site-crd.yaml |
| Python model | core/src/ecmwf/sites/core/model/site.py |
Version v1 has been removed from the Custom Resource Definition (CRD). Version v2 is the only served
and stored version. kubectl get sites prints owner, space, name, retention
date, and the enabled flag as additional columns.
The CRD declares no required fields: it validates the shape (types, patterns, lengths, defaults) of what is present. Completeness is enforced by the Hub form and by the Python model, which supplies defaults for absent fields (see "Build paths").
spec fields¶
| Field | Meaning and rules |
|---|---|
spec.id |
Exactly 16 characters (minLength = maxLength = 16), CRD pattern ^[a-z]([-a-z0-9]*[a-z])?$: a lowercase letter, lowercase letters/digits/hyphens, a letter at the end. The Hub generator (generate_site_id) produces letters and digits only, always starting and ending with a letter, so no hyphen appears in generated ids. The id is the name of every derived object. |
spec.name |
Display name and last URL segment. 1–60 characters; CRD pattern allows URL unreserved and sub-delimiter characters plus %XX escapes. |
spec.owner |
Owning account. minLength: 1, no pattern (an email is valid). <owner>-<name> forms the account id. |
spec.type |
private (default) or public. |
spec.created_by, spec.updated_by |
Usernames for audit. |
spec.creation_date, spec.updated_date |
Plain strings in the CRD; the model parses them as YYYY-MM-DD dates (see the zero date below). |
spec.description |
Free text, max 255 characters. |
spec.configuration fields¶
| Field | Meaning and rules |
|---|---|
space |
First URL segment (/<space>/<site>/). Same character pattern and 1–60 length as spec.name. When the resource omits it, the model builder resolves it to the owner on every load. The Hub form applies the same 60-character limit, also to the owner when it becomes the space. |
enabled |
Serves content or not. CRD default true. false scales the site to zero and redirects traffic; see 09. |
quota |
Integer GiB. CRD default 1, bounds 1..5000 (15). The Hub form caps the choice at site.storage_max_quota (default 500). The persistent volume claim (PVC) requests {quota}Gi. |
retention_date |
YYYY-MM-DD string, CRD default empty; see the zero date below. Drives the retention job; see 15. |
applications |
List of application objects; see "Applications registry". |
headers.cors |
Object with enabled (default false) and extra_domains list. |
headers.no_cache |
Object with enabled (default false) and file_extensions list: expires -1 for all files, or only for the configured extensions. |
headers.no_robots |
Object with enabled (default false); adds X-Robots-Tag: noindex. |
settings.resource_limits |
String tier: normal (default), medium, or high. An invalid value is converted to normal by the model. See the tier table below. |
settings.error_handling |
Object with enabled and custom_errors (list of status=target strings, for example 404=/errors/not-found.html): custom error-page redirects (otherwise the proxy redirects to Hub error pages). |
settings.monitored |
Hub-admin-only flag, CRD default false. Raises pod priority to medium-priority and includes the site in Hub health reports (09). |
share |
administrator_users, administrator_groups, access_allowed_users, access_allowed_groups (lists, default empty) and master_token. |
share.master_token |
The site master token, stored base64-encoded in the CR; the model decodes it on read and encodes it on write. A change rotates the per-site Secret and restarts the site (spec.configuration.share is a restart-triggering prefix). |
The retention zero date¶
The model stores a date object, not the raw string. An empty
retention_date (and any absent audit date) becomes the sentinel
2000-01-01 (DEFAULT_DATE_ZERO). The retention job compares against this
date like any other, so a Site whose retention_date is empty through
direct API use is treated as long expired: it is notified, archived, and
finally deleted. The Hub form avoids this: a new site gets today + 365
days as the default, the field carries a "greater than today" validator,
and a malformed submission falls back to the stored value.
Identity, naming, and labels¶
- Site path:
/<space|owner>/<name>/; account id:<owner>-<name>(core/.../model/utils.py). - Labels on every derived object (
Labels.create_labels):sites.ecmwf.int/app=site,sites.ecmwf.int/id,sites.ecmwf.int/name,sites.ecmwf.int/owner,sites.ecmwf.int/space, andapp.kubernetes.io/name=site-<id>. The id label is the raw id. The name, owner, and space values pass throughdns_label_hash: a valid DNS-1123 label keeps its value, anything else is replaced by Base32hex(SHA-256), 52 lowercase characters. An empty value omits the label. A hashed owner (for example an email) is therefore not searchable withkubectl get sites -lsites.ecmwf.int/owner=...by plain text; the Hub encodes label queries so its searches work. - Annotation
sites.ecmwf.int/operator_bound=trueon every derived object; pods inherit it, and the operator's pod handlers act only on annotated pods (09). - The StatefulSet and Service select pods by
sites.ecmwf.int/idalone (get_match_labels: only stable values go into selectors).
Applications registry¶
Defined in core/src/ecmwf/sites/core/model/site_apps_registry.py with per
application app_config.py modules under
site/src/ecmwf/sites/website/applications/; each module registers a class
in the singleton SiteApplicationRegistry. The runtime contracts and
request flow are in site application
architecture.
Every class carries flags that drive selection:
| Flag | Meaning |
|---|---|
IS_INIT |
Renders as an init container; configured without a spec entry. Only site-initializer. |
IS_ESSENTIAL |
site-oauth2, site-proxy, site-api, site-initializer. The Hub-side set_applications re-adds them on every model update, dedupes by name, and sorts by name, so the configuration can never lose them. |
IS_BASIC |
Included in the default list a new site gets from the form: site-oauth2, site-proxy, site-api, site-admin, site-initializer. |
IS_OPTIONAL |
Shown as removable in the interface; get_non_essential_apps_names reports IS_OPTIONAL non-essential applications. |
Selection rule (ConfiguredSiteApplications): a normal application is
configured only when the site's applications list contains an entry with
its name and a non-empty image. An entry without an image does not
enable anything. The effective container image is the entry's image, or
the registry formula when the entry omits it.
| Key | Image | Port | Flags | Role |
|---|---|---|---|---|
site-oauth2 |
site-oauth2 |
4180 | essential, basic | Front door: OpenID Connect (OIDC) login for browser traffic; the only port behind the Service |
site-proxy |
site-proxy |
8080 | essential, basic | OpenResty: path routing, Lua auth chain, listing, headers |
site-api |
site-api |
10000 | essential, basic | Go file and operations API |
site-admin |
site-admin |
9000 | basic, optional | File Browser admin UI |
site-webdav |
site-webdav |
7000 | optional | WebDAV access (enabled by adding the application) |
site-s3 |
site-s3 |
6000 | optional | S3-compatible access (enabled by adding the application) |
site-custom |
site-custom |
8000 | — | User-supplied application; replaces the default file listing UI |
site-initializer |
site-initializer |
— | essential, basic, init | Per-site startup preparation (init container) |
site-mirror |
site-mirror |
— | essential when migration.enabled (IS_ESSENTIAL = sites_app.config.migration.enabled, evaluated at import) |
Migration container, normal container, no listener |
In v2, webdav, s3, and the admin UI have no boolean flags: they are
enabled by the presence of their entry in spec.configuration.applications.
The platform setting migration.enabled adds site-mirror to every site;
it is not a per-site application entry.
Each application entry may set:
| Key | CRD rule |
|---|---|
name |
Not declared in the schema; preserved because the applications array node sets x-kubernetes-preserve-unknown-fields. It must equal a registered class name — an unknown name selects nothing. |
image |
Pattern: registry/repository/name[:tag][@sha256:...] — a registry host, one or more lowercase repository segments, a lowercase name, an optional tag (mixed case allowed), optional SHA-256 digest. |
proxy_type |
fastcgi or proxy; selects how the proxy integrates a custom application. |
context_root |
root_context or site_context; decides whether the application sees / or the site path. |
extra_env |
List of {name, value} pairs; the value is stored base64-encoded and decoded into container environment variables. |
For a custom application, platform settings restrict registries
(site.custom_application_allowed_registries) and public-site custom images
(site.allow_public_custom_application, default false). The complete
container, validation, and upgrade contract is in
custom applications.
Site application images resolve to
{kubernetes.private_registry}/{kubernetes.private_registry_project}/{name}:{site.version}
(default eccr.ecmwf.int/sites-oss, tag from the chart release; see
01-overview). A non-empty image field in
spec.configuration.applications overrides that formula. On upgrade, only
the custom application's image is kept; every built-in application's image
is rewritten to the platform version.
Resource tiers¶
settings.resource_limits is a tier name, not a quantity. The base table
(each application module may override the numbers) is:
| Tier | Memory (request = limit) | CPU (request = limit) |
|---|---|---|
normal |
32 Mi | 100 m |
medium |
64 Mi | 100 m |
high |
128 Mi | 100 m |
The proxy, API, and admin modules define their own larger values per tier, and so does every other application module: no current container uses the base numbers. See the per-application tables in built-in Site applications, custom applications, and content mirror.
Derived objects¶
SiteExporter (core/src/ecmwf/sites/core/model/site_exporter.py) renders,
per site, all objects named after spec.id:
| Object | Contents |
|---|---|
Secret <id> |
MASTER_TOKEN (base64 of the decoded token) |
Secret <id>-oauth2 |
cookie-secret: the OAuth2 Proxy cookie secret of this site, derived from session.secret_key with HMAC-SHA256 and the site ID (derive_site_oauth2_cookie_secret). Only the site-oauth2 container reads it |
ConfigMap <id> |
Environment, 11 fixed keys only: OWNER, DEBUG, SITE_TYPE, SITE_PATH, LOG_LEVEL, RETENTION_DATE, OIDC_INTROSPECTION_URL, AUTHORIZED_ADMIN_USERS/GROUPS, AUTHORIZED_STAFF_USERS/GROUPS |
ConfigMap <id>-files |
All rendered files: the full OpenResty configuration set, fancyindex header/footer, retention banner, oauth2.cfg, alpha-oauth2.cfg, oauth2-error.html, admin logo/favicon. Built from site data, OIDC and branding settings, the decoded extra_env of every application, and each application's get_config_map_values. It holds no secret values: the session secret key and the OIDC client secret are not render data |
PVC <id> |
{quota}Gi, access mode and StorageClass from platform config; grows only behind kubernetes.storage_expansion_supported (13-DAT-09) |
StatefulSet <id> |
Containers from the registry, pod runAsUser/Group/fsGroup 65534, no service-account token, enableServiceLinks: false, NAMESPACE and KUBERNETES_* set to empty values in every container, priority class by monitored, replicas = site.replicas (0 when disabled) |
Service <id> |
The port of site-oauth2 (4180), the only application exposed cluster-wide; sessionAffinity: ClientIP (10800 s) |
Ingress or HTTPRoute <id> |
One route for the active routing mode; see 14 |
Build paths¶
SiteBuilder produces the same model from the Hub form, from the API
request, or from the resource itself (from_form / from_json /
from_crd), so UI, API, and operator agree on one schema:
from_crdflattensmetadata,spec, andstatusinto one dictionary, keeps theresourceVersionfor conflict-safe writes, and optionally rebuilds application images (upgrade=True, see the registry section).from_jsonapplies model defaults for absent fields:spacefalls back toowner,enableddefaults to false,quotato1, dates to the zero sentinel. A CR withoutspec.configuration.enabled: trueis therefore disabled as far as the model is concerned, even though the CRD default for the stored object istrue.from_formbuilds a new site enabled, with the owner as space unless a custom space is set, the default application list, and the retention default of today + 365 days.
status¶
The operator writes status from Pod conditions: for each pod it resolves
the first failed or last successful condition (lastTransitionTime) and
patches it onto the Site resource
(operator/src/ecmwf/sites/operator/site_handler.py,
core/src/ecmwf/sites/core/kubernetes/utils.py:resolve_pod_status). The
stored shape is SiteStatus: condition (status, last transition time),
all_steps_started, phase (Running, Pending, Disabled),
start_time, message, reason, and containers — a list of
not-ready containers with name, kind (waiting, terminated, or
running for a container that fails its readiness check),
reason, and message. are_all_pods_running() is true only when
all_steps_started and the phase is Running. The CRD types status as
an open object (x-kubernetes-preserve-unknown-fields), so the model, not
the schema, defines the shape. status is display state only;
reconciliation never reads it.
Behaviour requirements¶
| ID | Requirement | Implemented in | Verified by |
|---|---|---|---|
| DM-01 | The CRD declares no required fields and validates only the shape of what is present; completeness is enforced by the Hub form and by the model, which supplies defaults for absent fields. | helm/ecmwf-sites/crds/site-crd.yaml, core/.../model/site.py, core/form/forms.py |
core/tests/test_model.py (test_site_builder_fake, test_site_builder_from_json default assertions) |
| DM-02 | spec.id is a 16-character DNS-safe string and names every derived object. |
CRD pattern, core/.../model/utils.py, site_exporter.py |
core/tests/test_site_model_utils.py (test_site_id, test_build_path, test_build_account) |
| DM-03 | The quota must not exceed the CRD maximum (5000) or site.storage_max_quota (default 500); the PVC then requests {quota}Gi. |
CRD, core/.../config/schema/models.py, core/form/forms.py, site_exporter.py |
core/tests/test_helm_configuration.py (test_quota_schema_bounds_match_the_documented_limits), core/tests/test_add_edit_site_form.py (test_quota_is_capped_at_the_platform_storage_max_quota), core/tests/test_site_exporter_volume_claim.py (PVC size) |
| DM-04 | Init applications are always configured; normal applications are selected from effective application images, and site-oauth2, site-proxy, site-api, and site-initializer are essential. |
core/.../model/site_apps_registry.py |
site/tests/test_site_apps_registry.py (selection rules, essential set, entry without image) |
| DM-05 | A custom application image survives site upgrades; built-in images are rewritten to the platform version. | core/.../model/site.py (upgrade=True path) |
hub/tests/test_sites_upgrade.py |
| DM-06 | status reflects Pod conditions and never influences reconciliation. |
operator/.../site_handler.py |
— |
| DM-07 | Label values for name, owner, and space keep their text when they are valid DNS-1123 labels and are replaced by a 52-character Base32hex SHA-256 otherwise; an empty value omits the label. | core/.../config/utils.py (dns_label_hash), site.py:create_labels |
core/tests/test_dns_utils.py (encode-decision and hash cases) |
| DM-08 | An absent or empty date in the model becomes the zero date 2000-01-01. |
core/.../model/site.py (set_retention_date, set_creation_date) |
core/tests/test_model.py (zero-date assertions) |
| DM-09 | The Hub form defaults a new site's retention date to today + 365 days, rejects a past date, and falls back to the stored value on a malformed submission. | core/.../form/forms.py, form/utils.py (DateGreaterThan) |
core/tests/test_add_edit_site_form.py (default assertion), core/tests/test_form_fixes.py (test_malformed_retention_date_falls_back) |
| DM-10 | A Hub-side application update re-adds the essential applications and deduplicates the list by name. | core/.../model/site.py (set_applications), core/.../service/sites.py |
— |
| DM-11 | The master token is stored base64-encoded in the CR and decoded transparently by the model. | core/.../model/site.py (get/set_master_token), site_exporter.py (to_secret) |
— |