Skip to content

feat: Phase 2b — ingest and ETL layer - #19

Open
TheMeinerLP wants to merge 2 commits into
clean2/phase-2-operatorfrom
clean2/phase-2b-ingest
Open

feat: Phase 2b — ingest and ETL layer#19
TheMeinerLP wants to merge 2 commits into
clean2/phase-2-operatorfrom
clean2/phase-2b-ingest

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Note: This replaces #11, stacked on clean2/phase-2-operator instead of clean/phase-2-operator. Same content otherwise.

Replaces #3, closed. Same content, rebuilt as a single squash commit on clean/phase-2b-ingest because a secret scanner flagged disposable test credentials in the old commit history and history cannot be rewritten in this environment. See the closing comment on #3 for detail.

Phase 2b of Apus: the ingest/ETL layer. Stacked on PR #2 (phase 2a), which must land first.

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.

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. IngestRenderContractTest runs the real ingest against a
Bukkit-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

Component Role
LayoutDetector Recognises vanilla, Bukkit and nested layouts; normalises all of them to overworld/the_nether/the_end
BundleWriter Writes the bundle, manifest last — the commit point that makes half-unpacked worlds impossible without S3 transactions
S3SourceConnector, PterodactylConnector Fetch raw data; only this step is source-specific
WorldSource/WorldIngest CRDs + reconcilers Cron-driven polling, job orchestration, retention
ingest container image Runs the ETL as a Kubernetes Job

Adding a source later costs one connector: discover() and fetch(). Transform and
load 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.

  • LayoutDetector followed symlinks and did not confine paths to the work root. A
    crafted archive with world/region -> /etc would have put system files into a bundle.
    Paths are now resolved and confined, symlinks rejected, world names validated.
  • Archive extraction had no size limits. A zip bomb could fill the node's disk and
    evict unrelated pods. Bounded by total size and entry count, and the job now declares
    ephemeral-storage.
  • Signed Pterodactyl download URLs leaked into logs through an error message. They
    are short-lived credentials for an entire server backup.
  • A WorldIngest could drive a foreign WorldSource — writing its status and
    deleting 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 IngestMain logs phase=Succeeded just before
exiting — before the Job controller sets succeeded. A reconcile landing in that window
marked the resource terminal, so the completion handler never ran: no latestBundle, no
retention, 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 latestBundle in the namespace is protected.

Known gaps

  • The Pterodactyl response envelope is the one part verified only against community docs,
    not panel source. It is now rejected if it does not match, instead of silently looking
    like "no backups available".
  • minecraftVersion comes from a new field on the world selector rather than from
    level.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.

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.
@gitguardian

gitguardian Bot commented Aug 9, 2026

Copy link
Copy Markdown

️✅ 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.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 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
TheMeinerLP force-pushed the clean2/phase-2-operator branch from d149859 to 85ac005 Compare August 9, 2026 10:30
@TheMeinerLP
TheMeinerLP force-pushed the clean2/phase-2b-ingest branch from 98276bd to e31fa4a Compare August 9, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant