Skip to content

Commit 26706cd

Browse files
authored
Bugs/container submit 409 (#94)
pass zun container info via exception on wait exc. The root cause of submit() returning null container id was because _wait_for_status can raise inside create_container, between the zun creation and returning the zun object. By adding the zun container to a custom exception inside create_container, submit() can still populate self.id and self._status from the zun object. This also makes it possible to set details from the zun container status_reason and status_detail fields.
1 parent f691d6a commit 26706cd

3 files changed

Lines changed: 142 additions & 26 deletions

File tree

chi/container.py

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from .clients import connection, zun
3030
from .context import session
31-
from .exception import ResourceError, ServiceError
31+
from .exception import ContainerCreateWaitError, ResourceError, ServiceError
3232
from .network import bind_floating_ip, get_free_floating_ip
3333

3434
DEFAULT_IMAGE_DRIVER = "docker"
@@ -150,24 +150,26 @@ def submit(
150150
if self.workdir:
151151
kwargs["workdir"] = self.workdir
152152

153-
container = create_container(
154-
name=self.name,
155-
image=self.image_ref,
156-
exposed_ports=self.exposed_ports,
157-
reservation_id=self.reservation_id,
158-
start=self.start,
159-
start_timeout=self.start_timeout,
160-
runtime=self.runtime,
161-
environment=self.environment,
162-
device_profiles=self.device_profiles,
163-
**kwargs,
164-
)
165-
166-
if container:
167-
self.id = zun().containers.get(self.name).uuid
168-
self._status = zun().containers.get(self.name).status
169-
else:
170-
raise ResourceError("could not create container")
153+
try:
154+
container = create_container(
155+
name=self.name,
156+
image=self.image_ref,
157+
exposed_ports=self.exposed_ports,
158+
reservation_id=self.reservation_id,
159+
start=self.start,
160+
start_timeout=self.start_timeout,
161+
runtime=self.runtime,
162+
environment=self.environment,
163+
device_profiles=self.device_profiles,
164+
**kwargs,
165+
)
166+
self.id = container.uuid
167+
self._status = container.status
168+
except ContainerCreateWaitError as exc:
169+
# ensure container object gets params even on error
170+
self.id = exc.zun_container.uuid
171+
self._status = exc.zun_container.status
172+
raise ResourceError(message=exc.zun_container.status_reason) from exc
171173

172174
if wait_for_active and self.status != "Running":
173175
self.wait(status="Running", timeout=wait_timeout)
@@ -435,13 +437,16 @@ def create_container(
435437
timeout = start_timeout or (60 * 30)
436438
LOG.info(f"Waiting up to {timeout}s for container creation ...")
437439

438-
if platform_version == 2:
439-
container = _wait_for_status(container.uuid, "Running", timeout=timeout)
440-
else:
441-
container = _wait_for_status(container.uuid, "Created", timeout=timeout)
442-
if start:
443-
LOG.info("Starting container ...")
444-
zun().containers.start(container.uuid)
440+
try:
441+
if platform_version == 2:
442+
container = _wait_for_status(container.uuid, "Running", timeout=timeout)
443+
else:
444+
container = _wait_for_status(container.uuid, "Created", timeout=timeout)
445+
if start:
446+
LOG.info("Starting container ...")
447+
zun().containers.start(container.uuid)
448+
except (RuntimeError, TimeoutError) as exc:
449+
raise ContainerCreateWaitError(zun_container=container, cause=exc) from exc
445450

446451
return container
447452

chi/exception.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,18 @@ class ServiceError(Exception):
2828

2929
def __init__(self, message):
3030
super().__init__(message)
31+
32+
33+
class ContainerCreateWaitError(ResourceError):
34+
"""Raised when Zun creates a container but waiting for target status fails."""
35+
36+
def __init__(self, zun_container, cause):
37+
self.zun_container = zun_container
38+
self.cause = cause
39+
message = (
40+
"Container {} was created, but waiting for target status failed: {}".format(
41+
self.zun_container.uuid,
42+
cause,
43+
)
44+
)
45+
super().__init__(message)

tests/test_container.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
from datetime import datetime
1919

2020
import pytest
21+
from zunclient.exceptions import Conflict
2122

2223
from chi.container import Container, download, upload
24+
from chi.exception import ContainerCreateWaitError, ResourceError
2325

2426

2527
@pytest.fixture()
@@ -110,3 +112,97 @@ def test_download_extracts_tar_and_writes_file(mocker):
110112
assert os.path.exists(dest_path)
111113
with open(dest_path, "rb") as f:
112114
assert f.read() == file_content
115+
116+
117+
def test_submit_idempotent_returns_existing_without_create(mocker):
118+
# idempotent=true, wait=true
119+
chi_container = Container(name="dup-name", image_ref="img")
120+
existing_zun_container = mocker.Mock(uuid="existing-uuid", status="Running")
121+
122+
mocker.patch("chi.container.get_container", return_value=existing_zun_container)
123+
create_mock = mocker.patch("chi.container.create_container")
124+
125+
submit_result = chi_container.submit(
126+
idempotent=True, wait_for_active=True, wait_timeout=123, show="text"
127+
)
128+
create_mock.assert_not_called()
129+
existing_zun_container.wait.assert_called_once_with(status="Running", timeout=123)
130+
existing_zun_container.show.assert_called_once_with(
131+
type="text", wait_for_active=True
132+
)
133+
134+
# submit returns only on idempotent=true
135+
assert submit_result is existing_zun_container
136+
137+
138+
def test_submit_idempotent_returns_existing_without_create_no_wait(mocker):
139+
# idempotent=true, wait=false
140+
chi_container = Container(name="dup-name", image_ref="img")
141+
existing_zun_container = mocker.Mock(uuid="existing-uuid", status="Running")
142+
143+
mocker.patch("chi.container.get_container", return_value=existing_zun_container)
144+
create_mock = mocker.patch("chi.container.create_container")
145+
146+
submit_result = chi_container.submit(
147+
idempotent=True, wait_for_active=False, show=None
148+
)
149+
create_mock.assert_not_called()
150+
existing_zun_container.wait.assert_not_called()
151+
existing_zun_container.show.assert_not_called()
152+
153+
# submit returns only on idempotent=true
154+
assert submit_result is existing_zun_container
155+
156+
157+
def test_submit_preserves_reference_on_create_wait_failure(mocker):
158+
"""Ensure that we keep the zun container id, even if create fails.
159+
160+
This case can arise because the container moves to an error state, or if
161+
the wait times out for another reason.
162+
"""
163+
chi_container = Container(name="test", image_ref="img")
164+
leaked_zun_container = mocker.Mock(uuid="leaked-uuid", status="Error")
165+
166+
mocker.patch(
167+
"chi.container.create_container",
168+
side_effect=ContainerCreateWaitError(
169+
zun_container=leaked_zun_container, cause=RuntimeError
170+
),
171+
)
172+
zun_mock = mocker.patch("chi.container.zun")()
173+
zun_mock.containers.get.return_value = leaked_zun_container
174+
175+
with pytest.raises(ResourceError):
176+
chi_container.submit(wait_for_active=False, show=None)
177+
178+
assert chi_container.id == "leaked-uuid"
179+
assert chi_container._status == "Error"
180+
181+
182+
def test_submit_duplicate_name_tracks_created_uuid(mocker):
183+
"""Test the case where we re-run submit after a failure.
184+
185+
An "old" container alreday already exists, with name = "dup-name".
186+
If idempotent=false, it should be possible to make a new container with the same name.
187+
However, name-based lookups will fail with a 409.
188+
"""
189+
190+
chi_container = Container(name="dup-name", image_ref="img")
191+
new_zun_container = mocker.Mock(uuid="new-uuid", status="Running")
192+
193+
def _get_side_effect(ref):
194+
if ref == "dup-name":
195+
raise Conflict(
196+
"Multiple containers exist with same name. Please use the container uuid instead."
197+
)
198+
if ref == "new-uuid":
199+
return new_zun_container
200+
raise AssertionError(ref)
201+
202+
mocker.patch("chi.container.create_container", return_value=new_zun_container)
203+
zun_mock = mocker.patch("chi.container.zun")()
204+
zun_mock.containers.get.side_effect = _get_side_effect
205+
206+
# disable optional behavor from wait_for_active and show
207+
chi_container.submit(wait_for_active=False, show=None)
208+
assert chi_container.id == "new-uuid"

0 commit comments

Comments
 (0)