# 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.95

########## fetch wimboot (and a sanity-check fetch of upstream iPXE) ##########
# v0.4.61: we no longer ship the boot.ipxe.org iPXE binaries directly;
# instead we build iPXE from source with IMAGE_PNG enabled (see the
# ipxe-build stage below). The fetch stage still pulls wimboot (a
# pre-signed binary from ipxe/wimboot's GitHub release) since that's
# unrelated to the PNG concern.
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
RUN mkdir -p assets/ipxe && \
    curl --fail --silent --show-error --location \
      -o assets/ipxe/wimboot \
      https://github.com/ipxe/wimboot/releases/latest/download/wimboot \
    || echo "wimboot fetch failed; Windows toggle will stay disabled"

########## build iPXE from source with IMAGE_PNG enabled ##########
# This stage replaces the old "grab pre-built binaries from
# boot.ipxe.org" path. The shipped binaries there are built with the
# default config which omits `IMAGE_PNG`, so the `console --picture`
# call in render_menu silently no-ops — operator logos never paint.
# Building from source lets us flip the one flag we need.
#
# Cross-compilation: x86_64 + i386 use the native toolchain that ships
# in the rust:bookworm base; arm64 uses gcc-aarch64-linux-gnu. The four
# output binaries match the names openpxe-ipxe-assets expects in
# assets/ipxe/.
FROM rust:${RUST_VERSION}-bookworm AS ipxe-build
RUN apt-get update \
 && apt-get install -y --no-install-recommends \
        git build-essential liblzma-dev mtools genisoimage syslinux \
        gcc-aarch64-linux-gnu \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /build
# Pin to a recent iPXE master tip via shallow clone. iPXE doesn't tag
# releases; pinning the SHA in source would be a periodic chore. The
# tradeoff is that "rebuild the container" silently picks up upstream
# patches — for a boot loader this is the right side of the
# pin-vs-fresh tradeoff (we want CVE fixes ASAP and the PXE chain is
# the trusted base).
RUN git clone --depth=1 https://github.com/ipxe/ipxe.git ipxe
WORKDIR /build/ipxe/src
# Feature flags landed via the `config/local/` override files iPXE's
# config system reads after `config/general.h`. We enable just the
# image format + framebuffer console plumbing — everything else stays
# at the upstream default. `keep-debug` is off; `parserrors` is off; we
# pin a small set of useful tweaks.
RUN mkdir -p config/local \
 && printf '%s\n' \
      '#define IMAGE_PNG'           \
      '#define CONSOLE_FRAMEBUFFER' \
      '#define CONSOLE_VESAFB'      \
      '#define DOWNLOAD_PROTO_HTTPS' \
      '#define NSLOOKUP_CMD'        \
      '#define NTP_CMD'             \
    > config/local/general.h
# Each arch builds to its own `bin-*` directory. We copy the four
# output binaries into /out/ with the names openpxe-ipxe-assets
# expects. Stripping the binaries saves ~30% — they go into the rust
# binary via include_bytes! so the savings ripple through the final
# image.
RUN mkdir -p /out && \
    make -j"$(nproc)" bin/undionly.kpxe && \
    cp bin/undionly.kpxe /out/undionly.kpxe && \
    make -j"$(nproc)" bin-x86_64-efi/snponly.efi && \
    cp bin-x86_64-efi/snponly.efi /out/snponly.efi && \
    make -j"$(nproc)" bin-x86_64-efi/ipxe.efi && \
    cp bin-x86_64-efi/ipxe.efi /out/ipxe.efi && \
    make -j"$(nproc)" bin-i386-efi/snponly.efi && \
    cp bin-i386-efi/snponly.efi /out/snponly-i386.efi && \
    make -j"$(nproc)" CROSS_COMPILE=aarch64-linux-gnu- bin-arm64-efi/snponly.efi && \
    cp bin-arm64-efi/snponly.efi /out/snponly-arm64.efi && \
    ls -lh /out/

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

# v0.4.5: build a fully static musl binary (matches Bootimus v0.1.70's
# move). The resulting `/openpxe` has no glibc dependency at all, which:
#   - Lets the runtime stage be any Linux distro (we still ship Debian
#     slim for the `samba` / `wimtools` / `nfs-common` shellouts, but a
#     scratch/distroless variant becomes a one-line swap).
#   - Cuts a class of "GLIBC_2.39 not found" surprises when running on
#     older RHEL/Rocky hosts that don't match Debian 12's libc version.
#   - Sidesteps cross-compilation snags (the binary is its own world).
#
# x86_64-unknown-linux-musl is fully static by default (no extra
# RUSTFLAGS needed). musl-tools provides the linker.
RUN apt-get update \
 && apt-get install -y --no-install-recommends musl-tools \
 && rm -rf /var/lib/apt/lists/* \
 && rustup target add x86_64-unknown-linux-musl

# 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 Cargo.lock ./
COPY crates/ crates/
# v0.4.61: iPXE binaries come from our own source-built stage with
# IMAGE_PNG enabled. wimboot still comes from the fetch stage (it's
# from ipxe/wimboot's GitHub release, separately signed).
COPY --from=ipxe-build /out/ /src/assets/ipxe/
COPY --from=fetch /src/assets/ipxe/wimboot /src/assets/ipxe/wimboot

# Cache cargo registry + target across builds. The mtime 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 --target x86_64-unknown-linux-musl --bin openpxe && \
    cp target/x86_64-unknown-linux-musl/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
# v0.4.5: the openpxe binary itself is now built against musl and is
# fully static — no glibc dependency. The runtime stage still ships
# Debian slim because OpenPXE shells out to the four packages below for
# functionality we deliberately don't reimplement in-process:
#   wimtools   - `wimlib-imagex`, used to inject startnet.cmd into boot.wim.
#   samba      - `smbd` serves extracted Windows install media on :445 so
#                WinPE can `net use`. Guest read-only, scoped to
#                /var/lib/openpxe/smb.
#   nfs-common - `mount.nfs` / `mount.nfs4` for the Storage tab's NFS
#                share manager. Mount requires CAP_SYS_ADMIN; without it
#                mount(2) returns EPERM and the manager surfaces a clear
#                error in the UI.
#   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).
# A future "openpxe-static" variant could drop everything except the
# binary onto distroless once we move the Windows + NFS legs to
# in-process Rust crates.

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"]
