Skip to content

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, and app.kubernetes.io/name=site-<id>. The id label is the raw id. The name, owner, and space values pass through dns_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 with kubectl get sites -lsites.ecmwf.int/owner=... by plain text; the Hub encodes label queries so its searches work.
  • Annotation sites.ecmwf.int/operator_bound=true on 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/id alone (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_crd flattens metadata, spec, and status into one dictionary, keeps the resourceVersion for conflict-safe writes, and optionally rebuilds application images (upgrade=True, see the registry section).
  • from_json applies model defaults for absent fields: space falls back to owner, enabled defaults to false, quota to 1, dates to the zero sentinel. A CR without spec.configuration.enabled: true is therefore disabled as far as the model is concerned, even though the CRD default for the stored object is true.
  • from_form builds 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) —