# Lint phase, built alone by script/lint. The linter is invoked directly
# rather than through `make lint`, which is itself a docker build and
# would recurse into a daemon that does not exist in a build step.
# golangci/golangci-lint:v2.14.0 (Debian-based), 2026-10-06
FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN golangci-lint run --config .golangci.yml ./...

# Test phase, built alone by script/test. -race needs cgo and so a C
# compiler, which the Debian Go image ships and the alpine one does not.
# The tests query live DNS (TESTING.md), so this step needs the network.
# -count=1 keeps Go's test result cache out of both runs, as TESTING.md
# requires. -timeout 90s is a backstop above the 60-second cap on the
# suite. The rerun with -v only shows details: the build fails however
# it ends, because the first run already failed.
# golang 1.25.7-trixie, 2026-10-06
FROM golang@sha256:2b174ffcf56c7ad0c47d30d2630693265639ddf2a5141149c2da34db921791b4 AS test
# The file permission tests skip themselves as root, so the tests run as
# an ordinary user, whose home directory holds Go's build cache.
RUN useradd --create-home tester
USER tester
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go test -count=1 -race -timeout 90s -cover ./... || \
    { echo "--- Rerunning with -v for details ---"; \
      go test -count=1 -race -timeout 90s -v ./...; exit 1; }

# Markdown formatting with prettier, at the version package.json and
# yarn.lock pin, so it is never installed on the host. script/fmt-check
# builds the fmt-check stage and script/fmt the fmt-out stage; the image
# does not depend on any of these stages, so a plain `docker build .`
# skips them.
# node:22-bookworm-slim, 2026-09-05
FROM node@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5 AS nodedeps
# prettier lives outside /src, so the COPY of the repo cannot overwrite
# it and node_modules is not in the tree prettier walks.
WORKDIR /tools
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --non-interactive --no-progress
ENV PATH="/tools/node_modules/.bin:${PATH}"
WORKDIR /src

# --config rather than discovery: a missing .prettierrc is then an error
# instead of prettier's defaults, under which every wrap passes.
# --no-editorconfig so .prettierrc alone sets the style.
FROM nodedeps AS fmt-check
COPY . .
RUN prettier --config .prettierrc --no-editorconfig --check "**/*.md"

# Only the markdown is copied out, with its paths, so the export cannot
# put anything else back over the working tree.
FROM nodedeps AS fmt
COPY . .
RUN prettier --config .prettierrc --no-editorconfig --write "**/*.md" && \
    mkdir -p /out && \
    find . -name '*.md' -type f -exec cp --parents '{}' /out/ ';'

FROM scratch AS fmt-out
COPY --from=fmt /out/ /

# Build stage. Nothing is wanted from the lint and test phases; the
# copies are what make BuildKit build them first, so this stage cannot
# run unless lint and test passed.
# golang 1.25-alpine, 2026-02-28
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
COPY --from=lint /src/go.sum /dev/null
COPY --from=test /src/go.sum /dev/null
RUN apk add --no-cache git
# A tar-stream context keeps the sender's file owners, which git refuses.
RUN git config --system --add safe.directory /src
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .

# The VERSION build arg when one is given (script/docker and
# script/cibuild pass one), otherwise `git describe --tags --always` on
# the .git in the build context. With .git present, a version that is
# still empty, dev or unknown fails the build: git is missing or could
# not read the checkout.
ARG VERSION
RUN VERSION="${VERSION:-$(git describe --tags --always)}"; \
    if [ -e .git ]; then \
        case "$VERSION" in ""|dev|unknown) \
            echo "version is '$VERSION' although .git is present" >&2; \
            exit 1 ;; \
        esac; \
    fi; \
    CGO_ENABLED=0 go build -trimpath \
        -ldflags="-s -w -X main.Version=${VERSION}" \
        -o /src/bin/dnswatcher ./cmd/dnswatcher/

# Runtime stage, and the last one: a plain `docker build .` builds this
# stage's chain and nothing else.
# alpine 3.21, 2026-02-28
FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709

RUN apk add --no-cache ca-certificates tzdata su-exec

COPY --from=builder /src/bin/dnswatcher /usr/local/bin/dnswatcher
COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh

# dnswatcher runs as this unprivileged user. The entrypoint creates the
# data directory and gives it to this user on every start.
RUN addgroup -S -g 10001 dnswatcher \
    && adduser -S -G dnswatcher -u 10001 dnswatcher

ENV DNSWATCHER_DATA_DIR=/var/lib/dnswatcher

# Config loading also reads a `.env` file and a file named `dnswatcher`
# (any config extension, or none) from the working directory. `/` holds
# neither, so every setting comes from the environment. Do not make the
# data directory, or the binary's directory, the working directory.
WORKDIR /

# No USER: the entrypoint must start as root to set up the data
# directory; it then runs dnswatcher as the dnswatcher user.

EXPOSE 8080

# busybox wget (already in alpine) probes the health endpoint every 10
# seconds, so the container is healthy well before upaas reads its health
# 60 seconds after a deploy and fails the deploy unless it is healthy.
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \
    CMD wget -q -O /dev/null "http://127.0.0.1:${PORT:-8080}/.well-known/healthcheck" || exit 1

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