修复 Runner 身份路径并补充关联日志
This commit is contained in:
@@ -4,12 +4,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import ssl
|
||||
from urllib.parse import quote
|
||||
import uuid
|
||||
|
||||
import nats
|
||||
@@ -49,16 +50,26 @@ RUNNER_TOKEN_SECRET = os.environ.get("RUNNER_TOKEN_SECRET", "gitea-dynamic-runne
|
||||
GITEA_INSTANCE = os.environ.get("GITEA_INSTANCE", "https://git.ddupan.top")
|
||||
CAPACITY = int(os.environ.get("RUNNER_CAPACITY", "4"))
|
||||
POD_TIMEOUT = int(os.environ.get("RUNNER_POD_TIMEOUT", str(4 * 60 * 60)))
|
||||
SPIFFE_PATH_SEGMENT = re.compile(r"^[A-Za-z0-9._-]+$")
|
||||
|
||||
|
||||
def identity_path(repository: str, job_name: str) -> str:
|
||||
parts = repository.split("/")
|
||||
if len(parts) != 2 or not all(parts):
|
||||
raise ValueError("repository must be owner/name")
|
||||
encoded = [quote(part, safe="-._~") for part in (*parts, job_name)]
|
||||
if not job_name.strip() or any(not part for part in encoded):
|
||||
if not job_name.strip():
|
||||
raise ValueError("identity components must not be empty")
|
||||
return "/".join(encoded)
|
||||
return "/".join(safe_identity_segment(part) for part in (*parts, job_name))
|
||||
|
||||
|
||||
def safe_identity_segment(value: str) -> str:
|
||||
"""Map arbitrary Gitea names to stable SPIFFE Operator path segments."""
|
||||
if SPIFFE_PATH_SEGMENT.fullmatch(value):
|
||||
return value
|
||||
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", value).strip("-._")
|
||||
slug = slug[:48].rstrip("-._") or "segment"
|
||||
digest = hashlib.sha256(value.encode()).hexdigest()[:12]
|
||||
return f"{slug}-{digest}"
|
||||
|
||||
|
||||
def pod_manifest(request: RunnerRequest, pod_name: str) -> dict[str, object]:
|
||||
@@ -174,36 +185,90 @@ async def run_request(message: object, client: KubernetesClient) -> None:
|
||||
return
|
||||
|
||||
pod_name = f"gitea-pod-{uuid.uuid4().hex[:12]}"
|
||||
LOG.info(
|
||||
"runner request received job_id=%d run_id=%d repository=%s job_name=%r "
|
||||
"backend=%s pod=%s",
|
||||
request.job_id,
|
||||
request.run_id,
|
||||
request.repository,
|
||||
request.job_name,
|
||||
request.backend,
|
||||
pod_name,
|
||||
)
|
||||
stop = asyncio.Event()
|
||||
pulse = asyncio.create_task(heartbeat(message, stop))
|
||||
try:
|
||||
await client.create_pod(pod_manifest(request, pod_name))
|
||||
LOG.info(
|
||||
"runner Pod created pod=%s job_id=%d run_id=%d image=%s",
|
||||
pod_name,
|
||||
request.job_id,
|
||||
request.run_id,
|
||||
RUNNER_IMAGE,
|
||||
)
|
||||
if await wait_for_pod(client, pod_name):
|
||||
await message.ack()
|
||||
LOG.info(
|
||||
"runner request acknowledged pod=%s job_id=%d result=succeeded",
|
||||
pod_name,
|
||||
request.job_id,
|
||||
)
|
||||
else:
|
||||
LOG.error("runner Pod %s failed", pod_name)
|
||||
LOG.error(
|
||||
"runner Pod failed pod=%s job_id=%d; request will be retried",
|
||||
pod_name,
|
||||
request.job_id,
|
||||
)
|
||||
await message.nak(delay=30)
|
||||
except Exception:
|
||||
LOG.exception(
|
||||
"runner request failed pod=%s job_id=%d; request will be retried",
|
||||
pod_name,
|
||||
request.job_id,
|
||||
)
|
||||
await message.nak(delay=30)
|
||||
raise
|
||||
finally:
|
||||
stop.set()
|
||||
await pulse
|
||||
LOG.info("deleting runner Pod pod=%s job_id=%d", pod_name, request.job_id)
|
||||
await client.delete_pod(pod_name)
|
||||
|
||||
|
||||
async def bind_request(message: object, client: KubernetesClient) -> None:
|
||||
binding: IdentityBinding | None = None
|
||||
try:
|
||||
document = json.loads(message.data)
|
||||
binding = IdentityBinding(**document)
|
||||
if binding.backend != "pod" or not binding.runner_name.startswith("gitea-pod-"):
|
||||
raise ValueError("invalid Pod identity binding")
|
||||
path = identity_path(binding.repository, binding.job_name)
|
||||
LOG.info(
|
||||
"identity binding received job_id=%d run_id=%d runner_name=%s "
|
||||
"repository=%s job_name=%r identity_path=%s",
|
||||
binding.job_id,
|
||||
binding.run_id,
|
||||
binding.runner_name,
|
||||
binding.repository,
|
||||
binding.job_name,
|
||||
path,
|
||||
)
|
||||
await client.bind_identity(binding.runner_name, path)
|
||||
except ClientResponseError as error:
|
||||
if error.status == 404:
|
||||
LOG.warning(
|
||||
"identity binding Pod not found runner_name=%s job_id=%s; binding will be retried",
|
||||
binding.runner_name if binding else None,
|
||||
binding.job_id if binding else None,
|
||||
)
|
||||
await message.nak(delay=2)
|
||||
return
|
||||
LOG.exception(
|
||||
"identity binding Kubernetes request failed runner_name=%s job_id=%s status=%d",
|
||||
binding.runner_name if binding else None,
|
||||
binding.job_id if binding else None,
|
||||
error.status,
|
||||
)
|
||||
await message.nak(delay=30)
|
||||
raise
|
||||
except (json.JSONDecodeError, TypeError, ValueError) as error:
|
||||
@@ -211,6 +276,12 @@ async def bind_request(message: object, client: KubernetesClient) -> None:
|
||||
await message.ack()
|
||||
return
|
||||
await message.ack()
|
||||
LOG.info(
|
||||
"identity binding applied and acknowledged runner_name=%s job_id=%d identity_path=%s",
|
||||
binding.runner_name,
|
||||
binding.job_id,
|
||||
path,
|
||||
)
|
||||
|
||||
|
||||
async def consume_requests(subscription: object, client: KubernetesClient) -> None:
|
||||
|
||||
Reference in New Issue
Block a user