feat: Phase 2b — ingest and ETL layer - #19
Open
TheMeinerLP wants to merge 2 commits into
Open
Conversation
Ingest/ETL layer on top of Phase 2a. A WorldSource is now configured once and Apus takes it from there: it polls for new world data, normalises whatever layout it finds into a versioned World Bundle in S3, and the render path picks it up without knowing where the world came from. - LayoutDetector recognises vanilla, Bukkit and nested layouts, normalising all of them to overworld/the_nether/the_end; BundleWriter writes the bundle with the manifest last so half-unpacked worlds are impossible without S3 transactions; S3SourceConnector and PterodactylConnector are the only source-specific pieces; WorldSource/WorldIngest CRDs and reconcilers handle cron-driven polling, job orchestration and retention; the ingest container image runs the ETL as a Kubernetes Job. - IngestRenderContractTest proves the contract end to end: real ingest against a Bukkit-layout world in real MinIO, rendered by the Phase 1 runner image, with no production code changes required to make it hold. - Fixes several security findings in this untrusted-input layer: LayoutDetector followed symlinks and did not confine paths to the work root; archive extraction had no size limits (zip-bomb risk); signed Pterodactyl download URLs leaked into logs; a WorldIngest could drive a foreign WorldSource because only the name was checked, not UID ownership. - Fixes two correctness bugs: an ingest could report success while its bundle stayed invisible (terminality now comes only from Job status, not a log line race), and retention could delete another source's live bundle when two sources shared a world name (bundle paths are now scoped by source). - 160 operator tests, 65 ingest tests, plus integration tests against real MinIO and k3s. This branch replaces clean/phase-2b-ingest (PR #11), stacked on clean2/phase-2-operator instead of clean/phase-2-operator. Same content otherwise.
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request. |
TheMeinerLP
force-pushed
the
clean2/phase-2-operator
branch
from
August 9, 2026 10:30
d149859 to
85ac005
Compare
TheMeinerLP
force-pushed
the
clean2/phase-2b-ingest
branch
from
August 9, 2026 10:30
98276bd to
e31fa4a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2b of Apus: the ingest/ETL layer. Stacked on PR #2 (phase 2a), which must land first.
A
WorldSourceis now configured once and Apus takes it from there: it polls for newworld data, normalises whatever layout it finds into a versioned World Bundle in S3, and
the render path picks it up without knowing where the world came from.
The contract, proven end to end
The whole point of this layer is that the render path stays ignorant of world origins.
That was a claim until now.
IngestRenderContractTestruns the real ingest against aBukkit-layout world in real MinIO, then renders the resulting bundle with the phase 1
runner image — and a tile lands in the map bucket. No production code had to change
for the contract to hold.
What this delivers
LayoutDetectoroverworld/the_nether/the_endBundleWriterS3SourceConnector,PterodactylConnectorWorldSource/WorldIngestCRDs + reconcilersingestcontainer imageAdding a source later costs one connector:
discover()andfetch(). Transform andload are shared.
Security findings fixed in this branch
This is the layer that handles untrusted input — unpacked server backups and
uploaded archives — so the findings here matter more than elsewhere.
LayoutDetectorfollowed symlinks and did not confine paths to the work root. Acrafted archive with
world/region -> /etcwould have put system files into a bundle.Paths are now resolved and confined, symlinks rejected, world names validated.
evict unrelated pods. Bounded by total size and entry count, and the job now declares
ephemeral-storage.are short-lived credentials for an entire server backup.
WorldIngestcould drive a foreignWorldSource— writing its status anddeleting its bundles — because only the name was checked. Ownership is now verified by
name and UID.
Two critical correctness bugs, both caught by review
An ingest could report success while its bundle stayed invisible. The reconciler took
the terminal phase from the pod log, but
IngestMainlogsphase=Succeededjust beforeexiting — before the Job controller sets
succeeded. A reconcile landing in that windowmarked the resource terminal, so the completion handler never ran: no
latestBundle, noretention, no retry, and a complete bundle sitting in S3 that nothing referenced.
Terminality now comes only from Job status.
Retention could delete another source's live bundle. Bundle paths were keyed by
tenant and world name, so two sources with a world called
world— the Minecraft default— shared a prefix, and pruning crossed the boundary. Paths are now scoped by source, and
every source's
latestBundlein the namespace is protected.Known gaps
not panel source. It is now rejected if it does not match, instead of silently looking
like "no backups available".
minecraftVersioncomes from a new field on the world selector rather than fromlevel.dat— the project carries no NBT dependency by design.Testing
160 operator tests, 65 ingest tests, plus integration tests against real MinIO and k3s.
Container-based tests stay out of
build:./gradlew :ingest:integrationTest,./gradlew :runner:integrationTest.A re-review of the final fix wave is still outstanding — the two critical fixes were
verified directly, the remainder rests on the fix agent's own test evidence.