diff --git a/src/launch_manager_daemon/config/future_config_schema/README.rst b/src/launch_manager_daemon/config/future_config_schema/README.rst new file mode 100755 index 00000000..281467ca --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/README.rst @@ -0,0 +1,173 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + + +Launch Manager Configuration Schema +################################### + +This folder contains the development environment for the Launch Manager configuration JSON Schema. The schema defines and validates the structure of Launch Manager configuration files. + +Overview +******** + +This project uses a **two-folder approach** for schema management: + +- ``draft_schema/`` - Multi-file schema structure for active development +- ``published_schema/`` - Single-file schema for end-user consumption + +The multi-file structure in ``draft_schema/`` makes it easier to maintain and modify the schema by organizing reusable components into separate files. When development is complete, these files are compiled into a single file in ``published_schema/`` for convenience of end users. + +**Project Structure:** + +:: + + +-- draft_schema/ # Multi-file schema under development + +-- published_schema/ # Single-file schema ready for use + +-- examples/ # Sample configuration files + +-- scripts/ # Tools for bundling and validation + +Quick Start +*********** + +For End Users +============= + +If you just want to validate your Launch Manager configuration: + +1. Use the schema in ``published_schema/s-core_launch_manager.schema.json`` +2. Check the ``examples/`` folder for sample configurations +3. Validate your config: + + .. code-block:: bash + + validate.py --schema published_schema/s-core_launch_manager.schema.json --instance your_config.json + +For Schema Developers +====================== + +If you're modifying or extending the schema: + +1. Edit files in ``draft_schema/`` +2. Bundle your changes: + + .. code-block:: bash + + bundle.py --input draft_schema/s-core_launch_manager.schema.json --output published_schema/s-core_launch_manager.schema.json + +3. Test against examples to ensure nothing broke + + +Examples +******** + +Configuration examples are provided in the ``examples`` folder, each accompanied by a brief description. **Start here** if you're new to Launch Manager configurations - these show real-world usage patterns. + + +Schema Development (draft_schema) +********************************** + +The ``draft_schema`` folder contains the primary development work. The setup uses a multi-file structure where: + +- **Reusable types** are stored in the ``types/`` subfolder +- **Top-level schema** resides in ``s-core_launch_manager.schema.json`` file + +Working with $ref Paths +======================== + +The multi-file schema uses JSON Schema's ``$ref`` keyword to reference definitions across files. Understanding how these references work is crucial when modifying the schema. + +**Key principle:** All ``$ref`` paths are relative to the location of the file containing the reference, not to any root folder. + +Reference Examples +------------------ + +**To reference a file in a subfolder** (e.g., from ``s-core_launch_manager.schema.json`` to ``types/deployment_config.schema.json``): + +.. code-block:: json + + "$ref": "./types/deployment_config.schema.json" + +**To reference a file in the same folder:** (e.g., from ``types/deployment_config.schema.json`` to ``types/recovery_action.schema.json``): + +.. code-block:: json + + "$ref": "./recovery_action.schema.json" + +Common Pitfalls +--------------- + +- **Always use relative paths** starting with ``./`` or ``../`` +- **Don't use absolute paths** or paths from the project root +- **Remember the current file's location** when constructing paths +- When moving files, **update all references** to and from that file + +The bundling script resolves all these relative references into a single file, so the published schema doesn't need external file references. + + +Published Schema (published_schema) +************************************ + +The official, end-user consumable schema is placed in the ``published_schema`` folder. Upon completion of development, the multi-file schema from the ``draft_schema`` folder is merged into a single file and published here. + +**This is the version end users should reference** in their validation tools and IDE configurations. + + +Scripts +******* + +Utility scripts for schema development are located in the ``scripts`` folder: + +bundle.py +========= + +Merges the multi-file schema into a single file for end-user distribution. + +**Usage:** + +.. code-block:: bash + + bundle.py --input ../draft_schema/s-core_launch_manager.schema.json --output ../published_schema/s-core_launch_manager.schema.json + Bundled schema written to: ../published_schema/s-core_launch_manager.schema.json + +**When to use:** After making changes in ``draft_schema/``, run this to create the publishable version. + +validate.py +=========== + +Validates Launch Manager configuration instances against the schema. This script supports both single-file and multi-file schema formats. + +**Validate against published schema:** + +.. code-block:: bash + + validate.py --schema ../published_schema/s-core_launch_manager.schema.json --instance ../examples/example_conf.json + Success --> ../examples/example_conf.json: valid + +**Validate against draft schema (during development):** + +.. code-block:: bash + + validate.py --schema ../draft_schema/s-core_launch_manager.schema.json --instance ../examples/example_conf.json + Success --> ../examples/example_conf.json: valid + +**When to use:** Run this frequently during development to catch errors early. Always validate examples before publishing. + + +Typical Workflow +**************** + +1. **Modify** schema files in ``draft_schema/`` +2. **Validate** your changes against examples using the draft schema +3. **Bundle** the multi-file schema into a single file +4. **Validate** examples again against the published schema diff --git a/src/launch_manager_daemon/config/future_config_schema/draft_schema/s-core_launch_manager.schema.json b/src/launch_manager_daemon/config/future_config_schema/draft_schema/s-core_launch_manager.schema.json new file mode 100755 index 00000000..f76e3591 --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/draft_schema/s-core_launch_manager.schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Configuration schema for the S-CORE Launch Manager", + "description": "Defines the structure and valid values for the Launch Manager configuration file, which specifies managed components, run targets, and recovery behaviors.", + "properties": { + "schema_version": { + "type": "integer", + "description": "Specifies the schema version number that the Launch Manager uses to determine how to parse and validate this configuration file.", + "enum": [ 1 ] + }, + "defaults": { + "type": "object", + "description": "Defines default configuration values that components and Run Targets inherit unless they provide their own overriding values.", + "properties": { + "component_properties": { + "$ref": "./types/component_properties.schema.json", + "description": "Defines default component property values applied to all components unless overridden in individual component definitions." + }, + "deployment_config": { + "$ref": "./types/deployment_config.schema.json", + "description": "Defines default deployment configuration values applied to all components unless overridden in individual component definitions." + }, + "run_target": { + "$ref": "./types/run_target.schema.json", + "description": "Defines default Run Target configuration values applied to all Run Targets unless overridden in individual Run Target definitions." + }, + "alive_supervision": { + "$ref": "./types/alive_supervision.schema.json", + "description": "Defines default alive supervision configuration values used unless a global 'alive_supervision' configuration is specified at the root level." + }, + "watchdog": { + "$ref": "./types/watchdog.schema.json", + "description": "Defines default watchdog configuration values applied to all watchdogs unless overridden in individual watchdog definitions." + } + }, + "required": [], + "additionalProperties": false + }, + "components": { + "type": "object", + "description": "Defines software components managed by the Launch Manager, where each property name is a unique component identifier and its value contains the component's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "description": "Defines an individual component's configuration properties and deployment settings.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a human-readable description of the component's purpose." + }, + "component_properties": { + "$ref": "./types/component_properties.schema.json", + "description": "Defines component properties for this component; any properties not specified here are inherited from 'defaults.component_properties'." + }, + "deployment_config": { + "$ref": "./types/deployment_config.schema.json", + "description": "Defines deployment configuration for this component; any properties not specified here are inherited from 'defaults.deployment_config'." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_targets": { + "type": "object", + "description": "Defines Run Targets representing different operational modes of the system, where each property name is a unique Run Target identifier and its value contains the Run Target's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "$ref": "./types/run_target.schema.json" + } + }, + "required": [], + "additionalProperties": false + }, + "initial_run_target": { + "type": "string", + "description": "Specifies the name of the initial Run Target that the Launch Manager activates during the startup sequence. This name must match a Run Target defined in 'run_targets'." + }, + "fallback_run_target": { + "type": "object", + "description": "Defines the fallback Run Target configuration that the Launch Manager activates when all recovery attempts have been exhausted. This Run Target does not include a recovery_action property.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a human-readable description of the fallback Run Target." + }, + "depends_on": { + "type": "array", + "description": "Specifies the names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Specifies the name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Specifies the time limit for the Run Target transition. If this limit is exceeded, the transition is considered failed.", + "exclusiveMinimum": 0 + } + }, + "required": [ + "depends_on" + ], + "additionalProperties": false + }, + "alive_supervision": { + "$ref": "./types/alive_supervision.schema.json", + "description": "Defines the alive supervision configuration parameters used to monitor component health. This configuration overrides 'defaults.alive_supervision' if specified." + }, + "watchdog": { + "$ref": "./types/watchdog.schema.json", + "description": "Defines the external watchdog device configuration used by the Launch Manager. This configuration overrides 'defaults.watchdog' if specified." + } + }, + "required": [ + "schema_version", + "initial_run_target" + ], + "additionalProperties": false +} diff --git a/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/alive_supervision.schema.json b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/alive_supervision.schema.json new file mode 100755 index 00000000..919de721 --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/alive_supervision.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "description": "Defines a reusable type that contains configuration parameters for alive supervision.", + "properties": { + "evaluation_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the length of the time window used to assess incoming alive supervision reports." + } + }, + + "required": [], + "additionalProperties": false +} diff --git a/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/component_properties.schema.json b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/component_properties.schema.json new file mode 100755 index 00000000..087b470f --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/component_properties.schema.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "description": "Defines a reusable type that captures essential development-time characteristics of a software component.", + "properties": { + "binary_name": { + "type": "string", + "description": "Specifies the relative path of the executable file inside the directory defined by 'deployment_config.bin_dir'. The final executable path will be resolved as '{bin_dir}/{binary_name}'. Example values include simple filenames (e.g., 'test_app1') or subdirectory paths (e.g., 'bin/test_app1')." + }, + + "application_profile": { + "type": "object", + "description": "Defines the application profile that specifies the runtime behavior and capabilities of this component.", + "properties": { + "application_type": { + "type": "string", + "enum": [ + "Native", + "Reporting", + "Reporting_And_Supervised", + "State_Manager" + ], + "description": "Specifies the level of integration between the component and the Launch Manager. 'Native': no integration with Launch Manager. 'Reporting': uses Launch Manager lifecycle APIs. 'Reporting_And_Supervised': uses lifecycle APIs and sends alive notifications. 'State_Manager': uses lifecycle APIs, sends alive notifications, and has permission to change the active Run Target." + }, + "is_self_terminating": { + "type": "boolean", + "description": "Indicates whether the component is designed to terminate automatically once its planned tasks are completed (true), or remain running until explicitly requested to terminate by the Launch Manager (false)." + }, + "alive_supervision": { + "type": "object", + "description": "Defines the configuration parameters used for alive monitoring of the component.", + "properties": { + "reporting_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the duration of the time interval used to verify that the component sends alive notifications within the expected time frame." + }, + "failed_cycles_tolerance": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of consecutive reporting cycle failures (see 'reporting_cycle'). Once the number of failed cycles exceeds this maximum, the Launch Manager will trigger the configured recovery action." + }, + "min_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the minimum number of checkpoints that must be reported within each configured 'reporting_cycle'." + }, + "max_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of checkpoints that may be reported within each configured 'reporting_cycle'." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + + "depends_on": { + "type": "array", + "description": "Specifies the names of components that this component depends on. Each dependency must be initialized and reach its ready state before this component can start.", + "items": { + "type": "string", + "description": "Specifies the name of a component on which this component depends." + } + }, + + "process_arguments": { + "type": "array", + "description": "Specifies an ordered list of command-line arguments passed to the component at startup.", + "items": { + "type": "string", + "description": "Specifies a single command-line argument token as a UTF-8 string; order is preserved." + } + }, + + "ready_condition": { + "type": "object", + "description": "Defines the set of conditions that determine when the component completes its initializing state and enters the ready state.", + "properties": { + "process_state": { + "type": "string", + "enum": [ + "Running", + "Terminated" + ], + "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false +} diff --git a/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/deployment_config.schema.json b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/deployment_config.schema.json new file mode 100755 index 00000000..7a22b221 --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/deployment_config.schema.json @@ -0,0 +1,126 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "description": "Defines a reusable type that contains configuration parameters that are specific to a particular deployment environment or system setup.", + + "properties": { + "ready_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time allowed for the component to reach its ready state. The timeout is measured from when the component process is created until the ready conditions specified in 'component_properties.ready_condition' are met." + }, + "shutdown_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time allowed for the component to terminate after it receives a SIGTERM signal from the Launch Manager. The timeout is measured from when the Launch Manager sends the SIGTERM signal until the Operating System notifies the Launch Manager that the child process has terminated." + }, + "environmental_variables": { + "type": "object", + "description": "Defines the set of environment variables passed to the component at startup.", + "additionalProperties": { + "type": "string", + "description": "Specifies the environment variable's value as a string. An empty string is allowed and represents an intentionally empty environment variable." + } + }, + "bin_dir": { + "type": "string", + "description": "Specifies the absolute filesystem path to the directory where the component is installed." + }, + "working_dir": { + "type": "string", + "description": "Specifies the directory used as the working directory for the component during execution." + }, + "ready_recovery_action": { + "allOf": [ + { "$ref": "./recovery_action.schema.json" }, + { + "properties": { + "restart": true + }, + "required": ["restart"], + "not": { + "required": ["switch_run_target"] + } + } + ], + "description": "Specifies the recovery action to execute when the component fails to reach its ready state within the configured timeout." + }, + "recovery_action": { + "allOf": [ + { "$ref": "./recovery_action.schema.json" }, + { + "properties": { + "switch_run_target": true + }, + "required": ["switch_run_target"], + "not": { + "required": ["restart"] + } + } + ], + "description": "Specifies the recovery action to execute when the component malfunctions after reaching its ready state." + }, + "sandbox": { + "type": "object", + "description": "Defines the sandbox configuration parameters that isolate and constrain the component's runtime execution.", + "properties": { + "uid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the POSIX user ID (UID) under which this component executes." + }, + "gid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the primary POSIX group ID (GID) under which this component executes." + }, + "supplementary_group_ids": { + "type": "array", + "description": "Specifies the list of supplementary POSIX group IDs (GIDs) assigned to this component.", + "items": { + "type": "integer", + "minimum": 0, + "description": "Specifies a single supplementary POSIX group ID (GID)." + } + }, + "security_policy": { + "type": "string", + "description": "Specifies the security policy or confinement profile name (such as an SELinux or AppArmor profile) assigned to the component." + }, + "scheduling_policy": { + "type": "string", + "description": "Specifies the scheduling policy applied to the component's initial thread. Supported values correspond to OS-defined policies (e.g., FIFO, RR, OTHER).", + "anyOf": [ + { + "enum": [ + "SCHED_FIFO", + "SCHED_RR", + "SCHED_OTHER" + ] + }, + { + "type": "string" + } + ] + }, + "scheduling_priority": { + "type": "integer", + "description": "Specifies the scheduling priority applied to the component's initial thread." + }, + "max_memory_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum amount of memory, in bytes, that the component is permitted to use during runtime." + }, + "max_cpu_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage of total CPU capacity." + } + }, + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false +} diff --git a/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/recovery_action.schema.json b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/recovery_action.schema.json new file mode 100755 index 00000000..2f5dd119 --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/recovery_action.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "description": "Defines a reusable type that specifies recovery actions to execute when an error or failure occurs.", + "properties": { + "restart": { + "type": "object", + "description": "Defines a recovery action that restarts the POSIX process associated with this component.", + "properties": { + "number_of_attempts": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of restart attempts before the Launch Manager concludes that recovery cannot succeed." + }, + "delay_before_restart": { + "type": "number", + "minimum": 0, + "description": "Specifies the delay duration that the Launch Manager waits before initiating a restart attempt." + } + }, + "required": [], + "additionalProperties": false + }, + "switch_run_target": { + "type": "object", + "description": "Defines a recovery action that switches to a Run Target. This can be a different Run Target or the same one to retry activation of the current Run Target.", + "properties": { + "run_target": { + "type": "string", + "description": "Specifies the name of the Run Target that the Launch Manager should switch to." + } + }, + "required": [], + "additionalProperties": false + } + }, + + "oneOf": [ + { "required": ["restart"] }, + { "required": ["switch_run_target"] } + ], + "additionalProperties": false +} diff --git a/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/run_target.schema.json b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/run_target.schema.json new file mode 100755 index 00000000..72f72a0a --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/run_target.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "description": "Defines a reusable type that specifies configuration parameters for a Run Target.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a user-defined description of the Run Target." + }, + + "depends_on": { + "type": "array", + "description": "Specifies the names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Specifies the name of a component or Run Target that this Run Target depends on." + } + }, + + "transition_timeout": { + "type": "number", + "description": "Specifies the time limit for the Run Target transition. If this limit is exceeded, the transition is considered failed.", + "exclusiveMinimum": 0 + }, + + "recovery_action": { + "allOf": [ + { "$ref": "./recovery_action.schema.json" }, + { + "properties": { + "switch_run_target": true + }, + "required": ["switch_run_target"], + "not": { + "required": ["restart"] + } + } + ], + "description": "Specifies the recovery action to execute when a component assigned to this Run Target fails." + } + }, + + "required": [], + "additionalProperties": false +} diff --git a/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/watchdog.schema.json b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/watchdog.schema.json new file mode 100755 index 00000000..a7716025 --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/draft_schema/types/watchdog.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "description": "Defines a reusable type that contains configuration parameters for the external watchdog.", + "properties": { + "device_file_path": { + "type": "string", + "description": "Specifies the path to the external watchdog device file (e.g., /dev/watchdog)." + }, + "max_timeout": { + "type": "number", + "minimum": 0, + "description": "Specifies the maximum timeout value that the Launch Manager configures on the external watchdog during startup. The external watchdog uses this timeout as the deadline for receiving periodic alive reports from the Launch Manager." + }, + "deactivate_on_shutdown": { + "type": "boolean", + "description": "Specifies whether the Launch Manager disables the external watchdog during shutdown. When set to true, the watchdog is deactivated; when false, it remains active." + }, + "require_magic_close": { + "type": "boolean", + "description": "Specifies whether the Launch Manager performs a defined shutdown sequence to inform the external watchdog that the shutdown is intentional and to prevent a watchdog-initiated reset. When true, the magic close sequence is performed; when false, it is not." + } + }, + + "required": [], + "additionalProperties": false +} diff --git a/src/launch_manager_daemon/config/future_config_schema/examples/example_conf.json b/src/launch_manager_daemon/config/future_config_schema/examples/example_conf.json new file mode 100755 index 00000000..ddb3bc7a --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/examples/example_conf.json @@ -0,0 +1,170 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0, + "delay_before_restart": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [], + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": [], + "process_arguments": [], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "depends_on": [], + "transition_timeout": 5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2.0, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + }, + "components": { + "setup_filesystem_sh": { + "description": "Script to mount partitions at the right directories", + "component_properties": { + "binary_name": "bin/setup_filesystem.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "process_arguments": ["-a", "-b"], + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts" + } + }, + "dlt-daemon": { + "description": "Logging application", + "component_properties": { + "binary_name": "dltd", + "application_profile": { + "application_type": "Native" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/dlt-daemon" + } + }, + "someip-daemon": { + "description": "SOME/IP application", + "component_properties": { + "binary_name": "someipd" + }, + "deployment_config": { + "bin_dir" : "/opt/apps/someip" + } + }, + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1", + "depends_on": ["dlt-daemon", "someip-daemon"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + }, + "state_manager": { + "description": "Application that manages life cycle of the ECU", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "State_Manager" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager" + } + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["test_app1", "Minimal"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Minimal" + } + } + }, + "Off": { + "description": "Nothing is running", + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "initial_run_target": "Minimal", + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } +} diff --git a/src/launch_manager_daemon/config/future_config_schema/published_schema/s-core_launch_manager.schema.json b/src/launch_manager_daemon/config/future_config_schema/published_schema/s-core_launch_manager.schema.json new file mode 100644 index 00000000..7a2c547d --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/published_schema/s-core_launch_manager.schema.json @@ -0,0 +1,491 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Configuration schema for the S-CORE Launch Manager", + "description": "Defines the structure and valid values for the Launch Manager configuration file, which specifies managed components, run targets, and recovery behaviors.", + "$defs": { + "component_properties": { + "type": "object", + "description": "Defines a reusable type that captures essential development-time characteristics of a software component.", + "properties": { + "binary_name": { + "type": "string", + "description": "Specifies the relative path of the executable file inside the directory defined by 'deployment_config.bin_dir'. The final executable path will be resolved as '{bin_dir}/{binary_name}'. Example values include simple filenames (e.g., 'test_app1') or subdirectory paths (e.g., 'bin/test_app1')." + }, + "application_profile": { + "type": "object", + "description": "Defines the application profile that specifies the runtime behavior and capabilities of this component.", + "properties": { + "application_type": { + "type": "string", + "enum": [ + "Native", + "Reporting", + "Reporting_And_Supervised", + "State_Manager" + ], + "description": "Specifies the level of integration between the component and the Launch Manager. 'Native': no integration with Launch Manager. 'Reporting': uses Launch Manager lifecycle APIs. 'Reporting_And_Supervised': uses lifecycle APIs and sends alive notifications. 'State_Manager': uses lifecycle APIs, sends alive notifications, and has permission to change the active Run Target." + }, + "is_self_terminating": { + "type": "boolean", + "description": "Indicates whether the component is designed to terminate automatically once its planned tasks are completed (true), or remain running until explicitly requested to terminate by the Launch Manager (false)." + }, + "alive_supervision": { + "type": "object", + "description": "Defines the configuration parameters used for alive monitoring of the component.", + "properties": { + "reporting_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the duration of the time interval used to verify that the component sends alive notifications within the expected time frame." + }, + "failed_cycles_tolerance": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of consecutive reporting cycle failures (see 'reporting_cycle'). Once the number of failed cycles exceeds this maximum, the Launch Manager will trigger the configured recovery action." + }, + "min_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the minimum number of checkpoints that must be reported within each configured 'reporting_cycle'." + }, + "max_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of checkpoints that may be reported within each configured 'reporting_cycle'." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "depends_on": { + "type": "array", + "description": "Specifies the names of components that this component depends on. Each dependency must be initialized and reach its ready state before this component can start.", + "items": { + "type": "string", + "description": "Specifies the name of a component on which this component depends." + } + }, + "process_arguments": { + "type": "array", + "description": "Specifies an ordered list of command-line arguments passed to the component at startup.", + "items": { + "type": "string", + "description": "Specifies a single command-line argument token as a UTF-8 string; order is preserved." + } + }, + "ready_condition": { + "type": "object", + "description": "Defines the set of conditions that determine when the component completes its initializing state and enters the ready state.", + "properties": { + "process_state": { + "type": "string", + "enum": [ + "Running", + "Terminated" + ], + "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "recovery_action": { + "type": "object", + "description": "Defines a reusable type that specifies recovery actions to execute when an error or failure occurs.", + "properties": { + "restart": { + "type": "object", + "description": "Defines a recovery action that restarts the POSIX process associated with this component.", + "properties": { + "number_of_attempts": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of restart attempts before the Launch Manager concludes that recovery cannot succeed." + }, + "delay_before_restart": { + "type": "number", + "minimum": 0, + "description": "Specifies the delay duration that the Launch Manager waits before initiating a restart attempt." + } + }, + "required": [], + "additionalProperties": false + }, + "switch_run_target": { + "type": "object", + "description": "Defines a recovery action that switches to a Run Target. This can be a different Run Target or the same one to retry activation of the current Run Target.", + "properties": { + "run_target": { + "type": "string", + "description": "Specifies the name of the Run Target that the Launch Manager should switch to." + } + }, + "required": [], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "restart" + ] + }, + { + "required": [ + "switch_run_target" + ] + } + ], + "additionalProperties": false + }, + "deployment_config": { + "type": "object", + "description": "Defines a reusable type that contains configuration parameters that are specific to a particular deployment environment or system setup.", + "properties": { + "ready_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time allowed for the component to reach its ready state. The timeout is measured from when the component process is created until the ready conditions specified in 'component_properties.ready_condition' are met." + }, + "shutdown_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time allowed for the component to terminate after it receives a SIGTERM signal from the Launch Manager. The timeout is measured from when the Launch Manager sends the SIGTERM signal until the Operating System notifies the Launch Manager that the child process has terminated." + }, + "environmental_variables": { + "type": "object", + "description": "Defines the set of environment variables passed to the component at startup.", + "additionalProperties": { + "type": "string", + "description": "Specifies the environment variable's value as a string. An empty string is allowed and represents an intentionally empty environment variable." + } + }, + "bin_dir": { + "type": "string", + "description": "Specifies the absolute filesystem path to the directory where the component is installed." + }, + "working_dir": { + "type": "string", + "description": "Specifies the directory used as the working directory for the component during execution." + }, + "ready_recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "restart": true + }, + "required": [ + "restart" + ], + "not": { + "required": [ + "switch_run_target" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component fails to reach its ready state within the configured timeout." + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component malfunctions after reaching its ready state." + }, + "sandbox": { + "type": "object", + "description": "Defines the sandbox configuration parameters that isolate and constrain the component's runtime execution.", + "properties": { + "uid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the POSIX user ID (UID) under which this component executes." + }, + "gid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the primary POSIX group ID (GID) under which this component executes." + }, + "supplementary_group_ids": { + "type": "array", + "description": "Specifies the list of supplementary POSIX group IDs (GIDs) assigned to this component.", + "items": { + "type": "integer", + "minimum": 0, + "description": "Specifies a single supplementary POSIX group ID (GID)." + } + }, + "security_policy": { + "type": "string", + "description": "Specifies the security policy or confinement profile name (such as an SELinux or AppArmor profile) assigned to the component." + }, + "scheduling_policy": { + "type": "string", + "description": "Specifies the scheduling policy applied to the component's initial thread. Supported values correspond to OS-defined policies (e.g., FIFO, RR, OTHER).", + "anyOf": [ + { + "enum": [ + "SCHED_FIFO", + "SCHED_RR", + "SCHED_OTHER" + ] + }, + { + "type": "string" + } + ] + }, + "scheduling_priority": { + "type": "integer", + "description": "Specifies the scheduling priority applied to the component's initial thread." + }, + "max_memory_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum amount of memory, in bytes, that the component is permitted to use during runtime." + }, + "max_cpu_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage of total CPU capacity." + } + }, + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_target": { + "type": "object", + "description": "Defines a reusable type that specifies configuration parameters for a Run Target.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a user-defined description of the Run Target." + }, + "depends_on": { + "type": "array", + "description": "Specifies the names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Specifies the name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Specifies the time limit for the Run Target transition. If this limit is exceeded, the transition is considered failed.", + "exclusiveMinimum": 0 + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when a component assigned to this Run Target fails." + } + }, + "required": [], + "additionalProperties": false + }, + "alive_supervision": { + "type": "object", + "description": "Defines a reusable type that contains configuration parameters for alive supervision.", + "properties": { + "evaluation_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the length of the time window used to assess incoming alive supervision reports." + } + }, + "required": [], + "additionalProperties": false + }, + "watchdog": { + "type": "object", + "description": "Defines a reusable type that contains configuration parameters for the external watchdog.", + "properties": { + "device_file_path": { + "type": "string", + "description": "Specifies the path to the external watchdog device file (e.g., /dev/watchdog)." + }, + "max_timeout": { + "type": "number", + "minimum": 0, + "description": "Specifies the maximum timeout value that the Launch Manager configures on the external watchdog during startup. The external watchdog uses this timeout as the deadline for receiving periodic alive reports from the Launch Manager." + }, + "deactivate_on_shutdown": { + "type": "boolean", + "description": "Specifies whether the Launch Manager disables the external watchdog during shutdown. When set to true, the watchdog is deactivated; when false, it remains active." + }, + "require_magic_close": { + "type": "boolean", + "description": "Specifies whether the Launch Manager performs a defined shutdown sequence to inform the external watchdog that the shutdown is intentional and to prevent a watchdog-initiated reset. When true, the magic close sequence is performed; when false, it is not." + } + }, + "required": [], + "additionalProperties": false + } + }, + "properties": { + "schema_version": { + "type": "integer", + "description": "Specifies the schema version number that the Launch Manager uses to determine how to parse and validate this configuration file.", + "enum": [ + 1 + ] + }, + "defaults": { + "type": "object", + "description": "Defines default configuration values that components and Run Targets inherit unless they provide their own overriding values.", + "properties": { + "component_properties": { + "description": "Defines default component property values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Defines default deployment configuration values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/deployment_config" + }, + "run_target": { + "description": "Defines default Run Target configuration values applied to all Run Targets unless overridden in individual Run Target definitions.", + "$ref": "#/$defs/run_target" + }, + "alive_supervision": { + "description": "Defines default alive supervision configuration values used unless a global 'alive_supervision' configuration is specified at the root level.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdog": { + "description": "Defines default watchdog configuration values applied to all watchdogs unless overridden in individual watchdog definitions.", + "$ref": "#/$defs/watchdog" + } + }, + "required": [], + "additionalProperties": false + }, + "components": { + "type": "object", + "description": "Defines software components managed by the Launch Manager, where each property name is a unique component identifier and its value contains the component's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "description": "Defines an individual component's configuration properties and deployment settings.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a human-readable description of the component's purpose." + }, + "component_properties": { + "description": "Defines component properties for this component; any properties not specified here are inherited from 'defaults.component_properties'.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Defines deployment configuration for this component; any properties not specified here are inherited from 'defaults.deployment_config'.", + "$ref": "#/$defs/deployment_config" + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_targets": { + "type": "object", + "description": "Defines Run Targets representing different operational modes of the system, where each property name is a unique Run Target identifier and its value contains the Run Target's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "$ref": "#/$defs/run_target" + } + }, + "required": [], + "additionalProperties": false + }, + "initial_run_target": { + "type": "string", + "description": "Specifies the name of the initial Run Target that the Launch Manager activates during the startup sequence. This name must match a Run Target defined in 'run_targets'." + }, + "fallback_run_target": { + "type": "object", + "description": "Defines the fallback Run Target configuration that the Launch Manager activates when all recovery attempts have been exhausted. This Run Target does not include a recovery_action property.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a human-readable description of the fallback Run Target." + }, + "depends_on": { + "type": "array", + "description": "Specifies the names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Specifies the name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Specifies the time limit for the Run Target transition. If this limit is exceeded, the transition is considered failed.", + "exclusiveMinimum": 0 + } + }, + "required": [ + "depends_on" + ], + "additionalProperties": false + }, + "alive_supervision": { + "description": "Defines the alive supervision configuration parameters used to monitor component health. This configuration overrides 'defaults.alive_supervision' if specified.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdog": { + "description": "Defines the external watchdog device configuration used by the Launch Manager. This configuration overrides 'defaults.watchdog' if specified.", + "$ref": "#/$defs/watchdog" + } + }, + "required": [ + "schema_version", + "initial_run_target" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/launch_manager_daemon/config/future_config_schema/scripts/bundle.py b/src/launch_manager_daemon/config/future_config_schema/scripts/bundle.py new file mode 100755 index 00000000..d5dc26f3 --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/scripts/bundle.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +""" +Bundle a multi-file JSON Schema into a single file using $defs. + +Usage: + python scripts/bundle_defs.py \ + --input config/schema/s-core-launch-manager.schema.json \ + --output config/schema/s-core-launch-manager.defs.bundle.json + + +Notes: +- Instead of fully inlining (flattening) each $ref, this script: + 1) Collects every local file $ref (e.g. "./types/*.schema.json"). + 2) Imports each referenced schema once into the top-level "$defs" (deduplicated). + 3) Rewrites $ref values to point to "#/$defs/" (JSON Pointer fragments like "#/foo/bar" are preserved by converting) + * For example: "file.json#/foo/bar" -> "#/$defs//foo/bar". +- If the imported schema itself has local file refs, those are also pulled into $defs recursively with the same logic. +- Name derivation for $defs keys: file basename without ".schema.json" (e.g., "watchdog"). Collisions are resolved by appending a numeric suffix (e.g., watchdog_2). +- To avoid base-URI confusion inside bundled defs, $id and nested $schema are stripped from the imported defs. +""" + +from __future__ import annotations +import argparse +import json +from pathlib import Path +from typing import Any, Dict, Tuple + +Json = Any + + +class DefsBundler: + def __init__(self, input_file: Path) -> None: + self.input_file = input_file.resolve() + self.defs: Dict[str, Json] = {} + self.file_to_defname: Dict[Path, str] = {} + + @staticmethod + def _deepcopy(obj: Json) -> Json: + return json.loads(json.dumps(obj)) + + @staticmethod + def _is_local_file_ref(ref: str) -> bool: + if not isinstance(ref, str): + return False + if ref.startswith("#"): + return False + if "://" in ref: + return False + return True + + @staticmethod + def _split_ref(ref: str) -> Tuple[str, str]: + # Returns (file_part, fragment_pointer) where fragment_pointer is like "/a/b" (without leading '#') or '' + if "#" in ref: + file_part, frag = ref.split("#", 1) + if frag.startswith("/"): + return file_part, frag # JSON Pointer already + if frag.startswith("#/"): + return file_part, frag[1:] + # treat unknown as JSON Pointer missing leading '/' + return file_part, "/" + frag if frag else "" + return ref, "" + + @staticmethod + def _derive_name_from_file(path: Path) -> str: + name = path.stem # e.g., "watchdog.schema" + if name.endswith(".schema"): + name = name[:-7] # remove trailing ".schema" + return name + + def _unique_def_name(self, base: str) -> str: + if base not in self.defs: + return base + i = 2 + while True: + cand = f"{base}_{i}" + if cand not in self.defs: + return cand + i += 1 + + def _strip_ids(self, schema: Json) -> Json: + # Remove $id and nested $schema fields from imported defs to avoid base-URI conflicts + if isinstance(schema, dict): + schema = { + k: self._strip_ids(v) + for k, v in schema.items() + if k not in ("$id", "$schema") + } + elif isinstance(schema, list): + schema = [self._strip_ids(v) for v in schema] + return schema + + def _register_def_from_file(self, current_file: Path, ref_path: str) -> str: + target = (current_file.parent / ref_path).resolve() + if target in self.file_to_defname: + return self.file_to_defname[target] + with open(target, "r", encoding="utf-8") as f: + schema = json.load(f) + name_base = self._derive_name_from_file(target) + name = self._unique_def_name(name_base) + cleaned = self._strip_ids(schema) + # Before storing, rewrite refs inside this imported schema + rewritten = self._rewrite_refs(cleaned, target) + self.defs[name] = rewritten + self.file_to_defname[target] = name + return name + + def _rewrite_refs(self, node: Json, current_file: Path) -> Json: + # Traverse node; for any local file $ref, add that file into $defs and rewrite the $ref to #/$defs//fragment + if isinstance(node, dict): + if "$ref" in node and isinstance(node["$ref"], str): + ref_str = node["$ref"] + if self._is_local_file_ref(ref_str): + file_part, frag = self._split_ref(ref_str) + defname = ( + self._register_def_from_file(current_file, file_part) + if file_part + else None + ) + if defname: + # Compose new JSON Pointer: #/$defs/ + pointer = f"#/$defs/{defname}{frag}" + return { + **{k: v for k, v in node.items() if k != "$ref"}, + "$ref": pointer, + } + # Otherwise, descend + return {k: self._rewrite_refs(v, current_file) for k, v in node.items()} + elif isinstance(node, list): + return [self._rewrite_refs(v, current_file) for v in node] + else: + return node + + def bundle(self, out_path: Path) -> None: + with open(self.input_file, "r", encoding="utf-8") as f: + root = json.load(f) + + bundled_root = self._deepcopy(root) + + # Rewrite refs in the root + bundled_root = self._rewrite_refs(bundled_root, self.input_file) + + # Merge with existing $defs if any + existing_defs = ( + bundled_root.get("$defs", {}) if isinstance(bundled_root, dict) else {} + ) + if not isinstance(existing_defs, dict): + existing_defs = {} + + merged_defs: Dict[str, Json] = {} + + # Copy existing defs + for k, v in existing_defs.items(): + merged_defs[k] = v + + # Add generated defs (dedupe) + for k, v in self.defs.items(): + kk = k + ctr = 2 + while kk in merged_defs: + kk = f"{k}_{ctr}" + ctr += 1 + merged_defs[kk] = v + + # ---------- force a specific order in generated schema ---------- + if isinstance(bundled_root, dict): + new_root = {} + + # The "$schema", "type", "$id", "title", and "description" elements should be at the top of the file + for key in ["$schema", "type", "$id", "title", "description"]: + if key in bundled_root: + new_root[key] = bundled_root[key] + + # Then insert "$defs" (before "properties") + new_root["$defs"] = merged_defs + + # Insert "properties" immediately after "$defs" + if "properties" in bundled_root: + new_root["properties"] = bundled_root["properties"] + + # Insert remaining keys (required, additionalProperties, etc.) + for key, value in bundled_root.items(): + if key not in new_root: + new_root[key] = value + + bundled_root = new_root + # ---------------------------------------------------------------- + + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w", encoding="utf-8") as f: + json.dump(bundled_root, f, indent=2, ensure_ascii=False) + + +def main() -> None: + ap = argparse.ArgumentParser( + description="Bundle multi-file JSON Schema into one file using $defs (deduplicated)." + ) + ap.add_argument("--input", required=True, help="Path to the top-level schema JSON") + ap.add_argument( + "--output", required=True, help="Path to write the bundled schema JSON" + ) + args = ap.parse_args() + + bundler = DefsBundler(Path(args.input)) + bundler.bundle(Path(args.output)) + print(f"Bundled schema written to: {args.output}") + + +if __name__ == "__main__": + main() diff --git a/src/launch_manager_daemon/config/future_config_schema/scripts/validate.py b/src/launch_manager_daemon/config/future_config_schema/scripts/validate.py new file mode 100755 index 00000000..f34418be --- /dev/null +++ b/src/launch_manager_daemon/config/future_config_schema/scripts/validate.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +""" +Validate JSON instance(s) against a multi-file JSON Schema with relative $ref paths. + +Usage examples: + Validate a single file: + python validate_config.py --schema ./schema/s-core_launch_manager.schema.json --instance ./examples/config.json + + Validate all JSON files in a directory (recursively): + python validate_config.py --schema ./schema/s-core_launch_manager.schema.json --instances-dir ./examples + +Exit codes: + 0 -> all instances are valid + 1 -> at least one instance failed validation or there was an error +""" + +import argparse +import json +import sys +from pathlib import Path + +try: + from jsonschema import validators, RefResolver, FormatChecker + from jsonschema.exceptions import RefResolutionError, SchemaError, ValidationError +except ImportError: + print( + "This script requires the 'jsonschema' package. Install with:\n pip install jsonschema", + file=sys.stderr, + ) + sys.exit(1) + + +def load_json(path: Path): + try: + with path.open("r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON file '{path}': {e}") from e + except OSError as e: + raise ValueError(f"Failed to read file '{path}': {e}") from e + + +def json_pointer_path(parts): + """ + Convert an error path into a friendly string like: + $.topLevel.items[2].name + """ + if not parts: + return "$" + s = "$" + for p in parts: + if isinstance(p, int): + s += f"[{p}]" + else: + s += f".{p}" + return s + + +def build_validator(schema_path: Path): + schema = load_json(schema_path) + + # Choose validator based on $schema automatically (Draft-07 / 2019-09 / 2020-12, etc.) + ValidatorClass = validators.validator_for(schema) + # Validate the schema itself (optional but helpful) + try: + ValidatorClass.check_schema(schema) + except SchemaError as e: + raise SchemaError( + f"Your schema appears invalid: {e.message}\nAt: {'/'.join(map(str, e.path))}" + ) from e + + # Base URI for resolving relative $refs like "./types/*.schema.json" + base_uri = schema_path.resolve().parent.as_uri() + "/" + + # RefResolver is deprecated upstream but still widely supported and reliable for local file resolution. + resolver = RefResolver(base_uri=base_uri, referrer=schema) + + # Enable common format checks (e.g., "uri", "email", "date-time") + format_checker = FormatChecker() + + return ValidatorClass(schema, resolver=resolver, format_checker=format_checker) + + +def validate_instance(validator, instance_path: Path): + instance = load_json(instance_path) + errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path)) + return errors + + +def find_json_files(root: Path): + # Recurse and pick *.json files only + return [p for p in root.rglob("*.json") if p.is_file()] + + +def main(): + parser = argparse.ArgumentParser( + description="Validate JSON instance(s) against a multi-file JSON Schema." + ) + parser.add_argument( + "--schema", + required=True, + type=Path, + help="Path to the top-level schema (e.g., ./schema/s-core_launch_manager.schema.json)", + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--instance", type=Path, help="Path to a single JSON instance to validate" + ) + group.add_argument( + "--instances-dir", + type=Path, + help="Path to a directory containing JSON instances (recursively)", + ) + parser.add_argument( + "--stop-on-first", + action="store_true", + help="Stop after the first instance with errors", + ) + args = parser.parse_args() + + try: + validator = build_validator(args.schema) + except (ValueError, SchemaError) as e: + print(f"[Schema Error] {e}", file=sys.stderr) + sys.exit(1) + + instance_paths = [] + if args.instance: + instance_paths = [args.instance] + else: + if not args.instances_dir.exists(): + print( + f"[Error] Instances directory not found: {args.instances_dir}", + file=sys.stderr, + ) + sys.exit(1) + instance_paths = find_json_files(args.instances_dir) + if not instance_paths: + print(f"[Info] No JSON files found under: {args.instances_dir}") + sys.exit(0) + + any_failed = False + for path in instance_paths: + try: + errors = validate_instance(validator, path) + except ValueError as e: + print(f"Error --> {path}: {e}", file=sys.stderr) + any_failed = True + if args.stop_on_first: + break + continue + except RefResolutionError as e: + print(f"Error --> {path}: Failed to resolve a $ref - {e}", file=sys.stderr) + print(" Tips:") + print( + " * Ensure $ref paths like './types/...' are correct relative to the top-level schema file." + ) + print(" * Make sure referenced files exist and are valid JSON schemas.") + any_failed = True + if args.stop_on_first: + break + continue + + if not errors: + print(f"Success --> {path}: valid") + else: + any_failed = True + print(f"Error --> {path}: {len(errors)} error(s)") + for i, err in enumerate(errors, 1): + instance_loc = json_pointer_path(err.path) + schema_loc = ( + "/".join(map(str, err.schema_path)) if err.schema_path else "(root)" + ) + print(f" [{i}] at {instance_loc}") + print(f" --> {err.message}") + print(f" schema path: {schema_loc}") + if args.stop_on_first: + break + + sys.exit(1 if any_failed else 0) + + +if __name__ == "__main__": + main()