重构为直接 OpenSandbox 生命周期调度
This commit is contained in:
@@ -1,114 +1,88 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from gitea_dynamic_runner.models import RunnerRequest
|
||||
from gitea_dynamic_runner import opensandbox_worker
|
||||
from gitea_dynamic_runner.sandbox_kubernetes import allocated_pod_name
|
||||
from gitea_dynamic_runner.opensandbox_worker import (
|
||||
OpenSandboxScheduler,
|
||||
RegistrationTokens,
|
||||
sandbox_request,
|
||||
)
|
||||
|
||||
|
||||
def request() -> RunnerRequest:
|
||||
def request(backend: str = "vm") -> RunnerRequest:
|
||||
return RunnerRequest(
|
||||
job_id=47,
|
||||
run_id=12,
|
||||
backend="vm",
|
||||
job_id=42,
|
||||
run_id=7,
|
||||
backend=backend,
|
||||
repository="panxiao81/example",
|
||||
job_name="publish image",
|
||||
labels=("self-hosted", "vm"),
|
||||
labels=("self-hosted", backend),
|
||||
)
|
||||
|
||||
|
||||
def test_sandbox_request_uses_pool_and_identity_gate(monkeypatch):
|
||||
monkeypatch.setattr(opensandbox_worker, "OPENSANDBOX_POOL", "ci-vm")
|
||||
document = opensandbox_worker.sandbox_request(request(), "gitea-vm-abcd")
|
||||
def test_sandbox_request_uses_pool_identity_and_one_time_token_url():
|
||||
document = sandbox_request(
|
||||
request(), "gitea-vm-abcd", "http://scheduler/token/nonce"
|
||||
)
|
||||
assert document["pool"] == "ci-vm"
|
||||
assert document["entrypoint"] == [
|
||||
"/usr/local/libexec/gitea-opensandbox-runner"
|
||||
]
|
||||
assert document["env"]["GITEA_RUNNER_NAME"] == "gitea-vm-abcd"
|
||||
assert document["env"]["GITEA_RUNNER_LABELS"] == "self-hosted:host,vm:host"
|
||||
assert document["env"]["CI_SPIFFE_ID"].startswith(
|
||||
"spiffe://ddupan.top/ci/panxiao81/example/publish-image-"
|
||||
assert document["env"]["GITEA_RUNNER_REGISTRATION_TOKEN_URL"].endswith(
|
||||
"/nonce"
|
||||
)
|
||||
assert document["metadata"]["ci.ddupan.top/job-id"] == "47"
|
||||
|
||||
|
||||
def test_identity_entry_is_bound_to_kata_pod_uid(monkeypatch):
|
||||
monkeypatch.setattr(opensandbox_worker, "SPIRE_CLUSTER_NAME", "sandbox-kata")
|
||||
monkeypatch.setattr(opensandbox_worker, "RUNNER_UID", 2000)
|
||||
manifest = opensandbox_worker.identity_entry(
|
||||
request(), sandbox_id="sandbox-123", pod_uid="pod-uid-456"
|
||||
assert document["env"]["CI_SPIFFE_ID"] == (
|
||||
"spiffe://ddupan.top/ci/panxiao81/example/publish-image-3e72cdc4a97e"
|
||||
)
|
||||
assert manifest["metadata"]["name"] == opensandbox_worker.entry_name(
|
||||
"sandbox-123"
|
||||
)
|
||||
spec = manifest["spec"]
|
||||
assert spec["parentID"] == (
|
||||
"spiffe://ddupan.top/spire/agent/k8s_psat/"
|
||||
"sandbox-kata/pod/pod-uid-456"
|
||||
)
|
||||
assert spec["selectors"] == ["unix:uid:2000"]
|
||||
assert spec["spiffeID"].startswith(
|
||||
"spiffe://ddupan.top/ci/panxiao81/example/publish-image-"
|
||||
assert document["metadata"]["ci.ddupan.top/runner"] == "true"
|
||||
|
||||
|
||||
async def test_registration_token_is_single_use(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"gitea_dynamic_runner.opensandbox_worker.TOKEN_BASE_URL",
|
||||
"http://scheduler/token",
|
||||
)
|
||||
tokens = RegistrationTokens(b"secret\n")
|
||||
nonce, url = await tokens.issue()
|
||||
assert url == f"http://scheduler/token/{nonce}"
|
||||
assert await tokens.consume(nonce) == b"secret"
|
||||
assert await tokens.consume(nonce) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("annotation", "expected"),
|
||||
[
|
||||
(json.dumps({"pods": ["pool-pod-1"], "poolRef": "ci-vm"}), "pool-pod-1"),
|
||||
(json.dumps({"pods": []}), None),
|
||||
(json.dumps({"pods": ["one", "two"]}), None),
|
||||
("not-json", None),
|
||||
],
|
||||
)
|
||||
def test_allocated_pod_name(annotation, expected):
|
||||
batchsandbox = {
|
||||
"metadata": {
|
||||
"annotations": {"sandbox.opensandbox.io/alloc-status": annotation}
|
||||
}
|
||||
}
|
||||
assert allocated_pod_name(batchsandbox) == expected
|
||||
|
||||
|
||||
class FakeKubernetesClient:
|
||||
class FakeOpenSandbox:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
self.created = None
|
||||
self.deleted = []
|
||||
|
||||
async def get_batchsandbox(self, sandbox_id):
|
||||
self.calls += 1
|
||||
if self.calls == 1:
|
||||
return {"metadata": {"annotations": {}}}
|
||||
return {
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"sandbox.opensandbox.io/alloc-status": json.dumps(
|
||||
{"pods": ["pool-pod-1"], "poolRef": "ci-vm"}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
async def create(self, **document):
|
||||
self.created = document
|
||||
return {"id": "sandbox-1"}
|
||||
|
||||
async def get_pod(self, name):
|
||||
assert name == "pool-pod-1"
|
||||
return {"metadata": {"uid": "pod-uid-1"}}
|
||||
async def get(self, sandbox_id):
|
||||
return {"status": {"state": "Terminated"}}
|
||||
|
||||
async def delete(self, sandbox_id):
|
||||
self.deleted.append(sandbox_id)
|
||||
|
||||
|
||||
async def test_wait_for_allocation_returns_real_pod_uid(monkeypatch):
|
||||
async def no_sleep(_):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(asyncio, "sleep", no_sleep)
|
||||
client = FakeKubernetesClient()
|
||||
assert await opensandbox_worker.wait_for_allocation(client, "sandbox-1") == (
|
||||
"pool-pod-1",
|
||||
"pod-uid-1",
|
||||
async def test_scheduler_creates_monitors_and_deletes(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"gitea_dynamic_runner.opensandbox_worker.TOKEN_BASE_URL",
|
||||
"http://scheduler/token",
|
||||
)
|
||||
client = FakeOpenSandbox()
|
||||
scheduler = OpenSandboxScheduler(client, RegistrationTokens(b"secret"))
|
||||
assert await scheduler.create(request()) == "sandbox-1"
|
||||
await scheduler.active[42]
|
||||
assert client.created["pool"] == "ci-vm"
|
||||
assert client.deleted == ["sandbox-1"]
|
||||
|
||||
|
||||
def test_entry_name_is_stable_and_dns_safe():
|
||||
name = opensandbox_worker.entry_name("sandbox/with unsafe characters")
|
||||
assert name == opensandbox_worker.entry_name("sandbox/with unsafe characters")
|
||||
assert name.startswith("gitea-ci-")
|
||||
assert "/" not in name
|
||||
async def test_scheduler_rejects_duplicate_active_job():
|
||||
client = FakeOpenSandbox()
|
||||
scheduler = OpenSandboxScheduler(client, RegistrationTokens(b"secret"))
|
||||
scheduler.active[42] = asyncio.Future()
|
||||
with pytest.raises(ValueError, match="already"):
|
||||
await scheduler.create(request())
|
||||
|
||||
Reference in New Issue
Block a user