Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement /resend-pending-builds endpoint #48

Merged
merged 4 commits into from
Feb 22, 2024
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
34 changes: 25 additions & 9 deletions softpack_core/schemas/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,15 +292,13 @@
)

for env in environment_objects:
env.avg_wait_secs = avg_wait_secs

Check warning on line 295 in softpack_core/schemas/environment.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/schemas/environment.py#L295

Added line #L295 was not covered by tests
status = status_map.get(str(Path(env.path, env.name)))
if not status:
if env.state == State.queued:
env.state = State.failed
continue
env.requested = status.requested
env.build_start = status.build_start
env.build_done = status.build_done
env.avg_wait_secs = avg_wait_secs

return environment_objects

Expand Down Expand Up @@ -360,7 +358,29 @@

version += 1

# Send build request
response = cls.submit_env_to_builder(env)

Check warning on line 361 in softpack_core/schemas/environment.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/schemas/environment.py#L361

Added line #L361 was not covered by tests
if response is not None:
return response

Check warning on line 363 in softpack_core/schemas/environment.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/schemas/environment.py#L363

Added line #L363 was not covered by tests

return CreateEnvironmentSuccess(

Check warning on line 365 in softpack_core/schemas/environment.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/schemas/environment.py#L365

Added line #L365 was not covered by tests
message="Successfully scheduled environment creation"
)

@classmethod
def submit_env_to_builder(
cls, env: EnvironmentInput
) -> Union[None, BuilderError, InvalidInputError]:
"""Submit an environment to the builder."""
try:
m = re.fullmatch(r"^(.*)-(\d+)$", env.name)

Check warning on line 375 in softpack_core/schemas/environment.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/schemas/environment.py#L374-L375

Added lines #L374 - L375 were not covered by tests
sb10 marked this conversation as resolved.
Show resolved Hide resolved
if not m:
raise Exception
versionless_name, version = m.groups()
except Exception:
return InvalidInputError(

Check warning on line 380 in softpack_core/schemas/environment.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/schemas/environment.py#L377-L380

Added lines #L377 - L380 were not covered by tests
message=f"could not parse version from name: {env.name!r}"
)

try:
host = app.settings.builder.host
port = app.settings.builder.port
Expand All @@ -383,15 +403,11 @@
)
r.raise_for_status()
except Exception as e:
cls.delete(env.name, env.path)
return BuilderError(
message="Connection to builder failed: "
+ "".join(format_exception_only(type(e), e))
)

return CreateEnvironmentSuccess(
message="Successfully scheduled environment creation"
)
return None

Check warning on line 410 in softpack_core/schemas/environment.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/schemas/environment.py#L410

Added line #L410 was not covered by tests

