feat(build): make the user-modpack build lane read its context (closes the last functional gap)

A submitted modpack was durable but unreadable: the uploads PVC cannot cross
namespaces (felis-api mounts it; Kaniko runs in felis-build) and the s3 lane
handed the sandboxed build Pod no credentials, so NO user build could ever
consume its context. The transport is now the API itself:

- submit: derived context refs become the internal-face URL
  /api/v1/internal/submissions/{id}/context (service-token gated), and Blobs
  gains Open (local + s3) with an ErrBlobNotFound sentinel for the route's 404.
- api: serves that route on the internal face only (openapi.yaml updated; the
  route-coverage test enforces it).
- build: an http(s) context renders a context-fetch initContainer (the felis
  image's new fetch-context entrypoint) that streams the blob with the
  namespace-local service-token Secret — never mounted into Kaniko — and
  extracts it under a zip-slip guard into a size-limited emptyDir that Kaniko
  reads read-only as --context=/context.
- platform/install: the api Deployment carries its own internal base URL; the
  build namespace gets the token Secret through the existing replica mechanism
  (bootstrap.sh + felis setup); the build egress lock opens exactly the control
  namespace on the internal port.
- cmd/felis: fetch-context entrypoint (registered, documented, unit-tested for
  escapes/symlinks/non-gzip).

Tests cover rendering, hardening, the s3/local Open paths, and the route's
404/503 mapping. Verified next on the real single-node cluster with Kaniko.
This commit is contained in:
Lemon-miaow committed 2026-09-22 22:45:09 +08:00
1 parent 0c8e29b05a
commit f79e5ebb5e
26 files changed
+1080 -72

No files matched your search

+16 -15
View File
@@ -42,21 +42,22 @@ A grep across `*.md` and `*.go` returns both sets; only the Go ones are seams.
does not exist. `felis update` runs with a zero window, under which every
`Scheduled` component degrades to a notify, so no path can currently claim an
apply is under way.
- `internal/submit/blobstore.go:40` — the uploads PVC is mounted into felis-api but
not into the Kaniko build Pod, so a submitted context is durable at the derived
location without yet being readable by the build that consumes it. Audited
2026-09-22: this is not a missing volume line — a PVC cannot cross namespaces
(uploads live in the control namespace; build Pods run in `felis-build`), so the
fix is a transport, not a mount. The `s3://` lane does not close it either: the
build Job carries no AWS credentials (no env, and the weak SA's token is
deliberately unmounted, so no IAM either). Options on the table: (a) object
storage with credentials plumbed into the build Pod as a per-build Secret plus an
egress allowance; (b) a context-handoff PVC/Job pair in `felis-build` fed from
the API side; (c) a node-local path both sides mount (single-node only, and it
hands an arbitrary Dockerfile a filesystem view — needs its own security review).
Kaniko/Trivy images are external-only by default; `[registry] kaniko_image /
trivy_image / build_cpu_limit / build_mem_limit` now override them for mirrored
or air-gapped installs.
- `internal/submit/blobstore.go` — CLOSED 2026-09-22. The uploads PVC still cannot
cross namespaces, so the transport went through the API instead of a mount: the
derived context ref is now the internal-face URL
(`/api/v1/internal/submissions/{id}/context`, service-token gated), the build
Job's `context-fetch` initContainer streams it with `felis fetch-context` and
extracts under a zip-slip guard into a size-limited emptyDir, and Kaniko builds
`--context=/context`. The token reaches the build namespace through the same
Secret-replica mechanism the login gate uses (bootstrap + `felis setup`), and the
build egress lock allows exactly the control namespace on the internal port.
Uniform for local and s3:// stores — neither hands the sandboxed build Pod a
filesystem view or object-store credentials. Kaniko/Trivy images are
external-only by default; `[registry] kaniko_image / trivy_image /
build_cpu_limit / build_mem_limit` override them for mirrored or air-gapped
installs, and Trivy's vulnerability DB download needs the same treatment (a
`package_source_cidrs` allowance or an internal `TRIVY_DB_REPOSITORY` mirror) or
the scan step fails closed on an egress-locked install.
## Built; only its I/O is unverifiable from this repo
+28
View File
@@ -607,6 +607,34 @@ paths:
'404':
$ref: '#/components/responses/NotFound'
/api/v1/internal/submissions/{id}/context:
get:
tags: [submissions-internal]
operationId: internalSubmissionContext
summary: Stream a submission's stored build-context tarball to the build Pod.
description: >-
The build Job's fetch initContainer cannot mount the control-plane uploads
PVC (a PVC does not cross namespaces) and holds no object-store
credentials, so the API that stored the blob streams it here. Served on
the internal face (service token, no Zero Trust).
x-felis-face: [internal]
x-felis-tier: service
security: [{ serviceToken: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200':
description: The stored gzip tarball, verbatim.
content:
application/gzip:
schema: { type: string, format: binary }
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/api/v1/internal/servers/{name}/join-event:
post:
tags: [servers-internal]