From d7b278fe14d965ade50dc553a5c9e2174d33ceb1 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:26:03 +0000 Subject: [PATCH 1/4] Update helm-values-schema --- .github/workflows/_tox.yml | 2 +- .pre-commit-config.yaml | 2 +- Dockerfile | 2 +- helm/blueapi/README.md | 1 - helm/blueapi/config_schema.json | 98 +-- helm/blueapi/values.schema.json | 987 +++++++++++++++-------------- helm/blueapi/values.yaml | 3 - src/blueapi/cli/cli.py | 6 +- src/blueapi/utils/serialization.py | 23 + tests/unit_tests/test_config.py | 4 +- 10 files changed, 597 insertions(+), 531 deletions(-) diff --git a/.github/workflows/_tox.yml b/.github/workflows/_tox.yml index 6871567489..fc13707f87 100644 --- a/.github/workflows/_tox.yml +++ b/.github/workflows/_tox.yml @@ -18,7 +18,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Install helm plugins - run: helm plugin install https://github.com/losisin/helm-values-schema-json.git --version 2.2.1 + run: helm plugin install https://github.com/losisin/helm-values-schema-json.git --version 2.3.1 - name: Run tox run: uv run --locked tox -e ${{ inputs.tox }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33ac34a25a..320976f4a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: - --chart-search-root=helm - repo: https://github.com/losisin/helm-values-schema-json - rev: v2.2.1 + rev: v2.3.1 hooks: - id: helm-schema args: diff --git a/Dockerfile b/Dockerfile index a98eaf98e4..44d7bdd002 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/s chmod 700 get_helm.sh; \ ./get_helm.sh; \ rm get_helm.sh -RUN helm plugin install https://github.com/losisin/helm-values-schema-json.git --version 2.2.1 +RUN helm plugin install https://github.com/losisin/helm-values-schema-json.git --version 2.3.1 # The build stage installs the context into the venv FROM developer AS build diff --git a/helm/blueapi/README.md b/helm/blueapi/README.md index 227d264a51..d63a5d4ae0 100644 --- a/helm/blueapi/README.md +++ b/helm/blueapi/README.md @@ -14,7 +14,6 @@ A Helm chart deploying a worker pod that runs Bluesky plans | debug.suspend | bool | `false` | If enabled does not start the service on startup This allows connecting to the pod and starting the service manually to allow debugging on the cluster | | extraEnvVars | list | `[]` | Additional envVars to mount to the pod | | fullnameOverride | string | `""` | | -| global | object | `{}` | Not used, but must be present for validation when using as a dependency of another chart | | hostNetwork | bool | `false` | May be needed for EPICS depending on gateway configuration | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"ghcr.io/diamondlightsource/blueapi"` | To use a container image that extends the blueapi one, set it here | diff --git a/helm/blueapi/config_schema.json b/helm/blueapi/config_schema.json index 7104b9050a..d54bc5357b 100644 --- a/helm/blueapi/config_schema.json +++ b/helm/blueapi/config_schema.json @@ -20,7 +20,8 @@ "password" ], "title": "BasicAuthentication", - "type": "object" + "type": "object", + "$id": "BasicAuthentication" }, "CORSConfig": { "additionalProperties": false, @@ -62,7 +63,8 @@ "origins" ], "title": "CORSConfig", - "type": "object" + "type": "object", + "$id": "CORSConfig" }, "DeviceManagerSource": { "additionalProperties": false, @@ -95,7 +97,8 @@ "module" ], "title": "DeviceManagerSource", - "type": "object" + "type": "object", + "$id": "DeviceManagerSource" }, "DeviceSource": { "additionalProperties": false, @@ -116,7 +119,8 @@ "module" ], "title": "DeviceSource", - "type": "object" + "type": "object", + "$id": "DeviceSource" }, "DodalSource": { "additionalProperties": false, @@ -143,7 +147,8 @@ "module" ], "title": "DodalSource", - "type": "object" + "type": "object", + "$id": "DodalSource" }, "EnvironmentConfig": { "additionalProperties": false, @@ -163,25 +168,25 @@ "items": { "discriminator": { "mapping": { - "deviceFunctions": "#/$defs/DeviceSource", - "deviceManager": "#/$defs/DeviceManagerSource", - "dodal": "#/$defs/DodalSource", - "planFunctions": "#/$defs/PlanSource" + "deviceFunctions": "DeviceSource", + "deviceManager": "DeviceManagerSource", + "dodal": "DodalSource", + "planFunctions": "PlanSource" }, "propertyName": "kind" }, "oneOf": [ { - "$ref": "#/$defs/PlanSource" + "$ref": "PlanSource" }, { - "$ref": "#/$defs/DeviceSource" + "$ref": "DeviceSource" }, { - "$ref": "#/$defs/DodalSource" + "$ref": "DodalSource" }, { - "$ref": "#/$defs/DeviceManagerSource" + "$ref": "DeviceManagerSource" } ] }, @@ -189,12 +194,12 @@ "type": "array" }, "events": { - "$ref": "#/$defs/WorkerEventConfig" + "$ref": "WorkerEventConfig" }, "metadata": { "anyOf": [ { - "$ref": "#/$defs/MetadataConfig" + "$ref": "MetadataConfig" }, { "type": "null" @@ -204,7 +209,8 @@ } }, "title": "EnvironmentConfig", - "type": "object" + "type": "object", + "$id": "EnvironmentConfig" }, "GraylogConfig": { "additionalProperties": false, @@ -223,7 +229,8 @@ } }, "title": "GraylogConfig", - "type": "object" + "type": "object", + "$id": "GraylogConfig" }, "LoggingConfig": { "additionalProperties": false, @@ -242,7 +249,7 @@ "type": "string" }, "graylog": { - "$ref": "#/$defs/GraylogConfig", + "$ref": "GraylogConfig", "default": { "enabled": false, "url": "tcp://localhost:5555" @@ -250,7 +257,8 @@ } }, "title": "LoggingConfig", - "type": "object" + "type": "object", + "$id": "LoggingConfig" }, "MetadataConfig": { "additionalProperties": false, @@ -264,7 +272,8 @@ "instrument" ], "title": "MetadataConfig", - "type": "object" + "type": "object", + "$id": "MetadataConfig" }, "NumtrackerConfig": { "additionalProperties": false, @@ -284,7 +293,8 @@ } }, "title": "NumtrackerConfig", - "type": "object" + "type": "object", + "$id": "NumtrackerConfig" }, "OIDCConfig": { "additionalProperties": false, @@ -317,7 +327,8 @@ "client_id" ], "title": "OIDCConfig", - "type": "object" + "type": "object", + "$id": "OIDCConfig" }, "PlanSource": { "additionalProperties": false, @@ -338,7 +349,8 @@ "module" ], "title": "PlanSource", - "type": "object" + "type": "object", + "$id": "PlanSource" }, "RestConfig": { "additionalProperties": false, @@ -354,7 +366,7 @@ "cors": { "anyOf": [ { - "$ref": "#/$defs/CORSConfig" + "$ref": "CORSConfig" }, { "type": "null" @@ -364,7 +376,8 @@ } }, "title": "RestConfig", - "type": "object" + "type": "object", + "$id": "RestConfig" }, "ScratchConfig": { "additionalProperties": false, @@ -392,14 +405,15 @@ "repositories": { "description": "Details of repositories to be cloned and imported into blueapi", "items": { - "$ref": "#/$defs/ScratchRepository" + "$ref": "ScratchRepository" }, "title": "Repositories", "type": "array" } }, "title": "ScratchConfig", - "type": "object" + "type": "object", + "$id": "ScratchConfig" }, "ScratchRepository": { "additionalProperties": false, @@ -418,7 +432,8 @@ } }, "title": "ScratchRepository", - "type": "object" + "type": "object", + "$id": "ScratchRepository" }, "StompConfig": { "additionalProperties": false, @@ -440,7 +455,7 @@ "auth": { "anyOf": [ { - "$ref": "#/$defs/BasicAuthentication" + "$ref": "BasicAuthentication" }, { "type": "null" @@ -451,7 +466,8 @@ } }, "title": "StompConfig", - "type": "object" + "type": "object", + "$id": "StompConfig" }, "TiledConfig": { "additionalProperties": false, @@ -484,7 +500,8 @@ } }, "title": "TiledConfig", - "type": "object" + "type": "object", + "$id": "TiledConfig" }, "WorkerEventConfig": { "additionalProperties": false, @@ -497,31 +514,32 @@ } }, "title": "WorkerEventConfig", - "type": "object" + "type": "object", + "$id": "WorkerEventConfig" } }, "additionalProperties": false, "description": "Config for the worker application as a whole. Root of\nconfig tree.", "properties": { "stomp": { - "$ref": "#/$defs/StompConfig" + "$ref": "StompConfig" }, "tiled": { - "$ref": "#/$defs/TiledConfig" + "$ref": "TiledConfig" }, "env": { - "$ref": "#/$defs/EnvironmentConfig" + "$ref": "EnvironmentConfig" }, "logging": { - "$ref": "#/$defs/LoggingConfig" + "$ref": "LoggingConfig" }, "api": { - "$ref": "#/$defs/RestConfig" + "$ref": "RestConfig" }, "scratch": { "anyOf": [ { - "$ref": "#/$defs/ScratchConfig" + "$ref": "ScratchConfig" }, { "type": "null" @@ -532,7 +550,7 @@ "oidc": { "anyOf": [ { - "$ref": "#/$defs/OIDCConfig" + "$ref": "OIDCConfig" }, { "type": "null" @@ -556,7 +574,7 @@ "numtracker": { "anyOf": [ { - "$ref": "#/$defs/NumtrackerConfig" + "$ref": "NumtrackerConfig" }, { "type": "null" diff --git a/helm/blueapi/values.schema.json b/helm/blueapi/values.schema.json index 5747d10c0e..1578c0dad8 100644 --- a/helm/blueapi/values.schema.json +++ b/helm/blueapi/values.schema.json @@ -33,8 +33,12 @@ "type": "string" }, "global": { - "description": "Not used, but must be present for validation when using as a dependency of another chart", - "type": "object" + "description": "Global values shared between all subcharts", + "$comment": "Added automatically by 'helm schema' to allow this chart to be used as a Helm dependency, as the `additionalProperties` setting would otherwise collide with Helm's special 'global' values key.", + "type": [ + "object", + "null" + ] }, "hostNetwork": { "description": "May be needed for EPICS depending on gateway configuration", @@ -432,547 +436,570 @@ }, "additionalProperties": false, "$defs": { - "config_schema.json": { - "$id": "config_schema.json", - "title": "ApplicationConfig", - "description": "Config for the worker application as a whole. Root of\nconfig tree.", + "BasicAuthentication": { + "$id": "BasicAuthentication", + "title": "BasicAuthentication", + "description": "User credentials for basic authentication", "type": "object", + "required": [ + "username", + "password" + ], "properties": { - "api": { - "$ref": "#/$defs/RestConfig" + "password": { + "title": "Password", + "description": "Password to verify user's identity", + "type": "string" }, - "auth_token_path": { - "title": "Auth Token Path", + "username": { + "title": "Username", + "description": "Unique identifier for user", + "type": "string" + } + }, + "additionalProperties": false + }, + "CORSConfig": { + "$id": "CORSConfig", + "title": "CORSConfig", + "type": "object", + "required": [ + "origins" + ], + "properties": { + "allow_credentials": { + "title": "Allow Credentials", + "default": false, + "type": "boolean" + }, + "allow_headers": { + "title": "Allow Headers", + "default": [ + "*" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "allow_methods": { + "title": "Allow Methods", + "default": [ + "*" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "origins": { + "title": "Origins", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "DeviceManagerSource": { + "$id": "DeviceManagerSource", + "title": "DeviceManagerSource", + "type": "object", + "required": [ + "module" + ], + "properties": { + "kind": { + "title": "Kind", + "default": "deviceManager", + "const": "deviceManager" + }, + "mock": { + "title": "Mock", + "description": "If true, ophyd_async device connections are mocked", + "default": false, + "type": "boolean" + }, + "module": { + "title": "Module", + "description": "Module to be imported", + "type": "string" + }, + "name": { + "title": "Name", + "description": "Name of the device manager in the module", + "default": "devices", + "type": "string" + } + }, + "additionalProperties": false + }, + "DeviceSource": { + "$id": "DeviceSource", + "title": "DeviceSource", + "type": "object", + "required": [ + "module" + ], + "properties": { + "kind": { + "title": "Kind", + "default": "deviceFunctions", + "const": "deviceFunctions" + }, + "module": { + "title": "Module", + "description": "Module to be imported", + "type": "string" + } + }, + "additionalProperties": false + }, + "DodalSource": { + "$id": "DodalSource", + "title": "DodalSource", + "type": "object", + "required": [ + "module" + ], + "properties": { + "kind": { + "title": "Kind", + "default": "dodal", + "const": "dodal" + }, + "mock": { + "title": "Mock", + "description": "If true, ophyd_async device connections are mocked", + "default": false, + "type": "boolean" + }, + "module": { + "title": "Module", + "description": "Module to be imported", + "type": "string" + } + }, + "additionalProperties": false + }, + "EnvironmentConfig": { + "$id": "EnvironmentConfig", + "title": "EnvironmentConfig", + "description": "Config for the RunEngine environment", + "type": "object", + "properties": { + "events": { + "$ref": "WorkerEventConfig" + }, + "metadata": { "anyOf": [ { - "type": "string" + "$ref": "MetadataConfig" }, { "type": "null" } ] }, - "env": { - "$ref": "#/$defs/EnvironmentConfig" - }, - "logging": { - "$ref": "#/$defs/LoggingConfig" - }, - "numtracker": { - "anyOf": [ + "sources": { + "title": "Sources", + "default": [ { - "$ref": "#/$defs/NumtrackerConfig" + "kind": "planFunctions", + "module": "dodal.plans" }, { - "type": "null" + "kind": "planFunctions", + "module": "dodal.plan_stubs.wrapped" } + ], + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "PlanSource" + }, + { + "$ref": "DeviceSource" + }, + { + "$ref": "DodalSource" + }, + { + "$ref": "DeviceManagerSource" + } + ] + } + } + }, + "additionalProperties": false + }, + "GraylogConfig": { + "$id": "GraylogConfig", + "title": "GraylogConfig", + "type": "object", + "properties": { + "enabled": { + "title": "Enabled", + "default": false, + "type": "boolean" + }, + "url": { + "title": "Url", + "default": "tcp://localhost:5555", + "type": "string", + "format": "uri", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "LoggingConfig": { + "$id": "LoggingConfig", + "title": "LoggingConfig", + "type": "object", + "properties": { + "graylog": { + "default": { + "enabled": false, + "url": "tcp://localhost:5555" + }, + "$ref": "GraylogConfig" + }, + "level": { + "title": "Level", + "default": "INFO", + "type": "string", + "enum": [ + "NOTSET", + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL" ] + } + }, + "additionalProperties": false + }, + "MetadataConfig": { + "$id": "MetadataConfig", + "title": "MetadataConfig", + "type": "object", + "required": [ + "instrument" + ], + "properties": { + "instrument": { + "title": "Instrument", + "type": "string" + } + }, + "additionalProperties": false + }, + "NumtrackerConfig": { + "$id": "NumtrackerConfig", + "title": "NumtrackerConfig", + "type": "object", + "properties": { + "detector_file_template": { + "title": "Detector File Template", + "default": "{instrument}-{scan_id}-{device_name}", + "type": "string" }, - "oidc": { + "url": { + "title": "Url", + "default": "http://localhost:8406/graphql", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1 + } + }, + "additionalProperties": false + }, + "OIDCConfig": { + "$id": "OIDCConfig", + "title": "OIDCConfig", + "type": "object", + "required": [ + "well_known_url", + "client_id" + ], + "properties": { + "client_audience": { + "title": "Client Audience", + "description": "Client Audience(s)", + "default": "blueapi", + "type": "string" + }, + "client_id": { + "title": "Client Id", + "description": "Client ID", + "type": "string" + }, + "logout_redirect_endpoint": { + "title": "Logout Redirect Endpoint", + "description": "The oidc endpoint required to logout", + "default": "", + "type": "string" + }, + "well_known_url": { + "title": "Well Known Url", + "description": "URL to fetch OIDC config from the provider", + "type": "string" + } + }, + "additionalProperties": false + }, + "PlanSource": { + "$id": "PlanSource", + "title": "PlanSource", + "type": "object", + "required": [ + "module" + ], + "properties": { + "kind": { + "title": "Kind", + "default": "planFunctions", + "const": "planFunctions" + }, + "module": { + "title": "Module", + "description": "Module to be imported", + "type": "string" + } + }, + "additionalProperties": false + }, + "RestConfig": { + "$id": "RestConfig", + "title": "RestConfig", + "type": "object", + "properties": { + "cors": { "anyOf": [ { - "$ref": "#/$defs/OIDCConfig" + "$ref": "CORSConfig" }, { "type": "null" } ] }, - "scratch": { + "url": { + "title": "Url", + "default": "http://localhost:8000/", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1 + } + }, + "additionalProperties": false + }, + "ScratchConfig": { + "$id": "ScratchConfig", + "title": "ScratchConfig", + "type": "object", + "properties": { + "repositories": { + "title": "Repositories", + "description": "Details of repositories to be cloned and imported into blueapi", + "type": "array", + "items": { + "$ref": "ScratchRepository" + } + }, + "required_gid": { + "title": "Required Gid", + "description": "\nRequired owner GID for the scratch directory. If supplied, the setup-scratch\ncommand will check the scratch area ownership and raise an error if it is\nnot owned by \u003cGID\u003e, or if it does not have SGID permission bit set.\n", "anyOf": [ { - "$ref": "#/$defs/ScratchConfig" + "type": "integer" }, { "type": "null" } ] }, - "stomp": { - "$ref": "#/$defs/StompConfig" - }, - "tiled": { - "$ref": "#/$defs/TiledConfig" + "root": { + "title": "Root", + "description": "The root directory of the scratch area, all repositories will be cloned under this directory.", + "default": "/tmp/scratch/blueapi", + "type": "string", + "format": "path" } }, - "additionalProperties": false, - "$defs": { - "BasicAuthentication": { - "title": "BasicAuthentication", - "description": "User credentials for basic authentication", - "type": "object", - "required": [ - "username", - "password" - ], - "properties": { - "password": { - "title": "Password", - "description": "Password to verify user's identity", - "type": "string" - }, - "username": { - "title": "Username", - "description": "Unique identifier for user", - "type": "string" - } - }, - "additionalProperties": false - }, - "CORSConfig": { - "title": "CORSConfig", - "type": "object", - "required": [ - "origins" - ], - "properties": { - "allow_credentials": { - "title": "Allow Credentials", - "default": false, - "type": "boolean" - }, - "allow_headers": { - "title": "Allow Headers", - "default": [ - "*" - ], - "type": "array", - "items": { - "type": "string" - } - }, - "allow_methods": { - "title": "Allow Methods", - "default": [ - "*" - ], - "type": "array", - "items": { - "type": "string" - } - }, - "origins": { - "title": "Origins", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false + "additionalProperties": false + }, + "ScratchRepository": { + "$id": "ScratchRepository", + "title": "ScratchRepository", + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "Unique name for this repository in the scratch directory", + "default": "example", + "type": "string" }, - "DeviceManagerSource": { - "title": "DeviceManagerSource", - "type": "object", - "required": [ - "module" - ], - "properties": { - "kind": { - "title": "Kind", - "default": "deviceManager", - "const": "deviceManager" - }, - "mock": { - "title": "Mock", - "description": "If true, ophyd_async device connections are mocked", - "default": false, - "type": "boolean" - }, - "module": { - "title": "Module", - "description": "Module to be imported", - "type": "string" + "remote_url": { + "title": "Remote Url", + "description": "URL to clone from", + "default": "https://github.com/example/example.git", + "type": "string" + } + }, + "additionalProperties": false + }, + "StompConfig": { + "$id": "StompConfig", + "title": "StompConfig", + "description": "Config for connecting to stomp broker", + "type": "object", + "properties": { + "auth": { + "description": "Auth information for communicating with STOMP broker, if required", + "anyOf": [ + { + "$ref": "BasicAuthentication" }, - "name": { - "title": "Name", - "description": "Name of the device manager in the module", - "default": "devices", - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, - "DeviceSource": { - "title": "DeviceSource", - "type": "object", - "required": [ - "module" - ], - "properties": { - "kind": { - "title": "Kind", - "default": "deviceFunctions", - "const": "deviceFunctions" - }, - "module": { - "title": "Module", - "description": "Module to be imported", - "type": "string" - } - }, - "additionalProperties": false + "enabled": { + "title": "Enabled", + "description": "True if blueapi should connect to stomp for asynchronous event publishing", + "default": false, + "type": "boolean" }, - "DodalSource": { - "title": "DodalSource", - "type": "object", - "required": [ - "module" - ], - "properties": { - "kind": { - "title": "Kind", - "default": "dodal", - "const": "dodal" - }, - "mock": { - "title": "Mock", - "description": "If true, ophyd_async device connections are mocked", - "default": false, - "type": "boolean" - }, - "module": { - "title": "Module", - "description": "Module to be imported", + "url": { + "title": "Url", + "default": "tcp://localhost:61613", + "type": "string", + "format": "uri", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "TiledConfig": { + "$id": "TiledConfig", + "title": "TiledConfig", + "type": "object", + "properties": { + "api_key": { + "title": "Api Key", + "anyOf": [ + { "type": "string" - } - }, - "additionalProperties": false - }, - "EnvironmentConfig": { - "title": "EnvironmentConfig", - "description": "Config for the RunEngine environment", - "type": "object", - "properties": { - "events": { - "$ref": "#/$defs/WorkerEventConfig" - }, - "metadata": { - "anyOf": [ - { - "$ref": "#/$defs/MetadataConfig" - }, - { - "type": "null" - } - ] }, - "sources": { - "title": "Sources", - "default": [ - { - "kind": "planFunctions", - "module": "dodal.plans" - }, - { - "kind": "planFunctions", - "module": "dodal.plan_stubs.wrapped" - } - ], - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/$defs/PlanSource" - }, - { - "$ref": "#/$defs/DeviceSource" - }, - { - "$ref": "#/$defs/DodalSource" - }, - { - "$ref": "#/$defs/DeviceManagerSource" - } - ] - } - } - }, - "additionalProperties": false - }, - "GraylogConfig": { - "title": "GraylogConfig", - "type": "object", - "properties": { - "enabled": { - "title": "Enabled", - "default": false, - "type": "boolean" - }, - "url": { - "title": "Url", - "default": "tcp://localhost:5555", - "type": "string", - "minLength": 1 + { + "type": "null" } - }, - "additionalProperties": false + ] }, - "LoggingConfig": { - "title": "LoggingConfig", - "type": "object", - "properties": { - "graylog": { - "default": { - "enabled": false, - "url": "tcp://localhost:5555" - }, - "$ref": "#/$defs/GraylogConfig" - }, - "level": { - "title": "Level", - "default": "INFO", - "type": "string", - "enum": [ - "NOTSET", - "DEBUG", - "INFO", - "WARNING", - "ERROR", - "CRITICAL" - ] - } - }, - "additionalProperties": false + "enabled": { + "title": "Enabled", + "description": "True if blueapi should forward data to a Tiled instance", + "default": false, + "type": "boolean" }, - "MetadataConfig": { - "title": "MetadataConfig", - "type": "object", - "required": [ - "instrument" - ], - "properties": { - "instrument": { - "title": "Instrument", - "type": "string" - } - }, - "additionalProperties": false + "url": { + "title": "Url", + "default": "http://localhost:8407/", + "type": "string", + "format": "uri", + "maxLength": 2083, + "minLength": 1 + } + }, + "additionalProperties": false + }, + "WorkerEventConfig": { + "$id": "WorkerEventConfig", + "title": "WorkerEventConfig", + "description": "Config for event broadcasting via the message bus", + "type": "object", + "properties": { + "broadcast_status_events": { + "title": "Broadcast Status Events", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "config_schema.json": { + "$id": "config_schema.json", + "title": "ApplicationConfig", + "description": "Config for the worker application as a whole. Root of\nconfig tree.", + "type": "object", + "properties": { + "api": { + "$ref": "RestConfig" }, - "NumtrackerConfig": { - "title": "NumtrackerConfig", - "type": "object", - "properties": { - "detector_file_template": { - "title": "Detector File Template", - "default": "{instrument}-{scan_id}-{device_name}", - "type": "string" - }, - "url": { - "title": "Url", - "default": "http://localhost:8406/graphql", + "auth_token_path": { + "title": "Auth Token Path", + "anyOf": [ + { "type": "string", - "maxLength": 2083, - "minLength": 1 - } - }, - "additionalProperties": false - }, - "OIDCConfig": { - "title": "OIDCConfig", - "type": "object", - "required": [ - "well_known_url", - "client_id" - ], - "properties": { - "client_audience": { - "title": "Client Audience", - "description": "Client Audience(s)", - "default": "blueapi", - "type": "string" - }, - "client_id": { - "title": "Client Id", - "description": "Client ID", - "type": "string" - }, - "logout_redirect_endpoint": { - "title": "Logout Redirect Endpoint", - "description": "The oidc endpoint required to logout", - "default": "", - "type": "string" + "format": "path" }, - "well_known_url": { - "title": "Well Known Url", - "description": "URL to fetch OIDC config from the provider", - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, - "PlanSource": { - "title": "PlanSource", - "type": "object", - "required": [ - "module" - ], - "properties": { - "kind": { - "title": "Kind", - "default": "planFunctions", - "const": "planFunctions" - }, - "module": { - "title": "Module", - "description": "Module to be imported", - "type": "string" - } - }, - "additionalProperties": false + "env": { + "$ref": "EnvironmentConfig" }, - "RestConfig": { - "title": "RestConfig", - "type": "object", - "properties": { - "cors": { - "anyOf": [ - { - "$ref": "#/$defs/CORSConfig" - }, - { - "type": "null" - } - ] - }, - "url": { - "title": "Url", - "default": "http://localhost:8000/", - "type": "string", - "maxLength": 2083, - "minLength": 1 - } - }, - "additionalProperties": false + "logging": { + "$ref": "LoggingConfig" }, - "ScratchConfig": { - "title": "ScratchConfig", - "type": "object", - "properties": { - "repositories": { - "title": "Repositories", - "description": "Details of repositories to be cloned and imported into blueapi", - "type": "array", - "items": { - "$ref": "#/$defs/ScratchRepository" - } - }, - "required_gid": { - "title": "Required Gid", - "description": "\nRequired owner GID for the scratch directory. If supplied, the setup-scratch\ncommand will check the scratch area ownership and raise an error if it is\nnot owned by \u003cGID\u003e, or if it does not have SGID permission bit set.\n", - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ] + "numtracker": { + "anyOf": [ + { + "$ref": "NumtrackerConfig" }, - "root": { - "title": "Root", - "description": "The root directory of the scratch area, all repositories will be cloned under this directory.", - "default": "/tmp/scratch/blueapi", - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, - "ScratchRepository": { - "title": "ScratchRepository", - "type": "object", - "properties": { - "name": { - "title": "Name", - "description": "Unique name for this repository in the scratch directory", - "default": "example", - "type": "string" + "oidc": { + "anyOf": [ + { + "$ref": "OIDCConfig" }, - "remote_url": { - "title": "Remote Url", - "description": "URL to clone from", - "default": "https://github.com/example/example.git", - "type": "string" + { + "type": "null" } - }, - "additionalProperties": false + ] }, - "StompConfig": { - "title": "StompConfig", - "description": "Config for connecting to stomp broker", - "type": "object", - "properties": { - "auth": { - "description": "Auth information for communicating with STOMP broker, if required", - "anyOf": [ - { - "$ref": "#/$defs/BasicAuthentication" - }, - { - "type": "null" - } - ] - }, - "enabled": { - "title": "Enabled", - "description": "True if blueapi should connect to stomp for asynchronous event publishing", - "default": false, - "type": "boolean" + "scratch": { + "anyOf": [ + { + "$ref": "ScratchConfig" }, - "url": { - "title": "Url", - "default": "tcp://localhost:61613", - "type": "string", - "minLength": 1 + { + "type": "null" } - }, - "additionalProperties": false + ] }, - "TiledConfig": { - "title": "TiledConfig", - "type": "object", - "properties": { - "api_key": { - "title": "Api Key", - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "enabled": { - "title": "Enabled", - "description": "True if blueapi should forward data to a Tiled instance", - "default": false, - "type": "boolean" - }, - "url": { - "title": "Url", - "default": "http://localhost:8407/", - "type": "string", - "maxLength": 2083, - "minLength": 1 - } - }, - "additionalProperties": false + "stomp": { + "$ref": "StompConfig" }, - "WorkerEventConfig": { - "title": "WorkerEventConfig", - "description": "Config for event broadcasting via the message bus", - "type": "object", - "properties": { - "broadcast_status_events": { - "title": "Broadcast Status Events", - "default": true, - "type": "boolean" - } - }, - "additionalProperties": false + "tiled": { + "$ref": "TiledConfig" } - } + }, + "additionalProperties": false } } } diff --git a/helm/blueapi/values.yaml b/helm/blueapi/values.yaml index 9376bc524a..9831981c98 100644 --- a/helm/blueapi/values.yaml +++ b/helm/blueapi/values.yaml @@ -230,6 +230,3 @@ debug: suspend: false # -- If enabled configures debugpy to use the option `--log-to-stderr` log_to_stderr: false - -# -- Not used, but must be present for validation when using as a dependency of another chart -global: {} diff --git a/src/blueapi/cli/cli.py b/src/blueapi/cli/cli.py index 957a9cb27c..1119902d1c 100644 --- a/src/blueapi/cli/cli.py +++ b/src/blueapi/cli/cli.py @@ -119,10 +119,10 @@ def schema(output: Path | None = None, update: bool = False) -> None: help="[Development only] update the config schema in the documentation", ) @main.command(name="config-schema") -def config_schema(output: Path | None = None, update: bool = False) -> None: - """Generates a json schema from the ApplicationConfig pydantic basemodel""" - schema = ApplicationConfig.model_json_schema() +def config_schema(output: Path | None = None, update: bool = False) -> None: # + from blueapi.utils.serialization import generate_config_schema + schema = generate_config_schema() if update: output = config.CONFIG_SCHEMA_LOCATION if output is not None: diff --git a/src/blueapi/utils/serialization.py b/src/blueapi/utils/serialization.py index deee82b1e1..3a9e472443 100644 --- a/src/blueapi/utils/serialization.py +++ b/src/blueapi/utils/serialization.py @@ -3,6 +3,9 @@ from typing import Any from pydantic import BaseModel +from pydantic.json_schema import GenerateJsonSchema + +from blueapi.config import ApplicationConfig def serialize(obj: Any) -> Any: @@ -46,3 +49,23 @@ def access_blob(instrument_session: str, beamline: str) -> str: "beamline": beamline, } return json.dumps(blob) + + +def generate_config_schema() -> dict[str, Any]: + """ + Generate a JSON schema from the ApplicationConfig Pydantic model. + + This schema is used to create config_schema.json, which is consumed by the + helm-values-schema plugin for validation. + """ + + class _GenerateJsonSchema(GenerateJsonSchema): + def generate(self, schema, mode="validation"): + json_schema = super().generate(schema, mode=mode) + for i in json_schema["$defs"]: + json_schema["$defs"][i]["$id"] = i + return json_schema + + return ApplicationConfig.model_json_schema( + schema_generator=_GenerateJsonSchema, ref_template="{model}" + ) diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index b2a0abd5ce..da5793503e 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -20,6 +20,7 @@ OIDCConfig, ) from blueapi.utils import InvalidConfigError +from blueapi.utils.serialization import generate_config_schema class Config(BaseModel): @@ -534,7 +535,8 @@ def validate_field_annotations(model_class: Any, model_field: str) -> None: def test_config_schema_updated() -> None: with CONFIG_SCHEMA_LOCATION.open("r") as stream: config_schema = json.load(stream) - assert config_schema == ApplicationConfig.model_json_schema(), ( + + assert config_schema == generate_config_schema(), ( f"ApplicationConfig model is out of date with schema at \ {CONFIG_SCHEMA_LOCATION}. You may need to run `blueapi config-schema -u`" ) From 49ca17ec794ccd6a7c04bcfd4c3677b26d1a62e2 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:12:12 +0000 Subject: [PATCH 2/4] move generate_config_schema to config.py --- src/blueapi/cli/cli.py | 2 +- src/blueapi/config.py | 21 +++++++++++++++++++++ src/blueapi/utils/serialization.py | 23 ----------------------- tests/unit_tests/test_config.py | 2 +- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/blueapi/cli/cli.py b/src/blueapi/cli/cli.py index 1119902d1c..d4cae3e354 100644 --- a/src/blueapi/cli/cli.py +++ b/src/blueapi/cli/cli.py @@ -120,7 +120,7 @@ def schema(output: Path | None = None, update: bool = False) -> None: ) @main.command(name="config-schema") def config_schema(output: Path | None = None, update: bool = False) -> None: # - from blueapi.utils.serialization import generate_config_schema + from blueapi.config import generate_config_schema schema = generate_config_schema() if update: diff --git a/src/blueapi/config.py b/src/blueapi/config.py index a05a26c6ae..eb1b3122ed 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -379,3 +379,24 @@ def __getattr__(name: str): ) return rename raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def generate_config_schema() -> dict[str, Any]: + """ + Generate a JSON schema from the ApplicationConfig Pydantic model. + + This schema is used to create config_schema.json, which is consumed by the + helm-values-schema plugin for validation. + """ + from pydantic.json_schema import GenerateJsonSchema + + class _GenerateJsonSchema(GenerateJsonSchema): + def generate(self, schema, mode="validation"): + json_schema = super().generate(schema, mode=mode) + for i in json_schema["$defs"]: + json_schema["$defs"][i]["$id"] = i + return json_schema + + return ApplicationConfig.model_json_schema( + schema_generator=_GenerateJsonSchema, ref_template="{model}" + ) diff --git a/src/blueapi/utils/serialization.py b/src/blueapi/utils/serialization.py index 3a9e472443..deee82b1e1 100644 --- a/src/blueapi/utils/serialization.py +++ b/src/blueapi/utils/serialization.py @@ -3,9 +3,6 @@ from typing import Any from pydantic import BaseModel -from pydantic.json_schema import GenerateJsonSchema - -from blueapi.config import ApplicationConfig def serialize(obj: Any) -> Any: @@ -49,23 +46,3 @@ def access_blob(instrument_session: str, beamline: str) -> str: "beamline": beamline, } return json.dumps(blob) - - -def generate_config_schema() -> dict[str, Any]: - """ - Generate a JSON schema from the ApplicationConfig Pydantic model. - - This schema is used to create config_schema.json, which is consumed by the - helm-values-schema plugin for validation. - """ - - class _GenerateJsonSchema(GenerateJsonSchema): - def generate(self, schema, mode="validation"): - json_schema = super().generate(schema, mode=mode) - for i in json_schema["$defs"]: - json_schema["$defs"][i]["$id"] = i - return json_schema - - return ApplicationConfig.model_json_schema( - schema_generator=_GenerateJsonSchema, ref_template="{model}" - ) diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index da5793503e..1d6c18e373 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -18,9 +18,9 @@ ApplicationConfig, ConfigLoader, OIDCConfig, + generate_config_schema, ) from blueapi.utils import InvalidConfigError -from blueapi.utils.serialization import generate_config_schema class Config(BaseModel): From e93306fa5ed9442ec43d49205f08eb3cb84c7c2c Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:15:29 +0000 Subject: [PATCH 3/4] Make change smaller --- src/blueapi/cli/cli.py | 2 +- tests/unit_tests/test_config.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/blueapi/cli/cli.py b/src/blueapi/cli/cli.py index d4cae3e354..c39ff13dff 100644 --- a/src/blueapi/cli/cli.py +++ b/src/blueapi/cli/cli.py @@ -119,7 +119,7 @@ def schema(output: Path | None = None, update: bool = False) -> None: help="[Development only] update the config schema in the documentation", ) @main.command(name="config-schema") -def config_schema(output: Path | None = None, update: bool = False) -> None: # +def config_schema(output: Path | None = None, update: bool = False) -> None: from blueapi.config import generate_config_schema schema = generate_config_schema() diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 1d6c18e373..26621502a4 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -535,7 +535,6 @@ def validate_field_annotations(model_class: Any, model_field: str) -> None: def test_config_schema_updated() -> None: with CONFIG_SCHEMA_LOCATION.open("r") as stream: config_schema = json.load(stream) - assert config_schema == generate_config_schema(), ( f"ApplicationConfig model is out of date with schema at \ {CONFIG_SCHEMA_LOCATION}. You may need to run `blueapi config-schema -u`" From 2ab4f4405d5637ffd9d272d8f88dd82549e2ee38 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:21:09 +0000 Subject: [PATCH 4/4] fix test --- tests/unit_tests/test_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 3c91d5b774..2a49a1fe80 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -39,6 +39,7 @@ ApplicationConfig, ScratchConfig, ScratchRepository, + generate_config_schema, ) from blueapi.core.bluesky_types import DataEvent, Plan from blueapi.service.model import ( @@ -1309,7 +1310,7 @@ def test_config_schema( args, ) - expected = ApplicationConfig.model_json_schema() + expected = generate_config_schema() if output_flag and (not update): with tmp_path.open("r") as stream: assert json.load(stream) == expected