@classmethod
def create_new_env(
Expand Down
36 changes: 35 additions & 1 deletion softpack_core/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@

import typer
import uvicorn
from fastapi import APIRouter, Request, UploadFile
from fastapi import APIRouter, Request, Response, UploadFile
from typer import Typer
from typing_extensions import Annotated

from softpack_core.artifacts import State
from softpack_core.schemas.environment import (
CreateEnvironmentSuccess,
Environment,
EnvironmentInput,
PackageInput,
WriteArtifactSuccess,
)

Expand Down Expand Up @@ -92,3 +94,35 @@
raise Exception(resp)

return resp

@staticmethod
@router.post("/resend-pending-builds")
async def resend_pending_builds( # type: ignore[no-untyped-def]
response: Response,
):
"""Resubmit any pending builds to the builder."""
successes = 0
failures = 0

Check warning on line 105 in softpack_core/service.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/service.py#L104-L105

Added lines #L104 - L105 were not covered by tests
for env in Environment.iter():
if env.state != State.queued:
continue

Check warning on line 108 in softpack_core/service.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/service.py#L108

Added line #L108 was not covered by tests
result = Environment.submit_env_to_builder(
EnvironmentInput(
name=env.name,
path=env.path,
description=env.description,
packages=[PackageInput(**vars(p)) for p in env.packages],
)
)
if result is None:
successes += 1

Check warning on line 118 in softpack_core/service.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/service.py#L118

Added line #L118 was not covered by tests
else:
failures += 1

Check warning on line 120 in softpack_core/service.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/service.py#L120

Added line #L120 was not covered by tests

if failures == 0:
message = "Successfully triggered resends"

Check warning on line 123 in softpack_core/service.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/service.py#L123

Added line #L123 was not covered by tests
else:
response.status_code = 500
message = "Failed to trigger all resends"

Check warning on line 126 in softpack_core/service.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/service.py#L125-L126

Added lines #L125 - L126 were not covered by tests

return {"message": message, "successes": successes, "failures": failures}

Check warning on line 128 in softpack_core/service.py

View check run for this annotation

Codecov / codecov/patch

softpack_core/service.py#L128

Added line #L128 was not covered by tests
39 changes: 7 additions & 32 deletions tests/integration/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import yaml
from fastapi import UploadFile

from softpack_core.artifacts import Artifacts, app
from softpack_core.artifacts import Artifacts
from softpack_core.schemas.environment import (
BuilderError,
CreateEnvironmentSuccess,
Expand All @@ -29,7 +29,7 @@
UpdateEnvironmentSuccess,
WriteArtifactSuccess,
)
from tests.integration.utils import file_in_remote
from tests.integration.utils import builder_called_correctly, file_in_remote

pytestmark = pytest.mark.repo

Expand Down Expand Up @@ -142,7 +142,7 @@ def test_create_path_invalid_disallowed(httpx_post, testable_env_input, path):
assert isinstance(result, InvalidInputError)


def test_create_cleans_up_after_builder_failure(
def test_create_does_not_clean_up_after_builder_failure(
httpx_post, testable_env_input
):
httpx_post.side_effect = Exception('could not contact builder')
Expand All @@ -156,33 +156,8 @@ def test_create_cleans_up_after_builder_failure(
)
builtPath = dir / Environment.artifacts.built_by_softpack_file
ymlPath = dir / Environment.artifacts.environments_file
assert not file_in_remote(builtPath)
assert not file_in_remote(ymlPath)


def builder_called_correctly(
post_mock, testable_env_input: EnvironmentInput
) -> None:
# TODO: don't mock this; actually have a real builder service to test with?
host = app.settings.builder.host
port = app.settings.builder.port
post_mock.assert_called_with(
f"http://{host}:{port}/environments/build",
json={
"name": f"{testable_env_input.path}/{testable_env_input.name}",
"version": "1",
"model": {
"description": testable_env_input.description,
"packages": [
{
"name": pkg.name,
"version": pkg.version,
}
for pkg in testable_env_input.packages
],
},
},
)
assert file_in_remote(builtPath)
assert file_in_remote(ymlPath)


def test_delete(httpx_post, testable_env_input) -> None:
Expand Down Expand Up @@ -307,8 +282,8 @@ def test_iter_no_statuses(testable_env_input, mocker):
assert envs[0].build_start is None
assert envs[0].build_done is None
assert envs[0].avg_wait_secs is None
assert envs[0].state == State.failed
assert envs[1].state == State.failed
assert envs[0].state == State.queued
assert envs[1].state == State.queued


@pytest.mark.asyncio
Expand Down
59 changes: 59 additions & 0 deletions tests/integration/test_resend_builds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Copyright (c) 2024 Genome Research Ltd.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
"""


import pytest
from fastapi.testclient import TestClient

from softpack_core.app import app
from softpack_core.schemas.environment import (
CreateEnvironmentSuccess,
Environment,
EnvironmentInput,
)
from softpack_core.service import ServiceAPI
from tests.integration.utils import builder_called_correctly

pytestmark = pytest.mark.repo


def test_resend_pending_builds(
httpx_post, testable_env_input: EnvironmentInput
):
Environment.delete("test_environment", "users/test_user")
Environment.delete("test_environment", "groups/test_group")
ServiceAPI.register()
client = TestClient(app.router)

orig_name = testable_env_input.name
testable_env_input.name += "-1"
r = Environment.create_new_env(
testable_env_input, Environment.artifacts.built_by_softpack_file
)
assert isinstance(r, CreateEnvironmentSuccess)
testable_env_input.name = orig_name

httpx_post.assert_not_called()

resp = client.post(
url="/resend-pending-builds",
)
assert resp.status_code == 200
assert resp.json().get("message") == "Successfully triggered resends"
assert resp.json().get("successes") == 1
assert resp.json().get("failures") == 0

httpx_post.assert_called_once()
builder_called_correctly(httpx_post, testable_env_input)

httpx_post.side_effect = Exception('could not contact builder')
resp = client.post(
url="/resend-pending-builds",
)
assert resp.status_code == 500
assert resp.json().get("message") == "Failed to trigger all resends"
assert resp.json().get("successes") == 0
assert resp.json().get("failures") == 1
2 changes: 1 addition & 1 deletion tests/integration/test_spack.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_spack_packages():
else:
assert len(packages) > len(pkgs)

spack = Spack(custom_repo = app.settings.spack.repo)
spack = Spack(custom_repo=app.settings.spack.repo)

spack.packages()

Expand Down
26 changes: 26 additions & 0 deletions tests/integration/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import pytest

from softpack_core.artifacts import Artifacts, app
from softpack_core.schemas.environment import EnvironmentInput

artifacts_dict = dict[
str,
Expand Down Expand Up @@ -199,3 +200,28 @@ def file_in_repo(
current = current[part]

return current


def builder_called_correctly(
post_mock, testable_env_input: EnvironmentInput
) -> None:
# TODO: don't mock this; actually have a real builder service to test with?
host = app.settings.builder.host
port = app.settings.builder.port
post_mock.assert_called_with(
f"http://{host}:{port}/environments/build",
json={
"name": f"{testable_env_input.path}/{testable_env_input.name}",
"version": "1",
"model": {
"description": testable_env_input.description,
"packages": [
{
"name": pkg.name,
"version": pkg.version,
}
for pkg in testable_env_input.packages
],
},
},
)
Loading