diff --git a/src/middlewared/middlewared/plugins/apps/crud.py b/src/middlewared/middlewared/plugins/apps/crud.py index 8450dc8493716..1c79bd8f511b2 100644 --- a/src/middlewared/middlewared/plugins/apps/crud.py +++ b/src/middlewared/middlewared/plugins/apps/crud.py @@ -18,6 +18,7 @@ from .ix_apps.path import get_installed_app_path, get_installed_app_version_path from .ix_apps.query import list_apps from .ix_apps.setup import setup_install_app_dir +from .ix_apps.utils import AppState from .version_utils import get_latest_version_from_app_versions @@ -33,7 +34,7 @@ class Config: 'app_entry', Str('name'), Str('id'), - Str('state', enum=['STOPPED', 'DEPLOYING', 'RUNNING']), + Str('state', enum=[state.value for state in AppState]), Bool('upgrade_available'), Str('human_version'), Str('version'), diff --git a/src/middlewared/middlewared/plugins/apps/ix_apps/query.py b/src/middlewared/middlewared/plugins/apps/ix_apps/query.py index e0cdd959d9447..73c731dcfcf00 100644 --- a/src/middlewared/middlewared/plugins/apps/ix_apps/query.py +++ b/src/middlewared/middlewared/plugins/apps/ix_apps/query.py @@ -6,7 +6,7 @@ from .metadata import get_collective_config, get_collective_metadata from .lifecycle import get_current_app_config from .path import get_app_parent_config_path -from .utils import get_app_name_from_project_name, normalize_reference, PROJECT_PREFIX +from .utils import AppState, ContainerState, get_app_name_from_project_name, normalize_reference, PROJECT_PREFIX COMPOSE_SERVICE_KEY: str = 'com.docker.compose.service' @@ -86,12 +86,18 @@ def list_apps( # When we stop docker service and start it again - the containers can be in exited # state which means we need to account for this. state = 'STOPPED' + exited_containers = 0 for container in workloads['container_details']: - if container['state'] == 'starting': - state = 'DEPLOYING' + if container['state'] == ContainerState.STARTING.value: + state = AppState.DEPLOYING.value break - elif container['state'] == 'running': - state = 'RUNNING' + elif container['state'] == ContainerState.RUNNING.value: + state = AppState.RUNNING.value + elif container['state'] == ContainerState.EXITED.value: + exited_containers += 1 + else: + if exited_containers != 0 and exited_containers == len(workloads['container_details']): + state = AppState.CRASHED.value app_metadata = metadata[app_name] active_workloads = get_default_workload_values() if state == 'STOPPED' else workloads @@ -127,7 +133,7 @@ def list_apps( 'name': entry.name, 'id': entry.name, 'active_workloads': get_default_workload_values(), - 'state': 'STOPPED', + 'state': AppState.STOPPED.value, 'upgrade_available': upgrade_available_for_app(train_to_apps_version_mapping, app_metadata), 'image_updates_available': False, **app_metadata | {'portals': normalize_portal_uris(app_metadata['portals'], host_ip)} @@ -187,9 +193,12 @@ def translate_resources_to_desired_workflow(app_resources: dict) -> dict: if container['State']['Status'].lower() == 'running': if health_config := container['State'].get('Health'): - state = 'running' if health_config['Status'] == 'healthy' else 'starting' + if health_config['Status'] == 'healthy': + state = ContainerState.RUNNING.value + else: + state = ContainerState.STARTING.value else: - state = 'running' + state = ContainerState.RUNNING.value else: state = 'exited' diff --git a/src/middlewared/middlewared/plugins/apps/ix_apps/utils.py b/src/middlewared/middlewared/plugins/apps/ix_apps/utils.py index 31e868d8743e9..fbff0181e87eb 100644 --- a/src/middlewared/middlewared/plugins/apps/ix_apps/utils.py +++ b/src/middlewared/middlewared/plugins/apps/ix_apps/utils.py @@ -1,8 +1,23 @@ +import enum + from catalog_reader.library import RE_VERSION # noqa from middlewared.plugins.apps_images.utils import normalize_reference # noqa from middlewared.plugins.apps.schema_utils import CONTEXT_KEY_NAME # noqa from middlewared.plugins.apps.utils import IX_APPS_MOUNT_PATH, PROJECT_PREFIX, run # noqa +class AppState(enum.Enum): + CRASHED = 'CRASHED' + DEPLOYING = 'DEPLOYING' + RUNNING = 'RUNNING' + STOPPED = 'STOPPED' + + +class ContainerState(enum.Enum): + EXITED = 'exited' + RUNNING = 'running' + STARTING = 'starting' + + def get_app_name_from_project_name(project_name: str) -> str: return project_name[len(PROJECT_PREFIX):] diff --git a/src/middlewared/middlewared/plugins/apps/logs.py b/src/middlewared/middlewared/plugins/apps/logs.py index 3cafc9a4248c5..cc16c34fff0cb 100644 --- a/src/middlewared/middlewared/plugins/apps/logs.py +++ b/src/middlewared/middlewared/plugins/apps/logs.py @@ -8,6 +8,7 @@ from middlewared.service import CallError from middlewared.validators import Range +from .ix_apps.utils import AppState from .ix_apps.docker.utils import get_docker_client @@ -38,8 +39,8 @@ def __init__(self, *args, **kwargs): def validate_log_args(self, app_name, container_id): app = self.middleware.call_sync('app.get_instance', app_name) - if app['state'] not in ('RUNNING', 'DEPLOYING'): - raise CallError(f'App "{app_name}" is not running') + if app['state'] not in (AppState.CRASHED.value, AppState.RUNNING.value, AppState.DEPLOYING.value): + raise CallError(f'Unable to retrieve logs of stopped {app_name!r} app') if not any(c['id'] == container_id for c in app['active_workloads']['container_details']): raise CallError(f'Container "{container_id}" not found in app "{app_name}"', errno=errno.ENOENT) diff --git a/src/middlewared/middlewared/plugins/apps/resources.py b/src/middlewared/middlewared/plugins/apps/resources.py index e0fca5b96fd77..728377478391f 100644 --- a/src/middlewared/middlewared/plugins/apps/resources.py +++ b/src/middlewared/middlewared/plugins/apps/resources.py @@ -3,6 +3,7 @@ from middlewared.utils.gpu import get_nvidia_gpus +from .ix_apps.utils import ContainerState from .resources_utils import get_normalized_gpu_choices @@ -43,7 +44,9 @@ async def container_ids(self, app_name, options): 'id': c['id'], } for c in ( await self.middleware.call('app.get_instance', app_name) - )['active_workloads']['container_details'] if (options['alive_only'] is False or c['state'] == 'running') + )['active_workloads']['container_details'] if ( + options['alive_only'] is False or ContainerState(c['state']) == ContainerState.RUNNING + ) } @accepts(Str('app_name'), roles=['APPS_READ'])