feat: Phase 2b — ingest and ETL layer - #11
Closed
TheMeinerLP wants to merge 2 commits into
Closed
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 feat/phase-2b-ingest (PR #3). The stepwise history is not preserved here: the phase branches were rebuilt from scratch as single squash commits stacked on the new clean/* branches, because a secret scanner flagged disposable test credentials in old test data commits and history cannot be rewritten in this environment.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 35899483 | Triggered | Generic High Entropy Secret | 99c38e7 | ingest/src/main/java/net/onelitefeather/apus/ingest/IngestConfig.java | View secret |
| 35899484 | Triggered | Generic High Entropy Secret | 99c38e7 | operator/src/main/java/net/onelitefeather/apus/operator/ingest/IngestJobBuilder.java | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 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.
Contributor
Author
TheMeinerLP
added a commit
that referenced
this pull request
Aug 9, 2026
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.
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.