From e8464b3a8beef7ed92c4ef38a2731e649b1df4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=A1=D0=B0?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B8=D0=BD?= Date: Sat, 11 Jul 2026 12:25:42 +0300 Subject: [PATCH] Add shared Erlang builder/runtime base images from registry. Publish erlang-builder and erlang-runtime as :latest, use them in prod and API test builds, auto-collect NIF libs, and sync CI to pull/push bases before eventhub build. Co-authored-by: Cursor --- .gitea/workflows/ci.yml | 32 +++++++--- docker/ApiTests.Dockerfile | 16 ++--- docker/Dockerfile | 41 ++++++------- docker/build-images.sh | 3 + docker/erlang/Dockerfile.builder | 11 ++++ docker/erlang/Dockerfile.runtime | 13 ++++ docker/scripts/collect-runtime-libs.sh | 48 +++++++++++++++ docker/scripts/verify-native-libs.sh | 23 +++++++ scripts/build-erlang-base-images.sh | 83 ++++++++++++++++++++++++++ 9 files changed, 228 insertions(+), 42 deletions(-) create mode 100644 docker/erlang/Dockerfile.builder create mode 100644 docker/erlang/Dockerfile.runtime create mode 100644 docker/scripts/collect-runtime-libs.sh create mode 100644 docker/scripts/verify-native-libs.sh create mode 100644 scripts/build-erlang-base-images.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 281fbba..8a88dbe 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -25,6 +25,29 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to registry + if: github.event_name == 'push' + env: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + bash scripts/retry-docker-build.sh 5 30 -- \ + bash -c 'echo "${REGISTRY_PASSWORD}" | docker login git.sabilin.com -u "${REGISTRY_USER}" --password-stdin' + + - name: Ensure Erlang base images (registry :latest) + run: | + set -euo pipefail + FORCE="" + if CHANGED="$(git diff --name-only HEAD~1..HEAD 2>/dev/null || true)" \ + && echo "${CHANGED}" | grep -q '^docker/erlang/'; then + FORCE="--force-build" + fi + bash scripts/build-erlang-base-images.sh ${FORCE} + + - name: Push Erlang base images + if: github.event_name == 'push' + run: bash scripts/build-erlang-base-images.sh --push + - name: Build eventhub image run: | set -euo pipefail @@ -56,15 +79,6 @@ jobs: - name: Run API tests (local CT) run: docker run --rm eventhub-api-tests:latest - - name: Login to registry - if: github.event_name == 'push' - env: - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - run: | - bash scripts/retry-docker-build.sh 5 30 -- \ - bash -c 'echo "${REGISTRY_PASSWORD}" | docker login git.sabilin.com -u "${REGISTRY_USER}" --password-stdin' - - name: Push tested eventhub image if: github.event_name == 'push' run: | diff --git a/docker/ApiTests.Dockerfile b/docker/ApiTests.Dockerfile index 5a00955..9e39542 100644 --- a/docker/ApiTests.Dockerfile +++ b/docker/ApiTests.Dockerfile @@ -1,21 +1,15 @@ -# ============================================================ -# Одноэтапный Dockerfile -# ============================================================ -FROM erlang:28-alpine +# API common_test — только builder-стадия (полный OTP + компиляторы). +ARG ERLANG_BUILDER_IMAGE=git.sabilin.com/eventhub/erlang-builder:latest +FROM ${ERLANG_BUILDER_IMAGE} -# Устанавливаем инструменты для сборки и runtime-зависимости -RUN apk add \ - make musl-dev \ - rust cargo openssl-dev libsodium-dev +WORKDIR /app -# Копируем конфигурацию и исходники COPY rebar.config ./ COPY include/ include/ COPY src/ src/ COPY test/api_*_SUITE.erl test/ COPY test/api/ test/api/ -# Компилируем и запускаем тесты RUN rebar3 compile -CMD rebar3 ct --sname ci_api_test -v \ No newline at end of file +CMD ["rebar3", "ct", "--sname", "ci_api_test", "-v"] diff --git a/docker/Dockerfile b/docker/Dockerfile index c963cc2..35348ca 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,12 @@ +# EventHub production image (multi-stage). +# Требует erlang-builder / erlang-runtime — см. scripts/build-erlang-base-images.sh +ARG ERLANG_BUILDER_IMAGE=git.sabilin.com/eventhub/erlang-builder:latest +ARG ERLANG_RUNTIME_IMAGE=git.sabilin.com/eventhub/erlang-runtime:latest + # ============================================================ # Этап 1: Сборка # ============================================================ -FROM erlang:28-alpine AS builder - -# RUN apk add --no-cache \ -RUN apk add \ - git curl make gcc musl-dev \ - rust cargo openssl-dev libsodium-dev +FROM ${ERLANG_BUILDER_IMAGE} AS builder WORKDIR /app COPY rebar.config ./ @@ -16,38 +16,35 @@ RUN rebar3 get-deps COPY include/ include/ COPY src/ src/ -# Копируем sys.config из src/config/ в config/ COPY src/config/sys.config ./config/sys.config COPY src/config/vm.args ./config/vm.args +COPY docker/scripts/collect-runtime-libs.sh /usr/local/bin/collect-runtime-libs.sh +RUN chmod +x /usr/local/bin/collect-runtime-libs.sh + RUN rebar3 as prod release RUN rebar3 as prod tar RUN mkdir -p /app/release && \ tar -xzf _build/prod/rel/eventhub/eventhub-*.tar.gz -C /app/release +# NIF (argon2 → libsodium/openssl): ldd + копия .so с builder (ABI = Alpine builder) +RUN collect-runtime-libs.sh /app/release /opt/runtime-libs + # ============================================================ # Этап 2: Финальный образ # ============================================================ -FROM alpine:3.20 +FROM ${ERLANG_RUNTIME_IMAGE} -# RUN apk add --no-cache \ -RUN apk add \ - openssl libstdc++ libgcc ncurses-libs libsodium COPY --from=builder /app/release /app -COPY --from=builder /usr/lib/libcrypto.* /usr/lib/ -COPY --from=builder /usr/lib/libssl.* /usr/lib/ +# Доп. .so, если версия apk-openssl на runtime не совпала с builder (страховка) +COPY --from=builder /opt/runtime-libs/ / -RUN mkdir -p /app/data && chmod 777 /app/data +COPY docker/scripts/verify-native-libs.sh /usr/local/bin/verify-native-libs.sh +RUN chmod +x /usr/local/bin/verify-native-libs.sh \ + && verify-native-libs.sh /app -WORKDIR /app EXPOSE 8080 8081 8445 8446 -ENV PATH="/app/erts-16.3.1/bin:$PATH" - ENV RELX_REPLACE_OS_VARS=true -CMD /app/bin/eventhub foreground -# COPY docker/entrypoint.sh /app/entrypoint.sh -# RUN chmod +x /app/entrypoint.sh -# -# ENTRYPOINT ["/app/entrypoint.sh"] \ No newline at end of file +CMD ["/app/bin/eventhub", "foreground"] diff --git a/docker/build-images.sh b/docker/build-images.sh index 8117e75..cee696f 100644 --- a/docker/build-images.sh +++ b/docker/build-images.sh @@ -1,3 +1,6 @@ +# Базовые Erlang-образы из registry (:latest) или локальная сборка +bash scripts/build-erlang-base-images.sh + # Основной образ EventHub (единственный для нод) docker build -t eventhub:latest -f docker/Dockerfile . diff --git a/docker/erlang/Dockerfile.builder b/docker/erlang/Dockerfile.builder new file mode 100644 index 0000000..78ff9cb --- /dev/null +++ b/docker/erlang/Dockerfile.builder @@ -0,0 +1,11 @@ +# EventHub — builder: OTP 28 + toolchain для rebar3 release и CT. +# erlang:28-alpine ≈ Alpine 3.23 — версия должна совпадать с Dockerfile.runtime (ALPINE_VERSION). +# Публикуется как git.sabilin.com/eventhub/erlang-builder:latest +FROM erlang:28-alpine + +RUN apk add --no-cache \ + git curl make gcc musl-dev \ + rust cargo openssl-dev libsodium-dev \ + file + +WORKDIR /build diff --git a/docker/erlang/Dockerfile.runtime b/docker/erlang/Dockerfile.runtime new file mode 100644 index 0000000..445954d --- /dev/null +++ b/docker/erlang/Dockerfile.runtime @@ -0,0 +1,13 @@ +# EventHub — runtime: libs для relx-release (ERTS внутри release). +# ALPINE_VERSION MUST match erlang:28-alpine (сейчас 3.23). +# Публикуется как git.sabilin.com/eventhub/erlang-runtime:latest +ARG ALPINE_VERSION=3.23 +FROM alpine:${ALPINE_VERSION} + +RUN apk add --no-cache \ + openssl libstdc++ libgcc ncurses-libs libsodium \ + file \ + && mkdir -p /app/data \ + && chmod 777 /app/data + +WORKDIR /app diff --git a/docker/scripts/collect-runtime-libs.sh b/docker/scripts/collect-runtime-libs.sh new file mode 100644 index 0000000..e90d1cc --- /dev/null +++ b/docker/scripts/collect-runtime-libs.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Собирает .so, нужные NIF/priv из release (ldd), для runtime-слоя. +# Исключает musl/libc — их даёт базовый alpine-образ. +# Usage: collect-runtime-libs.sh +set -eu + +RELEASE="${1:?release_dir}" +OUT="${2:?output_root}" + +mkdir -p "${OUT}" + +is_base_lib() { + case "$1" in + /lib/ld-musl-*|*/ld-musl-*|*/libc.musl-*) return 0 ;; + esac + return 1 +} + +copy_lib() { + lib="$1" + [ -e "${lib}" ] || return 0 + is_base_lib "${lib}" && return 0 + dest="${OUT}${lib}" + mkdir -p "$(dirname "${dest}")" + if [ -L "${lib}" ]; then + cp -a "${lib}" "${dest}" + else + cp -L "${lib}" "${dest}" 2>/dev/null || cp "${lib}" "${dest}" + fi +} + +# shellcheck disable=SC2044 +for so in $(find "${RELEASE}" -name '*.so' 2>/dev/null); do + ldd "${so}" 2>/dev/null | while read -r line; do + case "${line}" in + *'=>'*) + lib=$(echo "${line}" | awk '{print $3}') + [ -n "${lib}" ] && copy_lib "${lib}" + ;; + /lib/*|/usr/lib/*) + lib=$(echo "${line}" | awk '{print $1}') + copy_lib "${lib}" + ;; + esac + done +done + +echo "collect-runtime-libs: $(find "${OUT}" -name '*.so*' 2>/dev/null | wc -l) files under ${OUT}" diff --git a/docker/scripts/verify-native-libs.sh b/docker/scripts/verify-native-libs.sh new file mode 100644 index 0000000..b8ea822 --- /dev/null +++ b/docker/scripts/verify-native-libs.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Падает, если у .so из release есть unresolved ldd-зависимости. +# Usage: verify-native-libs.sh +set -eu + +RELEASE="${1:?release_dir}" +failed=0 + +# shellcheck disable=SC2044 +for so in $(find "${RELEASE}" -name '*.so' 2>/dev/null); do + if ldd "${so}" 2>/dev/null | grep -q 'not found'; then + echo "ERROR: missing libs for ${so}:" + ldd "${so}" 2>/dev/null | grep 'not found' || true + failed=1 + fi +done + +if [ "${failed}" -ne 0 ]; then + echo "verify-native-libs: FAILED" + exit 1 +fi + +echo "verify-native-libs: OK" diff --git a/scripts/build-erlang-base-images.sh b/scripts/build-erlang-base-images.sh new file mode 100644 index 0000000..71f9514 --- /dev/null +++ b/scripts/build-erlang-base-images.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Erlang builder + runtime в registry (git.sabilin.com/eventhub/*:latest). +# +# Usage: +# ./scripts/build-erlang-base-images.sh # pull latest или собрать локально +# ./scripts/build-erlang-base-images.sh --push # + push :latest в registry +# ./scripts/build-erlang-base-images.sh --force-build # пересобрать без pull +set -euo pipefail + +PUSH=0 +FORCE=0 +for arg in "$@"; do + case "${arg}" in + --push) PUSH=1 ;; + --force-build) FORCE=1 ;; + esac +done + +REGISTRY="${REGISTRY:-git.sabilin.com/eventhub}" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILDER="${REGISTRY}/erlang-builder:latest" +RUNTIME="${REGISTRY}/erlang-runtime:latest" + +check_alpine_sync() { + local erlang_ver runtime_ver erlang_mm + erlang_ver="$(docker run --rm erlang:28-alpine cat /etc/alpine-release)" + runtime_ver="$(grep '^ARG ALPINE_VERSION=' "${ROOT}/docker/erlang/Dockerfile.runtime" | cut -d= -f2)" + erlang_mm="$(echo "${erlang_ver}" | cut -d. -f1-2)" + if [[ "${erlang_mm}" != "${runtime_ver}" ]]; then + echo "ERROR: erlang:28-alpine uses Alpine ${erlang_ver}, runtime pins ${runtime_ver}" + echo " Обновите ALPINE_VERSION в docker/erlang/Dockerfile.runtime" + exit 1 + fi + echo "Alpine sync OK: ${erlang_mm} (erlang image ${erlang_ver})" +} + +build_one() { + local file="$1" + local tag="$2" + local extra=() + if docker image inspect "${tag}" >/dev/null 2>&1; then + extra+=(--cache-from "${tag}") + fi + bash "${ROOT}/scripts/retry-docker-build.sh" 5 60 -- \ + docker buildx build \ + --network=host \ + --pull \ + "${extra[@]}" \ + --file "${file}" \ + --tag "${tag}" \ + --load \ + --provenance=false \ + --sbom=false \ + "${ROOT}" +} + +try_pull_bases() { + docker pull "${BUILDER}" >/dev/null 2>&1 && docker pull "${RUNTIME}" >/dev/null 2>&1 +} + +need_build=1 +if (( FORCE == 0 )) && try_pull_bases; then + echo "Using erlang base images from registry:" + echo " ${BUILDER}" + echo " ${RUNTIME}" + need_build=0 +fi + +if (( need_build )); then + echo "Building erlang base images locally..." + check_alpine_sync + build_one "${ROOT}/docker/erlang/Dockerfile.builder" "${BUILDER}" + build_one "${ROOT}/docker/erlang/Dockerfile.runtime" "${RUNTIME}" + echo "=== erlang base images built ===" + echo " ${BUILDER}" + echo " ${RUNTIME}" +fi + +if (( PUSH )); then + bash "${ROOT}/scripts/retry-docker-build.sh" 5 30 -- docker push "${BUILDER}" + bash "${ROOT}/scripts/retry-docker-build.sh" 5 30 -- docker push "${RUNTIME}" + echo "Pushed ${BUILDER} and ${RUNTIME}" +fi