c76154aeaa
ci / lint (push) Successful in 34s
ci / unit (push) Successful in 1m41s
ci / types (push) Successful in 1m41s
ci / dockerfile (push) Successful in 18s
ci / security (push) Successful in 1m27s
ci / chart (push) Failing after 1m11s
ci / integration (push) Successful in 1m10s
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
CORRECTNESS - lost-lease race: complete()/fail() did not check ownership, so a worker whose lease expired could mark a task done while another worker was running it, or requeue a task someone else owned. Reproduced, fixed with a CAS on (state, locked_by), pinned by two regression tests. - worker died on report failure: _run_one's docstring claimed no exception escapes the TaskGroup; fail()/complete() were outside the guarded block, so a DB blip cancelled every sibling provision on the pod. - claim query used an INNER join, which could strand a just-claimed task and report 'queue empty'. LEFT join. - InstanceRepo.set_error bypassed the state machine and had no callers. Deleted. - handle_deprovision ignored its CAS result, so a wrong-state instance kept a dangling endpoint and got re-provisioned by the drift check 60s later. - handle_verify re-notified on every retry: five pages for one halt. DEPLOY-BREAKING - the migration Job could never succeed: no Dockerfile copied migrations/, and migrate.py resolved the path relative to the source tree, which only works for an editable install. Added COPY + SVCFORGE_MIGRATIONS_DIR. - ServiceMonitor selector did not match the Service: API metrics never scraped. - SvcforgeReconcilerStale fired permanently from every pod, because the gauge is module-level and every service exports it as 0. Scoped to the reconciler job. - SvcforgeTaskFailed latched forever on a monotonic counter. Now increase()[15m]. - the digest guard accepted the all-zeros placeholder. - worker terminationGracePeriodSeconds was 60s against a 600s helm timeout. DEAD CODE THAT SHOULD NOT HAVE BEEN - adapters/k8s.py was never called, so tenant namespaces were never created and the first provision for a new team would fail. Wired into handle_provision. - adapters/redis.py was never imported by any service. Rate limiting is now wired into the API, failing open. - Settings.check_production() had no callers. Given an explicit environment and called from every entrypoint. OBSERVABILITY - the API never called obs.setup(): no JSON logs, no trace correlation, log_json silently inert. - LogNotifier's structured fields were discarded by the stdlib->structlog bridge. - bind_task_context cleared the 'service' binding for the life of every task. - split tasks_failed into task_attempts_failed and tasks_dead_lettered. SECURITY - trivy correctly blocked the worker/reconciler images: helm 3.16.2 and kubectl 1.31.2 carry CRITICAL Go stdlib CVEs. Bumped to helm 3.21.3 and kubectl 1.35.3, which also closes a four-minor skew against the v1.35.3 cluster. TESTS THAT COULD NOT FAIL - the concurrency cap test passed on a fully serial worker. - the alert/metric cross-check asserted a hardcoded list instead of reading the chart, so it could not catch a rename on the chart side. - fixed OTel tracer-provider pollution between test files. DOCS - ARCHITECTURE.md: mermaid diagrams, user stories, and the helm-vs-ArgoCD guarantee (verified with --dry-run=server). - AGENTS.md + CLAUDE.md. - prose sweep for back-and-forth phrasing across 19 files.
63 lines
3.1 KiB
Docker
63 lines
3.1 KiB
Docker
# syntax=docker/dockerfile:1.10
|
|
#
|
|
# svcforge api. Build from the REPO ROOT:
|
|
# docker buildx build -f services/api/Dockerfile -t svcforge/api:dev .
|
|
# `COPY ../..` is illegal, so the context must be the root. There is no other option.
|
|
#
|
|
# Two syncs, not one: deps change rarely and our own code changes every commit, so the
|
|
# expensive layer (third-party wheels) must land before the cheap one (our source).
|
|
|
|
FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS builder
|
|
|
|
COPY --from=ghcr.io/astral-sh/uv:0.5.11@sha256:0ac957607303916420297a4c9c213bb33fbd3c888f9cd7f4f7273596ebf42b85 /uv /usr/local/bin/uv
|
|
|
|
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never
|
|
WORKDIR /app
|
|
|
|
# --- layer 1: third-party dependencies only -------------------------------------------
|
|
# --no-install-project skips the root; --no-install-package skips our path dependency.
|
|
# Without the latter, uv would try to build svcforge-core here, where its source is not
|
|
# yet in the context, and the build would fail.
|
|
COPY pyproject.toml uv.lock ./
|
|
COPY libs/svcforge_core/pyproject.toml libs/svcforge_core/
|
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
uv sync --frozen --no-dev --no-editable \
|
|
--no-install-project --no-install-package svcforge-core
|
|
|
|
# --- layer 2: our code ----------------------------------------------------------------
|
|
COPY libs/ libs/
|
|
COPY services/api/ services/api/
|
|
COPY catalog.yaml ./
|
|
# The migrate Job runs from THIS image (migrate-job.yaml pins the api digest), so the SQL
|
|
# has to be in it. svcforge_core.migrate's fallback resolves MIGRATIONS_DIR relative to
|
|
# its own __file__, which lands under site-packages here — a directory that does not and
|
|
# should not contain SQL — so main() returned 1 and the pre-install/pre-upgrade hook
|
|
# failed every sync. SVCFORGE_MIGRATIONS_DIR below points it at this copy instead.
|
|
COPY migrations/ migrations/
|
|
# --no-editable is what turns svcforge-core into a real wheel in site-packages.
|
|
# pyproject.toml declares it `editable = true` for local dev; an editable install in an
|
|
# image points at /app/libs, which is a source tree that need not survive the final stage.
|
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
uv sync --frozen --no-dev --no-editable && \
|
|
/app/.venv/bin/python -c 'import svcforge_core, sys; \
|
|
p = svcforge_core.__file__; \
|
|
sys.exit(0) if "site-packages" in p else sys.exit("not a wheel install: " + p)'
|
|
|
|
# --- runtime --------------------------------------------------------------------------
|
|
FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
|
|
|
|
ARG BUILD_SHA=unknown
|
|
LABEL org.opencontainers.image.title="svcforge-api" \
|
|
org.opencontainers.image.source="https://gitea.oci-oci.duckdns.org/gitea_admin/svcforge" \
|
|
org.opencontainers.image.revision="${BUILD_SHA}"
|
|
|
|
RUN useradd -u 10001 -m -s /usr/sbin/nologin svcforge
|
|
WORKDIR /app
|
|
COPY --from=builder --chown=10001:10001 /app /app
|
|
ENV PATH="/app/.venv/bin:$PATH" \
|
|
PYTHONUNBUFFERED=1 \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
SVCFORGE_MIGRATIONS_DIR=/app/migrations
|
|
USER 10001
|
|
ENTRYPOINT ["python", "-m", "services.api"]
|