# syntax=docker/dockerfile:1.7
#
# OpenPXE — multi-stage build.
#
# Design:
#   - stage `fetch`: runs scripts/fetch-ipxe.sh to pull official iPXE binaries
#     into assets/ipxe/ so the rust build can embed them via rust-embed.
#   - stage `build`: compiles the workspace with cargo in release mode.
#   - stage `runtime`: Debian slim image with setcap for NET_BIND_SERVICE,
#     running as a non-root UID. No shell in PATH for the service user;
#     attacker surface is just the openpxe binary + libc.
#
# Why not distroless? We want setcap support and easy debug (`oc rsh`).
# Debian slim at ~75 MB + binary ~25 MB is fine for a PXE server that
# spends most of its life idle.

ARG RUST_VERSION=1.82

########## fetch iPXE binaries ##########
FROM debian:12-slim AS fetch
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY scripts/fetch-ipxe.sh scripts/fetch-ipxe.sh
RUN mkdir -p assets/ipxe && bash scripts/fetch-ipxe.sh

########## build openpxe ##########
FROM rust:${RUST_VERSION}-bookworm AS build
WORKDIR /src

# Copy the whole workspace in one go. We used to do a two-pass "cache-prime
# with stubs, then real build" dance for dep-compile reuse; that turned out
# to silently serve stale stub binaries when cargo's fingerprint didn't
# notice the source swap. A single build is ~1.5 min longer on cold cache
# but guarantees the binary reflects the sources we copied.
# Do not copy rust-toolchain.toml into the image. The local workspace pins
# developer tooling, but inside Docker we intentionally use the Rust version
# selected by the base image. Copying rust-toolchain.toml with
# `channel = "stable"` makes rustup download a second full toolchain during
# `cargo build`, which is slow and can exhaust small Colima/CI disks.
COPY Cargo.toml ./
COPY crates/ crates/
COPY --from=fetch /src/assets/ipxe /src/assets/ipxe

# Cache cargo registry + target across builds. The `--no-edit` touch is
# belt-and-suspenders: cargo occasionally misses mtime-only changes on
# networked FS; this forces a fingerprint check.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/src/target,sharing=locked \
    find crates -name '*.rs' -exec touch {} + && \
    cargo build --release --bin openpxe && \
    cp target/release/openpxe /openpxe && \
    ls -l /openpxe

########## runtime ##########
FROM debian:12-slim AS runtime
RUN apt-get update \
 && apt-get install -y --no-install-recommends \
        ca-certificates libcap2-bin tini gosu iproute2 \
        wimtools samba nfs-common \
 && rm -rf /var/lib/apt/lists/* \
 && useradd --system --uid 10001 --home-dir /var/lib/openpxe --shell /usr/sbin/nologin openpxe \
 && mkdir -p /var/lib/openpxe/isos /var/lib/openpxe/work /var/lib/openpxe/smb \
 && chown -R openpxe:openpxe /var/lib/openpxe
# Runtime deps explained:
#   wimtools  - provides `wimlib-imagex`, used to inject startnet.cmd into boot.wim.
#   samba     - `smbd` serves extracted Windows install media on :445 for WinPE
#               to `net use`. Guest read-only, scoped to /var/lib/openpxe/smb.
#   nfs-common - provides `mount.nfs` / `mount.nfs4` for the Storage tab's
#               NFS share manager. Mount also requires the container to run
#               with CAP_SYS_ADMIN — without it, mount(2) returns EPERM and
#               the manager surfaces a clear error in the UI instead of
#               failing silently.
#   iproute2  - `ip addr` / `ip route` for the auto-detected Network tab
#               fields (NIC name, subnet mask, default gateway). Tiny,
#               always available; we don't pull in netlink crates for
#               this one-shot startup probe.
#   gosu      - drops privileges cleanly from root after the entrypoint fixes
#               bind-mount ownership (common OpenShift/Docker UX issue).
# Windows-specific tools only activate when the WebUI toggle is on.

COPY --from=build /openpxe /usr/local/bin/openpxe
COPY deploy/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

# Grant the binary the ability to bind <1024 ports as a non-root user.
# This is the only capability OpenPXE needs for proxy-mode DHCP + TFTP + HTTP.
RUN setcap cap_net_bind_service=+ep /usr/local/bin/openpxe

# IMPORTANT: we do NOT `USER openpxe` here. The entrypoint runs as root,
# chowns the mounted data dirs, then execs the binary via gosu as openpxe.
# OpenShift ignores USER directives anyway (it injects its own uid), and
# there entrypoint.sh's non-root branch just execs directly.
WORKDIR /var/lib/openpxe

ENV OPENPXE_ISO_DIR=/var/lib/openpxe/isos \
    OPENPXE_WORK_DIR=/var/lib/openpxe/work \
    OPENPXE_LOG=info,openpxe=info

EXPOSE 67/udp 69/udp 4011/udp 80/tcp 445/tcp

ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
