Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b69c3b67e2 | |||
| cc3621a12b | |||
| ea63e904b4 | |||
| 20db51b7a2 | |||
| e153143cc1 | |||
| 19f0526b2e | |||
| 0f2a646df2 | |||
| 04e7b5667f | |||
| 80b33d5132 | |||
| bbe0886342 | |||
| aac82e6d23 | |||
| 336df50ca0 | |||
| 88bdaf7d3a | |||
| a941525f2e | |||
| b13f5e8ac5 | |||
| d2f6e89d58 | |||
| 789c1854fe | |||
| ec44dcca4e | |||
| 71e9891247 | |||
| 2adc02cb3f |
@@ -0,0 +1,325 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
# CI-пайплайн с гейтами (issue #17, Refs #15)
|
||||||
|
# lint -> unit (coverage >= 70%) -> security (bandit/pip-audit) -> build -> image-scan (trivy) -> push staging-<sha>
|
||||||
|
# Гейты (роняют pipeline):
|
||||||
|
# * unit — покрытие < 70 % (--cov-fail-under=70)
|
||||||
|
# * security — bandit HIGH, pip-audit: любая известная уязвимость
|
||||||
|
# * image-scan — trivy: HIGH/CRITICAL в образе
|
||||||
|
# * build стартует только после зелёных lint+unit+security; push — только после зелёного image-scan.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["feat/**", "fix/**", "chore/**", "ci/**", "main"]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
PYTHON_VERSION: "3.12"
|
||||||
|
REGISTRY: 192.168.1.111:3000
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ─────────────────────────────── 1. LINT ────────────────────────────────
|
||||||
|
lint:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
has_dockerfile: ${{ steps.dockerfile.outputs.has_dockerfile }}
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
# Дефект Gitea git-http: upload-pack отдаёт пустой pack (Content-Length: 90),
|
||||||
|
# git clone/fetch падает "early EOF / invalid index-pack output".
|
||||||
|
# Обход: эндпоинт archive/<sha>.tar.gz работает — забираем исходники им.
|
||||||
|
# Когда git-http починят, шаг просто перестанет срабатывать (outcome != failure).
|
||||||
|
- name: "checkout fallback (archive tarball)"
|
||||||
|
if: steps.checkout.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
|
||||||
|
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
|
||||||
|
echo "sources: archive fallback, $(ls -1 | wc -l) entries"
|
||||||
|
|
||||||
|
- name: "python 3.12"
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt -r requirements-contract.txt
|
||||||
|
|
||||||
|
- name: ruff check
|
||||||
|
run: ruff check backend
|
||||||
|
|
||||||
|
- name: ruff format --check
|
||||||
|
run: ruff format --check backend
|
||||||
|
|
||||||
|
- name: "mypy (strict)"
|
||||||
|
run: mypy backend
|
||||||
|
|
||||||
|
- name: detect deploy/Dockerfile
|
||||||
|
id: dockerfile
|
||||||
|
run: |
|
||||||
|
if [ -f deploy/Dockerfile ]; then
|
||||||
|
echo "has_dockerfile=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "deploy/Dockerfile найден — jobs build/image-scan/push активны"
|
||||||
|
else
|
||||||
|
echo "has_dockerfile=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::warning::deploy/Dockerfile отсутствует — build/image-scan/push пропущены (ждут merge #18 / PR #49)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─────────────────── 2. UNIT + ГЕЙТ ПОКРЫТИЯ >= 70 % ───────────────────
|
||||||
|
unit:
|
||||||
|
name: unit
|
||||||
|
needs: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: "checkout fallback (archive tarball)"
|
||||||
|
if: steps.checkout.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
|
||||||
|
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
|
||||||
|
|
||||||
|
- name: "python 3.12"
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt -r requirements-contract.txt
|
||||||
|
|
||||||
|
- name: "pytest + coverage gate (fail below 70%)"
|
||||||
|
run: |
|
||||||
|
python -m pytest \
|
||||||
|
--cov=awg_agent --cov=awg_api \
|
||||||
|
--cov-report=term-missing \
|
||||||
|
--cov-report=xml:coverage.xml \
|
||||||
|
--cov-report=html:htmlcov \
|
||||||
|
--cov-fail-under=70
|
||||||
|
|
||||||
|
- name: coverage summary
|
||||||
|
if: always()
|
||||||
|
run: python -m coverage report --sort=cover
|
||||||
|
|
||||||
|
- name: upload coverage artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: coverage
|
||||||
|
path: |
|
||||||
|
coverage.xml
|
||||||
|
htmlcov/
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
# ───────────── 3. SECURITY: bandit (HIGH) + pip-audit ──────────────────
|
||||||
|
security:
|
||||||
|
name: security
|
||||||
|
needs: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: "checkout fallback (archive tarball)"
|
||||||
|
if: steps.checkout.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
|
||||||
|
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
|
||||||
|
|
||||||
|
- name: "python 3.12"
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
|
- name: install scanners
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install "bandit[toml]>=1.7" "pip-audit>=2.7"
|
||||||
|
pip install -r requirements.txt -r requirements-contract.txt
|
||||||
|
|
||||||
|
- name: "bandit -lll (HIGH severity gate)"
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
bandit -lll -r backend 2>&1 | tee bandit-report.txt
|
||||||
|
bandit -lll -f json -o bandit-report.json -r backend || true
|
||||||
|
echo "bandit: HIGH/CRITICAL не найдены"
|
||||||
|
|
||||||
|
- name: "pip-audit (known vulnerabilities gate)"
|
||||||
|
run: |
|
||||||
|
pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off
|
||||||
|
|
||||||
|
- name: pip-audit json report
|
||||||
|
if: always()
|
||||||
|
run: pip-audit -r requirements.txt -r requirements-contract.txt --progress-spinner off --format json -o pip-audit-report.json || true
|
||||||
|
|
||||||
|
- name: upload security artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: security-reports
|
||||||
|
path: |
|
||||||
|
bandit-report.txt
|
||||||
|
bandit-report.json
|
||||||
|
pip-audit-report.json
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
# ───────────────── 4. BUILD: docker build (нужен #18) ──────────────────
|
||||||
|
build:
|
||||||
|
name: build
|
||||||
|
needs: [lint, unit, security]
|
||||||
|
if: needs.lint.outputs.has_dockerfile == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: "checkout fallback (archive tarball)"
|
||||||
|
if: steps.checkout.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
|
||||||
|
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
|
||||||
|
|
||||||
|
- name: "guard: deploy/Dockerfile"
|
||||||
|
run: |
|
||||||
|
if [ ! -f deploy/Dockerfile ]; then
|
||||||
|
echo "::error::deploy/Dockerfile отсутствует: сборка образа ждёт merge #18 (PR #49)."
|
||||||
|
echo "Пайплайн целиком проверен на ветке feat/17-verify (она же + deploy/Dockerfile)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: image meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
SHA_SHORT="${GITHUB_SHA::8}"
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${SHA_SHORT}"
|
||||||
|
echo "sha_short=${SHA_SHORT}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
|
||||||
|
echo "image: ${IMAGE}"
|
||||||
|
|
||||||
|
- name: docker build
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
docker build --pull -f deploy/Dockerfile -t "$IMAGE" . 2>&1 | tee build.log
|
||||||
|
|
||||||
|
- name: image digest
|
||||||
|
run: |
|
||||||
|
docker image inspect "$IMAGE" --format 'id={{.Id}} size={{.Size}}'
|
||||||
|
docker image inspect "$IMAGE" --format '{{.Id}}' > image-id.txt
|
||||||
|
|
||||||
|
- name: upload build artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: "build-${{ github.sha }}"
|
||||||
|
path: |
|
||||||
|
build.log
|
||||||
|
image-id.txt
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
# ──────── 5. IMAGE-SCAN: trivy, гейт HIGH/CRITICAL ────────────────────
|
||||||
|
image-scan:
|
||||||
|
name: image-scan
|
||||||
|
needs: [lint, build]
|
||||||
|
if: needs.lint.outputs.has_dockerfile == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TRIVY_VERSION: "0.76.1"
|
||||||
|
TRIVY_SEVERITY: "HIGH,CRITICAL"
|
||||||
|
steps:
|
||||||
|
# trivy запускается официальным образом через docker-сокет: не требует
|
||||||
|
# скачивания бинаря (github releases отдаёт медленно) + БД кэшируется в
|
||||||
|
# /opt/trivy-cache на build-хосте.
|
||||||
|
- name: "trivy image (HIGH/CRITICAL -> fail)"
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}"
|
||||||
|
echo "scan: ${IMAGE}"
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v /opt/trivy-cache:/root/.cache/trivy \
|
||||||
|
"aquasec/trivy:${TRIVY_VERSION}" image \
|
||||||
|
--image-src docker --scanners vuln \
|
||||||
|
--severity "$TRIVY_SEVERITY" --ignore-unfixed \
|
||||||
|
--exit-code 1 --format table "$IMAGE" 2>&1 | tee trivy-report.txt
|
||||||
|
|
||||||
|
- name: trivy json report
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}"
|
||||||
|
mkdir -p reports
|
||||||
|
docker run --rm \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v /opt/trivy-cache:/root/.cache/trivy \
|
||||||
|
-v "$PWD/reports:/reports" \
|
||||||
|
"aquasec/trivy:${TRIVY_VERSION}" image \
|
||||||
|
--image-src docker --scanners vuln \
|
||||||
|
--severity "$TRIVY_SEVERITY" --ignore-unfixed \
|
||||||
|
--format json -o /reports/trivy-report.json "$IMAGE" || true
|
||||||
|
cp reports/trivy-report.json ./trivy-report.json 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: upload trivy report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: trivy-report
|
||||||
|
path: |
|
||||||
|
trivy-report.txt
|
||||||
|
trivy-report.json
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
# ─────── 6. PUSH: staging-<sha> в registry Gitea ──────────────────────
|
||||||
|
push:
|
||||||
|
name: push
|
||||||
|
needs: [lint, build, image-scan]
|
||||||
|
if: needs.lint.outputs.has_dockerfile == 'true' && github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: docker login
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
# Приоритет — секрет с правом write:package (REGISTRY_TOKEN), иначе встроенный токен Actions.
|
||||||
|
TOKEN="${{ secrets.REGISTRY_TOKEN }}"
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
TOKEN="${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
fi
|
||||||
|
echo "$TOKEN" | docker login "$REGISTRY" -u "${{ github.actor }}" --password-stdin
|
||||||
|
|
||||||
|
- name: "push staging-<sha>"
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}:staging-${GITHUB_SHA::8}"
|
||||||
|
docker push "$IMAGE" 2>&1 | tee push.log
|
||||||
|
echo "${IMAGE}" | tee pushed-image.txt
|
||||||
|
|
||||||
|
- name: upload push record
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: pushed-image
|
||||||
|
path: |
|
||||||
|
push.log
|
||||||
|
pushed-image.txt
|
||||||
|
retention-days: 14
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
#
|
||||||
|
# awg-dash — единый образ: AmneziaWG gateway + policy routing + REST API + Web UI.
|
||||||
|
# Задача: #18 (эпик #15). ADR: docs/adr/002-single-image-modular-monolith.md
|
||||||
|
#
|
||||||
|
# ВАЖНО: AMNEZIAWG_GO_REF / AMNEZIAWG_TOOLS_REF по умолчанию указывают на master
|
||||||
|
# и должны быть запинены на проверенный тег/коммит перед релизом (см. #45).
|
||||||
|
|
||||||
|
# Базовые образы и зависимости подняты под гейт image-scan (issue #61):
|
||||||
|
# golang:1.26-alpine — закрывает CVE Go stdlib 1.25.14 (CRITICAL CVE-2026-56857 + HIGH)
|
||||||
|
# golang.org/x/crypto v0.58.0 + golang.org/x/net v0.62.0 — закрывают CVE в ssh/* и http2/idna
|
||||||
|
# alpine:3.23 — закрывает HIGH в пакетах alpine 3.21
|
||||||
|
ARG GO_IMAGE=golang:1.26-alpine
|
||||||
|
ARG X_CRYPTO_VERSION=v0.58.0
|
||||||
|
ARG X_NET_VERSION=v0.62.0
|
||||||
|
|
||||||
|
# --- stage 1: сборка awg-утилит (Go) -----------------------------------------
|
||||||
|
FROM ${GO_IMAGE} AS go-build
|
||||||
|
|
||||||
|
ARG AMNEZIAWG_GO_REPO=https://github.com/amnezia-vpn/amneziawg-go.git
|
||||||
|
ARG AMNEZIAWG_GO_REF=master
|
||||||
|
ARG AMNEZIAWG_TOOLS_REPO=https://github.com/amnezia-vpn/amneziawg-tools.git
|
||||||
|
ARG AMNEZIAWG_TOOLS_REF=master
|
||||||
|
ARG X_CRYPTO_VERSION
|
||||||
|
ARG X_NET_VERSION
|
||||||
|
|
||||||
|
RUN apk add --no-cache git make gcc musl-dev linux-headers
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
RUN git clone --depth 1 --branch "${AMNEZIAWG_GO_REF}" "${AMNEZIAWG_GO_REPO}" amneziawg-go \
|
||||||
|
&& cd amneziawg-go \
|
||||||
|
&& go get "golang.org/x/crypto@${X_CRYPTO_VERSION}" "golang.org/x/net@${X_NET_VERSION}" \
|
||||||
|
&& go mod tidy \
|
||||||
|
&& go list -m golang.org/x/crypto golang.org/x/net \
|
||||||
|
&& make \
|
||||||
|
&& cp amneziawg-go /usr/local/bin/amneziawg-go
|
||||||
|
|
||||||
|
RUN git clone --depth 1 --branch "${AMNEZIAWG_TOOLS_REF}" "${AMNEZIAWG_TOOLS_REPO}" amneziawg-tools \
|
||||||
|
&& cd amneziawg-tools/src && make \
|
||||||
|
&& cp wg /usr/local/bin/awg \
|
||||||
|
&& if [ -f wg-quick ]; then cp wg-quick /usr/local/bin/awg-quick; fi
|
||||||
|
|
||||||
|
# --- stage 2: runtime -------------------------------------------------------
|
||||||
|
FROM alpine:3.23 AS runtime
|
||||||
|
|
||||||
|
# Пакеты базы обновляем на момент сборки: trivy --ignore-unfixed учитывает только исправимые CVE.
|
||||||
|
RUN apk upgrade --no-cache
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="awg-dash" \
|
||||||
|
org.opencontainers.image.description="AmneziaWG gateway with intelligent routing: REST API + Web UI + split-tunnel" \
|
||||||
|
org.opencontainers.image.source="http://192.168.1.111:3000/embeddevio/awg-dash"
|
||||||
|
|
||||||
|
# Сетевые утилиты: iptables/ip6tables, ipset, iproute2 (policy routing), dnsmasq (ADR-005),
|
||||||
|
# curl (HEALTHCHECK), tini (PID 1), su-exec (запуск API под непривилегированным пользователем).
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash curl iproute2 ipset iptables ip6tables dnsmasq \
|
||||||
|
python3 tini su-exec \
|
||||||
|
&& addgroup -S awg \
|
||||||
|
&& adduser -S -G awg -h /app awg
|
||||||
|
|
||||||
|
ENV PATH=/opt/venv/bin:$PATH \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONPATH=/app/backend \
|
||||||
|
AWG_CONFIG_DIR=/config \
|
||||||
|
AWG_DATA_DIR=/data \
|
||||||
|
AWG_RUN_DIR=/run/awg \
|
||||||
|
AWG_API_HOST=0.0.0.0 \
|
||||||
|
AWG_API_PORT=8080
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
# py3-pip нужен только на время сборки venv: в runtime pip не остаётся. Иначе trivy разбирает
|
||||||
|
# pip/_vendor/vendor.txt и репортит вендоренные версии (msgpack 1.1.2 / urllib3 2.7.0 /
|
||||||
|
# setuptools 70.3.0) как установленные пакеты — ложные HIGH на образе, где стоят 1.2.4/2.8.0/84.2.1 (issue #61).
|
||||||
|
RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev libffi-dev py3-pip \
|
||||||
|
&& python3 -m venv /opt/venv \
|
||||||
|
&& /opt/venv/bin/pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& /opt/venv/bin/pip install --no-cache-dir -r /app/requirements.txt \
|
||||||
|
&& apk del .build-deps \
|
||||||
|
&& rm -rf /opt/venv/lib/python3*/site-packages/pip /opt/venv/lib/python3*/site-packages/pip-* \
|
||||||
|
&& rm -f /opt/venv/bin/pip /opt/venv/bin/pip3 /opt/venv/bin/pip3.* \
|
||||||
|
&& rm -rf /usr/lib/python3*/site-packages/pip /usr/lib/python3*/site-packages/pip-* \
|
||||||
|
&& rm -f /usr/bin/pip3 /usr/bin/pip3.*
|
||||||
|
|
||||||
|
COPY backend/ /app/backend/
|
||||||
|
COPY --from=go-build /usr/local/bin/awg /usr/local/bin/awg
|
||||||
|
COPY --from=go-build /usr/local/bin/amneziawg-go /usr/local/bin/amneziawg-go
|
||||||
|
COPY deploy/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh \
|
||||||
|
&& mkdir -p /config /data /run/awg \
|
||||||
|
&& chown -R awg:awg /app /config /data /run/awg
|
||||||
|
|
||||||
|
VOLUME ["/config", "/data"]
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# healthcheck ходит в /api/v1/health (задача #31). До реализации API контейнер
|
||||||
|
# будет помечаться как unhealthy — это ожидаемое состояние до #31.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD curl -fsS "http://127.0.0.1:${AWG_API_PORT}/api/v1/health" >/dev/null || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--", "/entrypoint.sh"]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Пример клиентского конфига AmneziaWG для awg-dash.
|
||||||
|
#
|
||||||
|
# Скопируйте в том /config как awg0.conf (имя файла = имя интерфейса) или
|
||||||
|
# загрузите через Web UI (задача #38). Значения ниже — заглушки, замените своими.
|
||||||
|
# Файл в репозитории не является рабочим конфигом и не содержит реальных ключей.
|
||||||
|
|
||||||
|
[Interface]
|
||||||
|
# Адрес внутри туннеля (из панели провайдера AmneziaWG)
|
||||||
|
Address = 10.8.1.2/32
|
||||||
|
|
||||||
|
# DNS: адрес должен указывать на этот шлюз, иначе не сработает DNS-split (ADR-005)
|
||||||
|
DNS = 10.8.1.1
|
||||||
|
|
||||||
|
PrivateKey = <PRIVATE_KEY>
|
||||||
|
|
||||||
|
# Обфускация AmneziaWG (Amnezia VPN: экспорт -> AmneziaWG)
|
||||||
|
Jc = 4
|
||||||
|
Jmin = 40
|
||||||
|
Jmax = 70
|
||||||
|
S1 = 0
|
||||||
|
S2 = 0
|
||||||
|
H1 = 1
|
||||||
|
H2 = 2
|
||||||
|
H3 = 3
|
||||||
|
H4 = 4
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <SERVER_PUBLIC_KEY>
|
||||||
|
PresharedKey = <PRESHARED_KEY>
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::/0
|
||||||
|
Endpoint = <SERVER_HOST>:<SERVER_PORT>
|
||||||
|
PersistentKeepalive = 25
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Супервизор контейнера awg-dash (задача #18, ADR-002).
|
||||||
|
# Последовательность: preflight -> подготовка -> запуск awg-api под awg.
|
||||||
|
# Привилегированный агент awg-agent стартует отсюда же после реализации (#22).
|
||||||
|
#
|
||||||
|
# Роль процесса: все сетевые операции выполняет агент под root; здесь только
|
||||||
|
# проверяем, что нужные возможности и устройства проброшены, и падаем явной
|
||||||
|
# ошибкой, если контейнер запущен без них (а не тихо деградируем).
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
CONFIG_DIR="${AWG_CONFIG_DIR:-/config}"
|
||||||
|
DATA_DIR="${AWG_DATA_DIR:-/data}"
|
||||||
|
RUN_DIR="${AWG_RUN_DIR:-/run/awg}"
|
||||||
|
API_HOST="${AWG_API_HOST:-0.0.0.0}"
|
||||||
|
API_PORT="${AWG_API_PORT:-8080}"
|
||||||
|
ROUTING_TABLE_ID=100
|
||||||
|
ROUTING_TABLE_NAME=awg-vpn
|
||||||
|
|
||||||
|
log() { printf '[entrypoint] %s\n' "$*"; }
|
||||||
|
fail() { printf '[entrypoint] FATAL: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
log 'preflight'
|
||||||
|
[ -e /dev/net/tun ] || fail '/dev/net/tun не проброшен: добавьте --device=/dev/net/tun:/dev/net/tun'
|
||||||
|
iptables -t mangle -L -n >/dev/null 2>&1 || fail 'iptables недоступен: нужен --cap-add=NET_ADMIN (и/или --privileged)'
|
||||||
|
command -v awg >/dev/null 2>&1 || fail 'бинарь awg не найден в образе'
|
||||||
|
command -v amneziawg-go >/dev/null 2>&1 || log 'WARN: amneziawg-go не найден — остаётся только kernel-модуль'
|
||||||
|
command -v ipset >/dev/null 2>&1 || fail 'ipset не найден в образе'
|
||||||
|
|
||||||
|
log 'подготовка каталогов состояния'
|
||||||
|
mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$RUN_DIR"
|
||||||
|
chown -R awg:awg "$CONFIG_DIR" "$DATA_DIR" "$RUN_DIR" 2>/dev/null || true
|
||||||
|
chmod 700 "$CONFIG_DIR" "$DATA_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
|
mkdir -p /etc/iproute2
|
||||||
|
touch /etc/iproute2/rt_tables 2>/dev/null || true
|
||||||
|
if ! grep -q "^${ROUTING_TABLE_ID} ${ROUTING_TABLE_NAME}" /etc/iproute2/rt_tables 2>/dev/null; then
|
||||||
|
printf '%s %s\n' "$ROUTING_TABLE_ID" "$ROUTING_TABLE_NAME" >> /etc/iproute2/rt_tables \
|
||||||
|
|| log 'WARN: не удалось записать таблицу маршрутизации в /etc/iproute2/rt_tables'
|
||||||
|
fi
|
||||||
|
|
||||||
|
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 \
|
||||||
|
|| log 'WARN: net.ipv4.ip_forward не включён — задайте --sysctl net.ipv4.ip_forward=1 или --privileged (см. runbook #45)'
|
||||||
|
|
||||||
|
log "awg: $(awg --version 2>/dev/null | head -n 1 || echo 'version unknown')"
|
||||||
|
|
||||||
|
api_pid=''
|
||||||
|
if [ -f /app/backend/awg_api/app.py ]; then
|
||||||
|
log "запуск awg-api на ${API_HOST}:${API_PORT}"
|
||||||
|
su-exec awg /opt/venv/bin/uvicorn awg_api.app:app --host "$API_HOST" --port "$API_PORT" &
|
||||||
|
api_pid=$!
|
||||||
|
else
|
||||||
|
log 'WARN: awg_api/app.py ещё не реализован (задача #31) — API не запущен, контейнер работает только как агент'
|
||||||
|
fi
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if [ -n "$api_pid" ] && kill -0 "$api_pid" 2>/dev/null; then
|
||||||
|
log 'остановка awg-api'
|
||||||
|
kill -TERM "$api_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap stop INT TERM
|
||||||
|
|
||||||
|
if [ -n "$api_pid" ]; then
|
||||||
|
# ждём завершения API и прокидываем его код возврата в Docker
|
||||||
|
set +e
|
||||||
|
wait "$api_pid"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
log "awg-api завершился с кодом $rc"
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log 'активных процессов нет (agent #22 и api #31 ещё не реализованы) — ожидание сигнала'
|
||||||
|
while :; do
|
||||||
|
sleep 3600 &
|
||||||
|
wait $!
|
||||||
|
done
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
# CI/CD: пайплайн с гейтами (`ci.yml`)
|
||||||
|
|
||||||
|
**Владелец:** devops · **Issue:** #17 (Refs #15) · **Файл:** `.gitea/workflows/ci.yml`
|
||||||
|
**Runner:** `awg-dash-147-repo` (repo-scoped) на Docker-хосте `192.168.1.147`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Триггеры
|
||||||
|
|
||||||
|
| Событие | Ветки | Что запускается |
|
||||||
|
|---|---|---|
|
||||||
|
| `push` | `feat/**`, `fix/**`, `chore/**`, `ci/**`, `main` | полный пайплайн |
|
||||||
|
| `pull_request` (sync) | базис `main` | полный пайплайн |
|
||||||
|
| `workflow_dispatch` | любая | полный пайплайн (ручной перезапуск) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Стадии и гейты
|
||||||
|
|
||||||
|
```
|
||||||
|
lint ──┬── unit ────────┐
|
||||||
|
│ ├── build ── image-scan ── push (только push-событие)
|
||||||
|
└── security ────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Job | Что делает | Гейт (роняет pipeline) | Артефакты |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `lint` | `ruff check backend`, `ruff format --check backend`, `mypy backend` (strict) | любая ошибка линтера/типов | — |
|
||||||
|
| `unit` | `pytest --cov=awg_agent --cov=awg_api --cov-fail-under=70` | **покрытие < 70 %** | `coverage.xml`, `htmlcov/` |
|
||||||
|
| `security` | `bandit -lll -r backend`, `pip-audit -r requirements.txt` | **bandit severity HIGH**, любая известная уязвимость в зависимостях | `bandit-report.txt/.json`, `pip-audit-report.json` |
|
||||||
|
| `build` | `docker build -f deploy/Dockerfile -t <registry>/<repo>:staging-<sha8>` | ошибка сборки | `build.log`, `image-id.txt` |
|
||||||
|
| `image-scan` | `docker run aquasec/trivy:<ver> image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1` (по сокету) | **HIGH/CRITICAL в образе** | `trivy-report.txt`, `trivy-report.json` |
|
||||||
|
| `push` | `docker login` + `docker push ...:staging-<sha8>` | ошибка авторизации/пуша | `push.log`, `pushed-image.txt` |
|
||||||
|
|
||||||
|
**trivy** запускается официальным образом через docker-сокет (бинарь с github releases
|
||||||
|
в homelab качается крайне медленно), БД кэшируется в `/opt/trivy-cache` на build-хосте.
|
||||||
|
|
||||||
|
**Токен для push**: приоритет — секрет `REGISTRY_TOKEN` (PAT с правом `write:package`),
|
||||||
|
fallback — встроенный `secrets.GITHUB_TOKEN`. Проверено: PAT без `write:package` получает
|
||||||
|
`401 Unauthorized` на `POST /v2/<repo>/blobs/uploads/` — если push падает с 401, заведите секрет.
|
||||||
|
|
||||||
|
**Порядок гарантий:**
|
||||||
|
|
||||||
|
- `build` стартует только после зелёных `lint` + `unit` + `security`.
|
||||||
|
- `image-scan` — только после `build`; `push` — только после зелёного `image-scan`.
|
||||||
|
- Тег образа: `staging-<первые 8 символов GITHUB_SHA>` в registry `192.168.1.111:3000/embeddevio/awg-dash`.
|
||||||
|
|
||||||
|
## 3. Условная активация build/image-scan/push
|
||||||
|
|
||||||
|
`build`/`image-scan`/`push` выполняются, только если в репозитории есть `deploy/Dockerfile`
|
||||||
|
(вычисляется в `lint` → output `has_dockerfile`). До merge #18 (PR #49) они **скипаются**
|
||||||
|
с предупреждением в логе — это ожидаемое поведение, а не сбой.
|
||||||
|
|
||||||
|
## 4. Runner
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Контейнер | `gitea-runner-awg-repo` (`docker.io/gitea/act_runner:latest`, v0.6.1) на `192.168.1.147` |
|
||||||
|
| Область | **repo-scoped** (зарегистрирован repo-runner-token'ом для `embeddevio/awg-dash`) |
|
||||||
|
| Label | `ubuntu-latest` → образ `catthehacker/ubuntu:act-22.04` |
|
||||||
|
| Параллелизм | `capacity: 1`, `maxParallel=1` |
|
||||||
|
| Сеть job-контейнеров | `container.network: "host"` (см. п. 7 — иначе исчерпывается пул адресов Docker) |
|
||||||
|
| Docker socket | проброшен (`/var/run/docker.sock`), `container.options: ""` — v0.6.1 монтирует сокет сам, дубль в `options` даёт `Duplicate mount point` |
|
||||||
|
| Кэш инструментов | volume `act-toolcache` → `/opt/hostedtoolcache` (Python 3.12 скачивается один раз) |
|
||||||
|
| Конфиг | `/opt/gitea-runner-awg-repo/config.yaml`, состояние — `/opt/gitea-runner-awg-repo/.runner` |
|
||||||
|
|
||||||
|
### Диагностика runner'а
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.1.147
|
||||||
|
docker ps --filter name=gitea-runner # жив ли
|
||||||
|
docker logs --tail 50 gitea-runner-awg-repo # задачи, ошибки
|
||||||
|
docker logs gitea-runner-awg-repo 2>&1 | grep -E "task|Error|failed"
|
||||||
|
```
|
||||||
|
|
||||||
|
Лог job'а с его шагами и результатом доступен через API Gitea:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
http://192.168.1.111:3000/api/v1/repos/embeddevio/awg-dash/actions/jobs/<job_id>/logs
|
||||||
|
```
|
||||||
|
|
||||||
|
Перезапуск конкретного run'а без нового коммита:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
http://192.168.1.111:3000/api/v1/repos/embeddevio/awg-dash/actions/runs/<run_id>/rerun
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Registry (push staging-<sha>)
|
||||||
|
|
||||||
|
Gitea-registry `192.168.1.111:3000` работает по **HTTP**, поэтому Docker-демон на
|
||||||
|
build-хосте должен знать его как insecure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// /etc/docker/daemon.json
|
||||||
|
{ "log-driver": "journald", "insecure-registries": ["192.168.1.111:3000"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Применяется **без перезапуска контейнеров**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl reload docker # SIGHUP, контейнеры продолжают работать
|
||||||
|
docker info | grep -A3 "Insecure Registries"
|
||||||
|
```
|
||||||
|
|
||||||
|
Авторизация в job'е — `${{ secrets.GITHUB_TOKEN }}` (выдаётся Gitea Actions автоматически), пользователь `${{ github.actor }}`.
|
||||||
|
|
||||||
|
## 6. Известные дефекты инфраструктуры
|
||||||
|
|
||||||
|
### 6.1 Gitea git-http отдаёт пустой pack → `actions/checkout` не работает
|
||||||
|
|
||||||
|
**Симптом:** `git clone`/`git fetch` с любого хоста падает:
|
||||||
|
|
||||||
|
```
|
||||||
|
fatal: early EOF
|
||||||
|
fatal: fetch-pack: invalid index-pack output
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина (измерено):** ответ `git-upload-pack` содержит `Content-Length: 90` — только
|
||||||
|
`shallow-info` + заголовок `packfile`, без самих объектов. То есть обрыв на стороне
|
||||||
|
Gitea (git-http), а не у клиента. `ls-remote` и эндпоинт `archive/<sha>.tar.gz` при этом
|
||||||
|
работают (архив 200 OK, валидный tar).
|
||||||
|
|
||||||
|
**Обход в `ci.yml`:** основной путь — `actions/checkout@v4` (`continue-on-error: true`);
|
||||||
|
если он падает, следующий шаг забирает исходники архивом:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL -o /tmp/src.tar.gz "http://192.168.1.111:3000/${IMAGE_NAME}/archive/${GITHUB_SHA}.tar.gz"
|
||||||
|
tar -xzf /tmp/src.tar.gz --strip-components=1 -C .
|
||||||
|
```
|
||||||
|
|
||||||
|
Когда git-http починят, fallback перестанет срабатывать сам (условие `outcome == 'failure'`).
|
||||||
|
Исправление на стороне Gitea-хоста (`192.168.1.111`) — вне доступа агентов (нет SSH).
|
||||||
|
|
||||||
|
### 6.2 «all predefined address pools have been fully subnetted»
|
||||||
|
|
||||||
|
Каждый job создаёт Docker-сеть `GITEA-ACTIONS-*`; прерванные job'ы оставляли сети,
|
||||||
|
и пул `172.17.0.0/16`-диапазонов исчерпался → job'ы падали на создании контейнера.
|
||||||
|
|
||||||
|
Лечение (безопасно, только сети без контейнеров):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for n in $(docker network ls --filter name=GITEA-ACTIONS -q); do
|
||||||
|
[ "$(docker network inspect "$n" --format '{{len .Containers}}')" = "0" ] && docker network rm "$n"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Профилактика: `container.network: "host"` в конфиге runner'а (job-сети не создаются).
|
||||||
|
Кардинальное решение — `default-address-pools` в `daemon.json`, но оно требует
|
||||||
|
**перезапуска** демона (в отличие от `insecure-registries`), поэтому только по согласованию.
|
||||||
|
|
||||||
|
### 6.3 Instance-scoped runner не отчитывается о статусах
|
||||||
|
|
||||||
|
Runner, зарегистрированный instance-токеном (`gitea-runner-awg`), забирал задачи, но
|
||||||
|
Gitea не фиксировал результаты: jobs вечно в `queued`, `runner_id: 0`, логов нет.
|
||||||
|
Repo-scoped runner (`gitea-runner-awg-repo`, id 2) с тем же образом пишет статусы и логи
|
||||||
|
корректно. Legacy-контейнер остановлен.
|
||||||
|
|
||||||
|
### 6.4 Артефакты Actions не публикуются
|
||||||
|
|
||||||
|
`actions/upload-artifact@v3` в логе рапортует `Artifact has been successfully uploaded!`,
|
||||||
|
но `GET /actions/runs/<id>/artifacts` → `{"artifacts":[],"total_count":0}` и в UI пусто;
|
||||||
|
`@v4` зависает на старте экшена (>6 мин). Платформенный дефект Gitea 1.26.0 + act_runner v0.6.1 — issue #62.
|
||||||
|
|
||||||
|
**Мера в пайплайне:** `unit` печатает coverage-таблицу (`coverage report --sort=cover`), `security` — вывод
|
||||||
|
`bandit`/`pip-audit`, `image-scan` — trivy-таблицу. Эти строки в логе job'а и есть доступный
|
||||||
|
артефакт: ссылка на job лежит в commit status. Скачать `coverage.xml`/`htmlcov`/`trivy-report.json`
|
||||||
|
из UI нельзя, пока #62 не закрыт.
|
||||||
|
|
||||||
|
### 6.5 `contract-openapi.yml` — тот же дефект git-http
|
||||||
|
|
||||||
|
Оба job'а (`openapi-contract`, `ts-client-is-current`) падали на шаге `checkout` за 28–50 с
|
||||||
|
(`bad pack header`, exit 128) и делали combined status любого PR = `failure`, хотя `ci / *` зелёные.
|
||||||
|
В ветке `fix/59-checkout-fallback` (PR #63) применён тот же archive-fallback; `ts-client-is-current`
|
||||||
|
после fallback сравнивает `sha256sum` перегенерированного `ui/src/api/generated/schema.d.ts`
|
||||||
|
вместо `git diff` (в распакованном архиве нет `.git`). Проверено: run #64 — success.
|
||||||
|
|
||||||
|
## 7. Локальное воспроизведение гейтов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3.12 -m venv .venv && . .venv/bin/activate
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
|
||||||
|
ruff check backend && ruff format --check backend && mypy backend # lint
|
||||||
|
pytest # unit + coverage >= 70%
|
||||||
|
bandit -lll -r backend && pip-audit -r requirements.txt # security
|
||||||
|
docker build -f deploy/Dockerfile -t awg-dash:local . # build
|
||||||
|
trivy image --severity HIGH,CRITICAL --exit-code 1 awg-dash:local # image-scan
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка, что гейты действительно роняют пайплайн:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# покрытие: временно сузить --cov до одного модуля или добавить непокрытый код
|
||||||
|
pytest --cov=awg_agent --cov-fail-under=70 # FAIL: покрытие ниже порога
|
||||||
|
# security: HIGH-находка (eval / shell=True / weak hash)
|
||||||
|
bandit -lll -r backend # exit 1 при Severity: High
|
||||||
|
```
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Отчёт: Gitea Actions runner + CI-пайплайн `embeddevio/awg-dash` (v2)
|
||||||
|
|
||||||
|
**Владелец:** devops · **Дата:** 2026-10-28 · **Refs #17, Refs #15**
|
||||||
|
**Ветка:** `feat/17-ci` · **Ветка проверки:** `feat/17-verify` (= #17 + `deploy/*` из #18)
|
||||||
|
|
||||||
|
## 1. Итог
|
||||||
|
|
||||||
|
Runner, поднятый 28.10, **фактически не работал**: задачи забирались, но падали на создании
|
||||||
|
контейнера, а результаты не фиксировались в Gitea. Причины найдены и устранены на build-хосте
|
||||||
|
`192.168.1.147`. Пайплайн `ci.yml` написан, закоммичен и **прогнан на живом runner'е** —
|
||||||
|
все три гейта подтверждены реальными падениями jobs.
|
||||||
|
|
||||||
|
Осталось три внешних пункта, которые агент закрыть не может (нет SSH к `192.168.1.111`,
|
||||||
|
нет прав на merge): git-http дефект Gitea (#59), артефакты Gitea не публикуются,
|
||||||
|
`deploy/Dockerfile` появится в `main` только после merge #18.
|
||||||
|
|
||||||
|
## 2. Что было сломано в runner'е и как исправлено
|
||||||
|
|
||||||
|
| # | Симптом | Причина | Исправление |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | jobs вечно `queued`, `runner_id: 0`, логов нет | runner зарегистрирован **instance-token'ом** (`gitea-runner-awg`): задачи забирались, но результат не привязывался к репозиторию | зарегистрирован **repo-scoped** runner `awg-dash-147-repo` (repo registration token через API), `online`, id 2; legacy-контейнер остановлен |
|
||||||
|
| 2 | `failed to create container: Duplicate mount point: /var/run/docker.sock` | `container.options` монтировал сокет, а act_runner v0.6.1 монтирует его сам | `options: ""` + `valid_volumes: ["/var/run/docker.sock"]` |
|
||||||
|
| 3 | `all predefined address pools have been fully subnetted` | 23 «висимых» сети `GITEA-ACTIONS-*` от прерванных job'ов вычерпали пул Docker | удалены все `GITEA-ACTIONS-*` с 0 контейнеров (31 → 8 сетей); `container.network: "host"` как профилактика |
|
||||||
|
| 4 | `git clone` в шаге checkout: `bad pack header` | дефект Gitea git-http (issue #59), не runner | в `ci.yml` — fallback на `archive/<sha>.tar.gz` (archive работает: 200, валидный tar) |
|
||||||
|
| 5 | trivy: `install.sh` + `v0.57.1` → 404, tarball с github releases качается минутами | неверная версия в скрипте + медленная отдача github releases | trivy запускается официальным образом `aquasec/trivy:0.76.1` через docker-сокет, БД кэшируется в `/opt/trivy-cache` |
|
||||||
|
|
||||||
|
## 3. Итоговая конфигурация runner'а
|
||||||
|
|
||||||
|
```
|
||||||
|
контейнер : gitea-runner-awg-repo (docker.io/gitea/act_runner:latest v0.6.1), 192.168.1.147
|
||||||
|
область : repo-scoped (embeddevio/awg-dash), runner id 2, online
|
||||||
|
label : ubuntu-latest -> catthehacker/ubuntu:act-22.04
|
||||||
|
capacity : 1 (maxParallel=1)
|
||||||
|
сеть job : host, socket /var/run/docker.sock проброшен
|
||||||
|
toolcache : volume act-toolcache -> /opt/hostedtoolcache (Python 3.12.16 установлен один раз)
|
||||||
|
trivy : образ aquasec/trivy:0.76.1 + БД-кэш /opt/trivy-cache
|
||||||
|
registry : 192.168.1.111:3000 добавлен в insecure-registries на .147
|
||||||
|
(применено `systemctl reload docker`, контейнеры не перезапускались)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Пайплайн `.gitea/workflows/ci.yml`
|
||||||
|
|
||||||
|
```
|
||||||
|
lint ─┬─ unit ───────┐
|
||||||
|
│ ├─ build ── image-scan ── push (только push-событие)
|
||||||
|
└─ security ───┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Job | Команды | Гейт |
|
||||||
|
|---|---|---|
|
||||||
|
| lint | `ruff check backend`, `ruff format --check backend`, `mypy backend` | любая ошибка |
|
||||||
|
| unit | `pytest --cov=awg_agent --cov=awg_api --cov-report=xml/html --cov-fail-under=70` | **coverage < 70 %** |
|
||||||
|
| security | `bandit -lll -r backend`, `pip-audit -r requirements.txt -r requirements-contract.txt` | **bandit HIGH**, известные CVE в зависимостях |
|
||||||
|
| build | `docker build --pull -f deploy/Dockerfile -t 192.168.1.111:3000/embeddevio/awg-dash:staging-<sha8>` | ошибка сборки |
|
||||||
|
| image-scan | `trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1` | **HIGH/CRITICAL в образе** |
|
||||||
|
| push | `docker login` + `docker push ...:staging-<sha8>` | ошибка авторизации/пуша |
|
||||||
|
|
||||||
|
`build`/`image-scan`/`push` активируются только при наличии `deploy/Dockerfile`
|
||||||
|
(output `has_dockerfile` из `lint`); до merge #18 они скипаются с предупреждением.
|
||||||
|
|
||||||
|
## 5. Прогоны на живом runner'е (доказательства по AC)
|
||||||
|
|
||||||
|
### 5.1 `feat/17-ci` (ветка от `main`, Dockerfile ещё нет) — run #39
|
||||||
|
|
||||||
|
http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/39
|
||||||
|
|
||||||
|
| Job | Результат | Детали |
|
||||||
|
|---|---|---|
|
||||||
|
| lint | ✅ success | ruff check/format + mypy strict |
|
||||||
|
| unit | ✅ success | **198 passed**, coverage **96.80 %** (порог ≥ 70 %) |
|
||||||
|
| security | ✅ success | bandit: 0 HIGH, pip-audit: CVE не найдены |
|
||||||
|
| build / image-scan / push | ⏭ skipped | нет `deploy/Dockerfile` (ждёт #18) — штатное поведение |
|
||||||
|
|
||||||
|
### 5.2 `feat/17-verify` (= #17 + `deploy/*` из #18) — run #49
|
||||||
|
|
||||||
|
http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/49
|
||||||
|
|
||||||
|
| Job | Результат | Детали |
|
||||||
|
|---|---|---|
|
||||||
|
| lint | ✅ success | |
|
||||||
|
| unit | ✅ success | 198 passed, coverage 96.80 % |
|
||||||
|
| security | ✅ success | |
|
||||||
|
| build | ✅ success | образ `192.168.1.111:3000/embeddevio/awg-dash:staging-<sha8>` собран на runner'е |
|
||||||
|
| image-scan | ❌ failure | `Total: 22 (HIGH: 21, CRITICAL: 1)` — golang stdlib v1.25.14, `golang.org/x/crypto` — см. п. 6.1 |
|
||||||
|
| push | ⏭ skipped | fail-closed: при красном image-scan образ не публикуется |
|
||||||
|
|
||||||
|
### 5.3 Негативные прогоны гейтов — `ci/17-gate-negative` (ветка-пробник, удалена)
|
||||||
|
|
||||||
|
| Проверка | Run | Что в логе |
|
||||||
|
|---|---|---|
|
||||||
|
| покрытие < порога | #51 http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/51 | `FAIL Required test coverage of 99% not reached. Total coverage: 96.80%` → job `unit` **failure** |
|
||||||
|
| HIGH-находка bandit | #55 http://192.168.1.111:3000/embeddevio/awg-dash/actions/runs/55 | `Issue: [B602 ...] Severity: High Confidence: High` → job `security` **failure**, pipeline красный |
|
||||||
|
| HIGH/CRITICAL в образе | #49 (см. 5.2) | 22 находки → `image-scan` **failure** |
|
||||||
|
|
||||||
|
**Вывод по AC:** пуш в `feat/*` даёт jobs `lint`/`unit`/`security`/`build`; снижение покрытия
|
||||||
|
ниже порога роняет job; HIGH-уязвимость (код или образ) роняет pipeline.
|
||||||
|
|
||||||
|
## 6. Открытые вопросы (эскалация)
|
||||||
|
|
||||||
|
### 6.1 Образ содержит 21 HIGH + 1 CRITICAL (нужен апгрейд сборки, #18)
|
||||||
|
|
||||||
|
Trivy по образу, собранному из `deploy/Dockerfile` (#18):
|
||||||
|
|
||||||
|
- `stdlib` Go **v1.25.14** → CVE-2026-56857 (CRITICAL), CVE-2026-111943, CVE-2026-78667, CVE-2026-97031 (HIGH);
|
||||||
|
- `golang.org/x/crypto v0.42.0` → CVE-2025-47913 (HIGH, fix в 0.43.0).
|
||||||
|
|
||||||
|
Причина: `ARG GO_IMAGE=golang:1.25-alpine` в `deploy/Dockerfile`. Гейт работает правильно —
|
||||||
|
публикация образа заблокирована. Требуется бамп Go до ≥ 1.26.10 и `golang.org/x/crypto` ≥ 0.43.0
|
||||||
|
(владелец #18), после этого `image-scan` и `push` пойдут зелёными без правок CI.
|
||||||
|
|
||||||
|
### 6.2 Артефакты Gitea не публикуются
|
||||||
|
|
||||||
|
`actions/upload-artifact@v3` отрабатывает без ошибок («Artifact has been successfully uploaded!»,
|
||||||
|
173 КБ на coverage), но ни API (`/actions/runs/<id>/artifacts`, `/actions/artifacts`), ни UI
|
||||||
|
артефактов не показывают (`total_count: 0`). `upload-artifact@v4` **зависает** (job висел >6 мин
|
||||||
|
на старте экшена, контейнер пришлось снять). Итог: содержимое артефактов в Gitea 1.26 + runner
|
||||||
|
v0.6.1 не сохраняется — поэтому в шагах дополнительно печатается сводка в лог job
|
||||||
|
(coverage-таблица, bandit-отчёт, trivy-таблица) — эти логи стабильны и доступны по ссылке run'а.
|
||||||
|
|
||||||
|
### 6.3 Git-http Gitea (issue #59)
|
||||||
|
|
||||||
|
Checkout штатным `actions/checkout` падает (`bad pack header`), работает только архивный
|
||||||
|
fallback. Нужен доступ к хосту Gitea `192.168.1.111` (fsck/repack, проверка диска) — у агентов
|
||||||
|
его нет.
|
||||||
|
|
||||||
|
### 6.4 Token для registry push
|
||||||
|
|
||||||
|
PAT агента не имеет `write:package` → `401 Unauthorized` на `POST /v2/.../blobs/uploads/`
|
||||||
|
(проверено вручную на .147). В `ci.yml` логин берёт `secrets.REGISTRY_TOKEN`, если он задан,
|
||||||
|
иначе `secrets.GITHUB_TOKEN`. Для гарантированного push нужен секрет `REGISTRY_TOKEN`
|
||||||
|
(owner, PAT с `write:package`).
|
||||||
|
|
||||||
|
## 7. Эксплуатация
|
||||||
|
|
||||||
|
- Перезапуск run'а без коммита: `POST /api/v1/repos/embeddevio/awg-dash/actions/runs/<id>/rerun`.
|
||||||
|
- Ручной запуск: `workflow_dispatch` у `ci.yml`.
|
||||||
|
- Логи job'а: `GET /api/v1/repos/embeddevio/awg-dash/actions/jobs/<job_id>/logs`.
|
||||||
|
- Полный runbook: `docs/ci/CI.md` в ветке `feat/17-ci`.
|
||||||
@@ -8,5 +8,13 @@ httpx>=0.27
|
|||||||
structlog>=24.4
|
structlog>=24.4
|
||||||
prometheus-client>=0.21
|
prometheus-client>=0.21
|
||||||
|
|
||||||
|
# Транзитивные зависимости с открытыми HIGH — пиним снизу под гейт image-scan (issue #61):
|
||||||
|
# urllib3 2.7.0 → CVE-2026-97687 (MITM через HTTPS-прокси) / CVE-2026-97689 (DoS), fix 2.8.0
|
||||||
|
# msgpack 1.1.2 → CVE-2026-114613 / GHSA-6v7p-g79w-8964, fix 1.2.3
|
||||||
|
# setuptools 70.3.0 → CVE-2025-47273 (path traversal), fix 78.1.1
|
||||||
|
urllib3>=2.8.0
|
||||||
|
msgpack>=1.2.3
|
||||||
|
setuptools>=78.1.1
|
||||||
|
|
||||||
# Сетевой слой вызывается через бинари (awg, iptables, ipset, dnsmasq),
|
# Сетевой слой вызывается через бинари (awg, iptables, ipset, dnsmasq),
|
||||||
# поэтому python-зависимостей для сети не требуется.
|
# поэтому python-зависимостей для сети не требуется.
|
||||||
|
|||||||
Reference in New Issue
Block a user