diff --git a/tdp/cli/commands/status/generate_stales.py b/tdp/cli/commands/status/generate_stales.py index 9808905b..78cdc1ee 100644 --- a/tdp/cli/commands/status/generate_stales.py +++ b/tdp/cli/commands/status/generate_stales.py @@ -43,8 +43,8 @@ def generate_stales( Stales components are components that have been modified and need to be reconfigured and/or restarted. """ - from tdp.cli.utils import check_services_cleanliness, print_hosted_entity_status_log + from tdp.core.exceptions import ServiceVariablesNotInitializedErrorList from tdp.core.variables import ClusterVariables from tdp.dao import Dao @@ -54,9 +54,13 @@ def generate_stales( check_services_cleanliness(cluster_variables) with Dao(db_engine) as dao: - stale_status_logs = dao.get_cluster_status().generate_stale_sch_logs( - cluster_variables=cluster_variables, collections=collections - ) + try: + stale_status_logs = dao.get_cluster_status().generate_stale_sch_logs( + cluster_variables=cluster_variables, collections=collections + ) + except ServiceVariablesNotInitializedErrorList as e: + click.echo(str(e)) + click.echo("Their status will not be updated.") dao.session.add_all(stale_status_logs) dao.session.commit() diff --git a/tdp/cli/commands/vars/edit.py b/tdp/cli/commands/vars/edit.py index 1956daa8..8d46f981 100644 --- a/tdp/cli/commands/vars/edit.py +++ b/tdp/cli/commands/vars/edit.py @@ -65,6 +65,7 @@ def edit( ServiceComponentName, parse_entity_name, ) + from tdp.core.exceptions import ServiceVariablesNotInitializedErrorList from tdp.core.repository.repository import EmptyCommit from tdp.core.variables import ClusterVariables from tdp.core.variables.schema.exceptions import InvalidSchemaError @@ -147,9 +148,13 @@ def edit( # Generate stale component list and save it to the database with Dao(db_engine) as dao: - stale_status_logs = dao.get_cluster_status().generate_stale_sch_logs( - cluster_variables=cluster_variables, collections=collections - ) + try: + stale_status_logs = dao.get_cluster_status().generate_stale_sch_logs( + cluster_variables=cluster_variables, collections=collections + ) + except ServiceVariablesNotInitializedErrorList as e: + click.echo(str(e)) + click.echo("Their status will not be updated.") dao.session.add_all(stale_status_logs) dao.session.commit() diff --git a/tdp/cli/commands/vars/update.py b/tdp/cli/commands/vars/update.py index f598b858..6cbbdf6c 100644 --- a/tdp/cli/commands/vars/update.py +++ b/tdp/cli/commands/vars/update.py @@ -55,10 +55,8 @@ def update( ): """Update configuration from the given directories.""" - from tdp.core.variables.cluster_variables import ( - ClusterVariables, - ServicesNotInitializedError, - ) + from tdp.core.exceptions import ServiceVariablesNotInitializedErrorList + from tdp.core.variables.cluster_variables import ClusterVariables from tdp.core.variables.exceptions import ServicesUpdateError from tdp.dao import Dao @@ -71,14 +69,8 @@ def update( base_validation_msg=msg, ) # Stop the update process if some services are not initialized - except ServicesNotInitializedError as e: - error_messages = "\n".join( - f"{error.service_name} (from {error.source_definition})" - for error in e.services - ) - raise click.ClickException( - f"The following services are not initialized:\n{error_messages}" - ) from e + except ServiceVariablesNotInitializedErrorList as e: + raise click.ClickException(str(e)) from e # Do not stop the update as some services may have been updated successfully except ServicesUpdateError as e: error_messages = "\n".join( diff --git a/tdp/core/exceptions.py b/tdp/core/exceptions.py new file mode 100644 index 00000000..5b94cbc2 --- /dev/null +++ b/tdp/core/exceptions.py @@ -0,0 +1,35 @@ +# Copyright 2025 TOSIT.IO +# SPDX-License-Identifier: Apache-2.0 + + +from typing import Optional + + +class ServiceVariablesNotInitializedError(Exception): + def __init__(self, service_name: str, source: Optional[str] = None): + super().__init__( + f"Variables for service '{service_name}' have not been initialized." + ) + self.name = service_name + self.source = source + + def as_list_item(self) -> str: + """Return a string representation of the error for listing.""" + return f"{self.name}" + (f" (from {self.source})" if self.source else "") + + +class ServiceVariablesNotInitializedErrorList(Exception): + base_msg = "The following services are not initialized:" + + def __init__(self, errors: list[ServiceVariablesNotInitializedError]): + super().__init__( + self.base_msg + " " + ", ".join(e.as_list_item() for e in errors) + ) + self.errors = errors + + def __str__(self): + return ( + self.base_msg + + "\n" + + "\n".join(f"- {e.as_list_item()}" for e in self.errors) + ) diff --git a/tdp/core/variables/cluster_variables.py b/tdp/core/variables/cluster_variables.py index dbcac8fb..3295d784 100644 --- a/tdp/core/variables/cluster_variables.py +++ b/tdp/core/variables/cluster_variables.py @@ -9,16 +9,18 @@ from pathlib import Path from typing import TYPE_CHECKING, Optional -from tdp.core.constants import DEFAULT_VALIDATION_MESSAGE, VALIDATION_MESSAGE_FILE +from tdp.core.constants import ( + DEFAULT_VALIDATION_MESSAGE, + VALIDATION_MESSAGE_FILE, +) +from tdp.core.exceptions import ( + ServiceVariablesNotInitializedError, + ServiceVariablesNotInitializedErrorList, +) from tdp.core.repository.git_repository import GitRepository from tdp.core.repository.repository import EmptyCommit, NoVersionYet, Repository from tdp.core.types import PathLike -from tdp.core.variables.exceptions import ( - ServicesNotInitializedError, - ServicesUpdateError, - UnknownService, - UpdateError, -) +from tdp.core.variables.exceptions import ServicesUpdateError, UpdateError from tdp.core.variables.messages import ValidationMessageBuilder from tdp.core.variables.planner import ServiceUpdatePlanner from tdp.core.variables.scanner import ServiceDirectoryScanner @@ -156,13 +158,17 @@ def update( (name, path) for name, path in self._collections.default_vars_dirs.items() ] + [("override", Path(p)) for p in override_folders] - unknown = [] + unknown: list[ServiceVariablesNotInitializedError] = [] for _, source_path in sources: for name, _ in ServiceDirectoryScanner.scan(source_path).items(): if name not in self: - unknown.append(UnknownService(name, source_path.as_posix())) + unknown.append( + ServiceVariablesNotInitializedError( + name, source_path.as_posix() + ) + ) if unknown: - raise ServicesNotInitializedError(unknown) + raise ServiceVariablesNotInitializedErrorList(unknown) validation_builder = ValidationMessageBuilder( self._collections, diff --git a/tdp/core/variables/exceptions.py b/tdp/core/variables/exceptions.py index 767eb9b1..49f37f21 100644 --- a/tdp/core/variables/exceptions.py +++ b/tdp/core/variables/exceptions.py @@ -4,27 +4,6 @@ from dataclasses import dataclass -@dataclass(frozen=True) -class UnknownService: - """Represents a service that is referenced but not initialized.""" - - service_name: str - source_definition: str - - -class ServicesNotInitializedError(Exception): - """Raised when some expected services are missing from the initialized set.""" - - def __init__(self, services: list[UnknownService]): - super().__init__( - "The following services are not initialized:\n" - + "\n".join( - f"{e.service_name} (from {e.source_definition})" for e in services - ) - ) - self.services = services - - @dataclass(frozen=True) class UpdateError: """Represents an error that occurred during a service update.""" diff --git a/tdp/core/variables/inspection.py b/tdp/core/variables/inspection.py index 7b6ca3a8..8ec58642 100644 --- a/tdp/core/variables/inspection.py +++ b/tdp/core/variables/inspection.py @@ -7,6 +7,11 @@ from collections.abc import Iterable from typing import TYPE_CHECKING +from tdp.core.exceptions import ( + ServiceVariablesNotInitializedError, + ServiceVariablesNotInitializedErrorList, +) + if TYPE_CHECKING: from tdp.core.entities.hosted_entity import HostedEntity from tdp.core.entities.hosted_entity_status import HostedEntityStatus @@ -31,16 +36,17 @@ def get_modified_entities( RuntimeError: If a service is deployed but its repository is missing. """ modified_entities: set[HostedEntity] = set() + not_initialized_services: set[ServiceVariablesNotInitializedError] = set() for status in entity_statuses: # Skip if the entity has already been listed as modified if status.entity in modified_entities: continue # Raise an error if the service is deployed but its repository is missing if status.entity.name.service not in cluster_variables: - raise RuntimeError( - f"Service '{status.entity.name.service}' is deployed but its" - + "repository is missing." + not_initialized_services.add( + ServiceVariablesNotInitializedError(status.entity.name.service) ) + continue # Check if the entity has been modified if status.configured_version and cluster_variables[ status.entity.name.service @@ -52,4 +58,6 @@ def get_modified_entities( + (f" for host {status.entity.host}" if status.entity.host else "") ) modified_entities.add(status.entity) + if not_initialized_services: + raise ServiceVariablesNotInitializedErrorList(list(not_initialized_services)) return modified_entities