Skip to content

Commit 068394c

Browse files
GitHKAndrei Neagu
andauthored
🐛 Fix issue with agent and volume permissions when backing up (#8214)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent cbf29d6 commit 068394c

File tree

9 files changed

+83
-47
lines changed

9 files changed

+83
-47
lines changed
Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
import asyncio
22
import logging
33
from collections.abc import Sequence
4-
from typing import Any
4+
from typing import Any, Final
55

66
from aiodocker import Docker, DockerError
77
from aiodocker.execs import Exec
88
from aiodocker.stream import Stream
9+
from common_library.errors_classes import OsparcErrorMixin
910
from pydantic import NonNegativeFloat
10-
from starlette import status
1111

12-
from ..core.errors import (
13-
ContainerExecCommandFailedError,
14-
ContainerExecContainerNotFoundError,
15-
ContainerExecTimeoutError,
16-
)
12+
13+
class BaseContainerUtilsError(OsparcErrorMixin, Exception):
14+
pass
15+
16+
17+
class ContainerExecContainerNotFoundError(BaseContainerUtilsError):
18+
msg_template = "Container '{container_name}' was not found"
19+
20+
21+
class ContainerExecTimeoutError(BaseContainerUtilsError):
22+
msg_template = "Timed out after {timeout} while executing: '{command}'"
23+
24+
25+
class ContainerExecCommandFailedError(BaseContainerUtilsError):
26+
msg_template = (
27+
"Command '{command}' exited with code '{exit_code}'"
28+
"and output: '{command_result}'"
29+
)
30+
31+
32+
_HTTP_404_NOT_FOUND: Final[int] = 404
1733

1834
_logger = logging.getLogger(__name__)
1935

@@ -77,10 +93,10 @@ async def run_command_in_container(
7793
_execute_command(container_name, command), timeout
7894
)
7995
except DockerError as e:
80-
if e.status == status.HTTP_404_NOT_FOUND:
96+
if e.status == _HTTP_404_NOT_FOUND:
8197
raise ContainerExecContainerNotFoundError(
8298
container_name=container_name
8399
) from e
84100
raise
85-
except asyncio.TimeoutError as e:
101+
except TimeoutError as e:
86102
raise ContainerExecTimeoutError(timeout=timeout, command=command) from e

services/dynamic-sidecar/tests/unit/test_modules_container_utils.py renamed to packages/service-library/tests/test_container_utils.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# pylint: disable=redefined-outer-name
22

3-
import contextlib
43
from collections.abc import AsyncIterable
54

65
import aiodocker
76
import pytest
8-
from simcore_service_dynamic_sidecar.modules.container_utils import (
7+
from servicelib.container_utils import (
98
ContainerExecCommandFailedError,
109
ContainerExecContainerNotFoundError,
1110
ContainerExecTimeoutError,
@@ -26,9 +25,7 @@ async def running_container_name() -> AsyncIterable[str]:
2625

2726
yield container_inspect["Name"][1:]
2827

29-
with contextlib.suppress(aiodocker.DockerError):
30-
await container.kill()
31-
await container.delete()
28+
await container.delete(force=True)
3229

3330

3431
async def test_run_command_in_container_container_not_found():

services/agent/src/simcore_service_agent/services/backup.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import asyncio
22
import logging
3+
import os
34
import tempfile
45
from asyncio.streams import StreamReader
6+
from datetime import timedelta
57
from pathlib import Path
68
from textwrap import dedent
79
from typing import Final
810
from uuid import uuid4
911

1012
from fastapi import FastAPI
13+
from servicelib.container_utils import run_command_in_container
1114
from settings_library.utils_r_clone import resolve_provider
1215

1316
from ..core.settings import ApplicationSettings
1417
from ..models.volumes import DynamicServiceVolumeLabels, VolumeDetails
1518

19+
_TIMEOUT_PERMISSION_CHANGES: Final[timedelta] = timedelta(minutes=5)
20+
1621
_logger = logging.getLogger(__name__)
1722

1823

@@ -107,6 +112,15 @@ def _log_expected_operation(
107112
_logger.log(log_level, formatted_message)
108113

109114

115+
async def _ensure_permissions_on_source_dir(source_dir: Path) -> None:
116+
self_container = os.environ["HOSTNAME"]
117+
await run_command_in_container(
118+
self_container,
119+
command=f"chmod -R o+rX '{source_dir}'",
120+
timeout=_TIMEOUT_PERMISSION_CHANGES.total_seconds(),
121+
)
122+
123+
110124
async def _store_in_s3(
111125
settings: ApplicationSettings, volume_name: str, volume_details: VolumeDetails
112126
) -> None:
@@ -148,6 +162,8 @@ async def _store_in_s3(
148162
volume_details.labels, s3_path, r_clone_ls_output, volume_name
149163
)
150164

165+
await _ensure_permissions_on_source_dir(source_dir)
166+
151167
# sync files via rclone
152168
r_clone_sync = [
153169
"rclone",

services/agent/tests/unit/test_services_backup.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
23

34
import asyncio
4-
from collections.abc import Awaitable, Callable
5+
from collections.abc import AsyncIterable, Awaitable, Callable
56
from pathlib import Path
67
from typing import Final
78
from uuid import uuid4
89

910
import aioboto3
11+
import aiodocker
1012
import pytest
1113
from fastapi import FastAPI
1214
from models_library.projects import ProjectID
@@ -37,6 +39,28 @@ def volume_content(tmpdir: Path) -> Path:
3739
return path
3840

3941

42+
@pytest.fixture
43+
async def mock_container_with_data(
44+
volume_content: Path, monkeypatch: pytest.MonkeyPatch
45+
) -> AsyncIterable[None]:
46+
async with aiodocker.Docker() as client:
47+
container = await client.containers.run(
48+
config={
49+
"Image": "alpine:latest",
50+
"Cmd": ["/bin/ash", "-c", "sleep 10000"],
51+
"HostConfig": {"Binds": [f"{volume_content}:{volume_content}:rw"]},
52+
}
53+
)
54+
container_inspect = await container.show()
55+
56+
container_name = container_inspect["Name"][1:]
57+
monkeypatch.setenv("HOSTNAME", container_name)
58+
59+
yield None
60+
61+
await container.delete(force=True)
62+
63+
4064
@pytest.fixture
4165
def downlaoded_from_s3(tmpdir: Path) -> Path:
4266
path = Path(tmpdir) / "downloaded_from_s3"
@@ -45,6 +69,7 @@ def downlaoded_from_s3(tmpdir: Path) -> Path:
4569

4670

4771
async def test_backup_volume(
72+
mock_container_with_data: None,
4873
volume_content: Path,
4974
project_id: ProjectID,
5075
swarm_stack_name: str,

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/rest/containers.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
ActivityInfoOrNone,
1515
)
1616
from pydantic import TypeAdapter, ValidationError
17-
from servicelib.fastapi.requests_decorators import cancel_on_disconnect
18-
19-
from ...core.docker_utils import docker_client
20-
from ...core.errors import (
17+
from servicelib.container_utils import (
2118
ContainerExecCommandFailedError,
2219
ContainerExecContainerNotFoundError,
2320
ContainerExecTimeoutError,
21+
run_command_in_container,
2422
)
23+
from servicelib.fastapi.requests_decorators import cancel_on_disconnect
24+
25+
from ...core.docker_utils import docker_client
2526
from ...core.settings import ApplicationSettings
2627
from ...core.validation import (
2728
ComposeSpecValidation,
@@ -30,7 +31,6 @@
3031
)
3132
from ...models.schemas.containers import ContainersComposeSpec
3233
from ...models.shared_store import SharedStore
33-
from ...modules.container_utils import run_command_in_container
3434
from ...modules.mounted_fs import MountedVolumes
3535
from ._dependencies import (
3636
get_container_restart_lock,

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/core/errors.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,3 @@ class VolumeNotFoundError(BaseDynamicSidecarError):
1414

1515
class UnexpectedDockerError(BaseDynamicSidecarError):
1616
msg_template = "An unexpected Docker error occurred status_code={status_code}, message={message}"
17-
18-
19-
class ContainerExecContainerNotFoundError(BaseDynamicSidecarError):
20-
msg_template = "Container '{container_name}' was not found"
21-
22-
23-
class ContainerExecTimeoutError(BaseDynamicSidecarError):
24-
msg_template = "Timed out after {timeout} while executing: '{command}'"
25-
26-
27-
class ContainerExecCommandFailedError(BaseDynamicSidecarError):
28-
msg_template = (
29-
"Command '{command}' exited with code '{exit_code}'"
30-
"and output: '{command_result}'"
31-
)

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/long_running_tasks_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
from aiodocker import DockerError
77
from models_library.callbacks_mapping import UserServiceCommand
8-
from servicelib.logging_utils import log_context
9-
10-
from ..core.errors import (
8+
from servicelib.container_utils import (
119
ContainerExecCommandFailedError,
1210
ContainerExecContainerNotFoundError,
1311
ContainerExecTimeoutError,
12+
run_command_in_container,
1413
)
14+
from servicelib.logging_utils import log_context
15+
1516
from ..models.shared_store import SharedStore
1617
from ..modules.mounted_fs import MountedVolumes
17-
from .container_utils import run_command_in_container
1818

1919
_logger = logging.getLogger(__name__)
2020

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/prometheus_metrics.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
from fastapi import FastAPI, status
1111
from models_library.callbacks_mapping import CallbacksMapping, UserServiceCommand
1212
from pydantic import BaseModel, NonNegativeFloat, NonNegativeInt
13-
from servicelib.logging_utils import log_context
14-
from servicelib.sequences_utils import pairwise
15-
from simcore_service_dynamic_sidecar.core.errors import (
13+
from servicelib.container_utils import (
1614
ContainerExecContainerNotFoundError,
15+
run_command_in_container,
1716
)
17+
from servicelib.logging_utils import log_context
18+
from servicelib.sequences_utils import pairwise
1819

1920
from ..models.shared_store import SharedStore
20-
from .container_utils import run_command_in_container
2121

2222
_logger = logging.getLogger(__name__)
2323

services/dynamic-sidecar/tests/unit/test_core_docker_utils.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# pylint: disable=unused-argument
33
# pylint: disable=unused-variable
44
from collections.abc import AsyncIterable, AsyncIterator
5-
from contextlib import suppress
65

76
import aiodocker
87
import pytest
@@ -73,9 +72,7 @@ async def started_services(container_names: list[str]) -> AsyncIterator[None]:
7372
yield
7473

7574
for container in started_containers:
76-
with suppress(aiodocker.DockerError):
77-
await container.kill()
78-
await container.delete()
75+
await container.delete(force=True)
7976

8077

8178
async def test_volume_with_label(

0 commit comments

Comments
 (0)