Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/harbor_hypeman/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,16 @@ async def start(self, force_build: bool) -> None:
raise RuntimeError("Hypeman environment is already started.")

image = await self._prepare_image(force_build)
instance_name = self._instance_name()
instance = await self._client.instances.create(
image=image,
name=self._instance_name(),
name=instance_name,
entrypoint=["/bin/sh", "-c"],
cmd=["while true; do sleep 3600; done"],
env=self._startup_env(),
env={
**self._startup_env(),
"HYPEMAN_INSTANCE_NAME": instance_name,
},
network={"enabled": self.network_policy.network_mode == NetworkMode.PUBLIC},
tags={
"harbor.managed": "true",
Expand Down Expand Up @@ -183,11 +187,15 @@ async def _prepare_image(self, force_build: bool) -> str:
):
if docker_image is None:
raise RuntimeError("Prebuilt image selection requires docker_image.")
image = await self._client.images.create(
name=docker_image,
tags={_BUILD_TAG: self.environment_id},
)
return image.name
try:
await self._client.images.get(docker_image)
return docker_image
except NotFoundError:
image = await self._client.images.create(
name=docker_image,
tags={_BUILD_TAG: self.environment_id},
)
return image.name

if not force_build:
cached_image = await self._cached_build_image()
Expand Down
34 changes: 31 additions & 3 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest
from harbor.environments.base import SandboxBuildFailedError
from harbor.environments.factory import EnvironmentFactory
from harbor.models.task.config import EnvironmentConfig, NetworkMode, NetworkPolicy
from harbor.models.trial.config import EnvironmentConfig as TrialEnvironmentConfig
from harbor.models.trial.paths import TrialPaths
from hypeman import AsyncHypeman
from hypeman import AsyncHypeman, NotFoundError

import harbor_hypeman.environment as environment_module
from harbor_hypeman import HypemanEnvironment
Expand All @@ -25,7 +26,10 @@ def _client() -> Any:
client.images.create = AsyncMock(
return_value=SimpleNamespace(name="docker.io/library/alpine:latest")
)
client.images.get = AsyncMock()
response = httpx.Response(404, request=httpx.Request("GET", "https://example.com"))
client.images.get = AsyncMock(
side_effect=NotFoundError("missing", response=response, body=None)
)
client.builds.list = AsyncMock(return_value=[])
client.builds.create = AsyncMock()
client.builds.get = AsyncMock()
Expand Down Expand Up @@ -131,8 +135,11 @@ async def test_start_prebuilt_image_maps_resources_and_network(tmp_path: Path) -
assert create_kwargs["size"] == "2048MB"
assert create_kwargs["overlay_size"] == "4096MB"
assert create_kwargs["network"] == {"enabled": False}
assert create_kwargs["env"] == {"TASK_ENV": "value"}
assert create_kwargs["name"].startswith("harbor-test-task-abc123-env-")
assert create_kwargs["env"] == {
"TASK_ENV": "value",
"HYPEMAN_INSTANCE_NAME": create_kwargs["name"],
}
client.instances.wait.assert_awaited_once_with(
"instance-1",
state="Running",
Expand All @@ -141,6 +148,26 @@ async def test_start_prebuilt_image_maps_resources_and_network(tmp_path: Path) -
)


async def test_start_uses_existing_prebuilt_image(tmp_path: Path) -> None:
client = _client()
client.images.get.side_effect = None
client.images.get.return_value = SimpleNamespace(
name="docker.io/builds/build-1:latest",
status="ready",
)
environment = _environment(
tmp_path,
task_config=EnvironmentConfig(docker_image="builds/build-1"),
client=client,
)

await environment.start(force_build=False)

client.images.get.assert_awaited_once_with("builds/build-1")
client.images.create.assert_not_awaited()
assert client.instances.create.await_args.kwargs["image"] == "builds/build-1"


async def test_start_creates_configured_workdir(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down Expand Up @@ -247,6 +274,7 @@ async def test_start_failure_deletes_created_instance(tmp_path: Path) -> None:

async def test_start_reuses_ready_build(tmp_path: Path) -> None:
client = _client()
client.images.get.side_effect = None
client.builds.list.return_value = [
SimpleNamespace(
status="ready",
Expand Down