Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
869b13cb57 | ||
|
|
e190785d4f | ||
|
|
3fd05ff833 | ||
|
|
e7f24f0be4 | ||
|
|
1bfe460e4b | ||
|
|
e55d2c5aba | ||
|
|
f63e9b5fd0 | ||
|
|
0148d4ca37 |
+24
-2
@@ -14,7 +14,11 @@ Job **changes** вычисляет флаги по путям в diff. Полн
|
|||||||
|
|
||||||
На **pull request** — **commitlint**. При изменении `deploy/docker/**` — job **docker-check** (`bake --print`, bake без `--push` если есть доступ к registry).
|
На **pull request** — **commitlint**. При изменении `deploy/docker/**` — job **docker-check** (`bake --print`, bake без `--push` если есть доступ к registry).
|
||||||
|
|
||||||
Кэш зависимостей: `actions/cache` с ключом `sha256sum` lockfile (`go.sum` / `pnpm-lock.yaml`). `hashFiles` в Gitea не используем.
|
Кэш зависимостей — нативный `actions/cache` (cache server act_runner), ключ `sha256sum` lockfile (не `hashFiles`). Пути **абсолютные** (`$HOME/.pnpm-store`, `go env GOMODCACHE` / `GOCACHE`): тильда `~` на Gitea часто не раскрывается и даёт вечный miss.
|
||||||
|
|
||||||
|
Кэшируется целиком: pnpm store + `node_modules` + corepack; Go modules + GOCACHE + `golangci-lint` в `GOBIN`. При hit: `pnpm install --offline`, `go mod download` без сети. `setup-go cache:` и `golangci-lint-action` не используем — они завязаны на `hashFiles`.
|
||||||
|
|
||||||
|
Если restore пишет `connect ECONNREFUSED` / `cache server not configured` — на runner включите cache server (см. ниже). Иначе каждый job снова качает пакеты (~минуты).
|
||||||
|
|
||||||
Runner: `ubuntu-latest`, **bird2** из apt, Docker для **docker-check** (PR) и **publish** (CD).
|
Runner: `ubuntu-latest`, **bird2** из apt, Docker для **docker-check** (PR) и **publish** (CD).
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ Runner: `ubuntu-latest`, **bird2** из apt, Docker для **docker-check** (PR)
|
|||||||
|
|
||||||
1. `pnpm exec semantic-release` — тег `vX.Y.Z` на **текущий commit** (без дополнительного commit в main).
|
1. `pnpm exec semantic-release` — тег `vX.Y.Z` на **текущий commit** (без дополнительного commit в main).
|
||||||
2. Gitea Release + `CHANGELOG.md` как attachment (не в git).
|
2. Gitea Release + `CHANGELOG.md` как attachment (не в git).
|
||||||
3. Зеркало base-образов в `evobgp-buildcache:base-*` (`deploy/docker/mirror-base-images.sh`).
|
3. Зеркало base-образов в `evobgp-buildcache:base-*` (`deploy/docker/mirror-base-images.sh`; skip существующих тегов, `linux/amd64`, retry при 429).
|
||||||
4. `docker buildx bake default --push` с `VERSION=X.Y.Z`, `pull=false`, named builder `evobgp` (`cleanup: false`).
|
4. `docker buildx bake default --push` с `VERSION=X.Y.Z`, `pull=false`, named builder `evobgp` (`cleanup: false`).
|
||||||
|
|
||||||
Если releasable-коммитов нет — semantic-release no-op, образы не публикуются.
|
Если releasable-коммитов нет — semantic-release no-op, образы не публикуются.
|
||||||
@@ -58,3 +62,21 @@ docker pull git.shx.one/myuser/evobgp-api:1.2.3
|
|||||||
```
|
```
|
||||||
|
|
||||||
См. [deploy/docker/README.md](../deploy/docker/README.md), [docs/quickstart.md](../docs/quickstart.md).
|
См. [deploy/docker/README.md](../deploy/docker/README.md), [docs/quickstart.md](../docs/quickstart.md).
|
||||||
|
|
||||||
|
## act_runner: cache server
|
||||||
|
|
||||||
|
`actions/cache` ходит в **встроенный cache server** runner (не GitHub `type=gha`). Кэш локален для этого runner.
|
||||||
|
|
||||||
|
В `config.yaml` runner:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
dir: "" # по умолчанию $HOME/.cache/actcache
|
||||||
|
host: "" # IP, доступный из job-контейнера (не 0.0.0.0)
|
||||||
|
port: 8088
|
||||||
|
```
|
||||||
|
|
||||||
|
Если runner в Docker, а jobs — отдельные контейнеры: пробросьте порт и задайте `host` (LAN IP хоста) или `external_server: "http://<host>:8088/"`. Иначе restore — timeout/ECONNREFUSED и пакеты качаются снова.
|
||||||
|
|
||||||
|
Не делайте `docker system prune -a` по cron: сотрётся и Docker-кэш FROM, и пользы от `cleanup: false` у buildx не будет.
|
||||||
|
|||||||
+23
-16
@@ -39,18 +39,26 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Enable pnpm via corepack
|
- name: Export cache paths
|
||||||
run: corepack enable
|
run: sh scripts/ci/export-cache-env.sh
|
||||||
- id: pnpm-hash
|
- id: pnpm-hash
|
||||||
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
||||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
- id: pnpm-cache
|
||||||
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ~/.local/share/pnpm/store
|
path: |
|
||||||
|
${{ env.PNPM_STORE_DIR }}
|
||||||
|
${{ env.COREPACK_HOME }}
|
||||||
|
node_modules
|
||||||
|
apps/web/node_modules
|
||||||
|
packages/ui/node_modules
|
||||||
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-${{ runner.os }}-
|
pnpm-${{ runner.os }}-
|
||||||
- name: Install release tooling
|
- name: Install release tooling
|
||||||
run: pnpm install --frozen-lockfile
|
env:
|
||||||
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
||||||
|
run: sh scripts/ci/pnpm-ci.sh
|
||||||
- name: Verify releasable commit messages
|
- name: Verify releasable commit messages
|
||||||
run: pnpm exec node scripts/commit/verify-release-commits.mjs
|
run: pnpm exec node scripts/commit/verify-release-commits.mjs
|
||||||
- name: Semantic release
|
- name: Semantic release
|
||||||
@@ -108,10 +116,9 @@ jobs:
|
|||||||
password: ${{ secrets.ACTIONS_PAT }}
|
password: ${{ secrets.ACTIONS_PAT }}
|
||||||
- name: Mirror base images into buildcache
|
- name: Mirror base images into buildcache
|
||||||
if: steps.rel.outputs.released == 'true'
|
if: steps.rel.outputs.released == 'true'
|
||||||
id: mirror
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.shx.one/${{ steps.meta.outputs.owner_lc }}
|
REGISTRY: git.shx.one/${{ steps.meta.outputs.owner_lc }}
|
||||||
|
MIRROR_ENV_FILE: ${{ runner.temp }}/mirror-base.env
|
||||||
run: sh deploy/docker/mirror-base-images.sh
|
run: sh deploy/docker/mirror-base-images.sh
|
||||||
- name: Build and push images (bake)
|
- name: Build and push images (bake)
|
||||||
if: steps.rel.outputs.released == 'true'
|
if: steps.rel.outputs.released == 'true'
|
||||||
@@ -126,16 +133,16 @@ jobs:
|
|||||||
CACHE_REF_WEB: git.shx.one/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:web-buildcache
|
CACHE_REF_WEB: git.shx.one/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:web-buildcache
|
||||||
CACHE_REF_BIRDC: git.shx.one/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:birdc-buildcache
|
CACHE_REF_BIRDC: git.shx.one/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache:birdc-buildcache
|
||||||
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
||||||
|
BUILDX_BAKE_FILE_RELATIVE_PATHS: "1"
|
||||||
|
MIRROR_ENV_FILE: ${{ runner.temp }}/mirror-base.env
|
||||||
|
working-directory: deploy/docker
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
cache="git.shx.one/${{ steps.meta.outputs.owner_lc }}/evobgp-buildcache"
|
if [ -f "${MIRROR_ENV_FILE}" ]; then
|
||||||
if [ "${{ steps.mirror.outcome }}" = "success" ]; then
|
set -a
|
||||||
export BASE_GOLANG="${cache}:base-golang-1.24-alpine"
|
# shellcheck disable=SC1090
|
||||||
export BASE_DEBIAN="${cache}:base-debian-bookworm-slim"
|
. "${MIRROR_ENV_FILE}"
|
||||||
export BASE_NODE="${cache}:base-node-22-alpine"
|
set +a
|
||||||
export BASE_NGINX="${cache}:base-nginx-1.27-alpine"
|
|
||||||
export BASE_UBUNTU="${cache}:base-ubuntu-noble"
|
|
||||||
export BASE_DISTROLESS="${cache}:base-distroless-static-debian12-nonroot"
|
|
||||||
fi
|
fi
|
||||||
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
|
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
|
||||||
-f deploy/docker/docker-bake.hcl default --push
|
-f docker-bake.hcl default --push
|
||||||
|
|||||||
@@ -183,20 +183,28 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Enable pnpm via corepack
|
- name: Export cache paths
|
||||||
run: corepack enable
|
run: sh scripts/ci/export-cache-env.sh
|
||||||
- id: pnpm-hash
|
- id: pnpm-hash
|
||||||
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
||||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
- id: pnpm-cache
|
||||||
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ~/.local/share/pnpm/store
|
path: |
|
||||||
|
${{ env.PNPM_STORE_DIR }}
|
||||||
|
${{ env.COREPACK_HOME }}
|
||||||
|
node_modules
|
||||||
|
apps/web/node_modules
|
||||||
|
packages/ui/node_modules
|
||||||
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-${{ runner.os }}-
|
pnpm-${{ runner.os }}-
|
||||||
- name: pnpm install, Redocly, codegen check
|
- name: pnpm install, Redocly, codegen check
|
||||||
|
env:
|
||||||
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
pnpm install --frozen-lockfile
|
sh scripts/ci/pnpm-ci.sh
|
||||||
pnpm exec redocly lint docs/openapi.yaml
|
pnpm exec redocly lint docs/openapi.yaml
|
||||||
chmod +x scripts/check-openapi-gen.sh
|
chmod +x scripts/check-openapi-gen.sh
|
||||||
sh scripts/check-openapi-gen.sh
|
sh scripts/check-openapi-gen.sh
|
||||||
@@ -210,20 +218,28 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Enable pnpm via corepack
|
- name: Export cache paths
|
||||||
run: corepack enable
|
run: sh scripts/ci/export-cache-env.sh
|
||||||
- id: pnpm-hash
|
- id: pnpm-hash
|
||||||
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
||||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
- id: pnpm-cache
|
||||||
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ~/.local/share/pnpm/store
|
path: |
|
||||||
|
${{ env.PNPM_STORE_DIR }}
|
||||||
|
${{ env.COREPACK_HOME }}
|
||||||
|
node_modules
|
||||||
|
apps/web/node_modules
|
||||||
|
packages/ui/node_modules
|
||||||
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-${{ runner.os }}-
|
pnpm-${{ runner.os }}-
|
||||||
- name: pnpm install, typecheck, lint, test, build
|
- name: pnpm install, typecheck, lint, test, build
|
||||||
|
env:
|
||||||
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
pnpm install --frozen-lockfile
|
sh scripts/ci/pnpm-ci.sh
|
||||||
pnpm --filter @evobgp/web run typecheck
|
pnpm --filter @evobgp/web run typecheck
|
||||||
pnpm --filter @evobgp/web run lint
|
pnpm --filter @evobgp/web run lint
|
||||||
pnpm --filter @evobgp/web run test
|
pnpm --filter @evobgp/web run test
|
||||||
@@ -239,17 +255,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.24"
|
||||||
cache: false
|
cache: false
|
||||||
|
- name: Export cache paths
|
||||||
|
run: sh scripts/ci/export-cache-env.sh
|
||||||
- id: go-hash
|
- id: go-hash
|
||||||
run: echo "key=$(sha256sum go.sum | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
run: echo "key=$(sha256sum go.sum | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
||||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
${{ env.GOMODCACHE }}
|
||||||
~/.cache/go-build
|
${{ env.GOCACHE }}
|
||||||
key: go-${{ runner.os }}-${{ steps.go-hash.outputs.key }}
|
${{ env.GOBIN }}
|
||||||
|
${{ env.GOLANGCI_LINT_CACHE }}
|
||||||
|
key: go-${{ runner.os }}-1.24-gl1.64.8-${{ steps.go-hash.outputs.key }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
go-${{ runner.os }}-
|
go-${{ runner.os }}-1.24-gl1.64.8-
|
||||||
|
go-${{ runner.os }}-1.24-
|
||||||
|
- name: Download modules
|
||||||
|
env:
|
||||||
|
GOMODCACHE: ${{ env.GOMODCACHE }}
|
||||||
|
GOCACHE: ${{ env.GOCACHE }}
|
||||||
|
run: go mod download
|
||||||
- name: Vet
|
- name: Vet
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=readonly
|
||||||
run: go vet ./...
|
run: go vet ./...
|
||||||
- name: Lint httpapi (ERR-01 / ARCH-01)
|
- name: Lint httpapi (ERR-01 / ARCH-01)
|
||||||
run: sh scripts/lint-httpapi.sh
|
run: sh scripts/lint-httpapi.sh
|
||||||
@@ -258,13 +286,20 @@ jobs:
|
|||||||
- name: Validate remote speaker compose
|
- name: Validate remote speaker compose
|
||||||
run: sh scripts/validate-remote-speaker-compose.sh
|
run: sh scripts/validate-remote-speaker-compose.sh
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2
|
env:
|
||||||
with:
|
GOLANGCI_LINT_VERSION: v1.64.8
|
||||||
version: v1.64.8
|
run: sh scripts/ci/golangci-lint.sh
|
||||||
skip-cache: true
|
|
||||||
- name: Test
|
- name: Test
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=readonly
|
||||||
|
GOMODCACHE: ${{ env.GOMODCACHE }}
|
||||||
|
GOCACHE: ${{ env.GOCACHE }}
|
||||||
run: go test ./... -race -count=1
|
run: go test ./... -race -count=1
|
||||||
- name: Build all commands
|
- name: Build all commands
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=readonly
|
||||||
|
GOMODCACHE: ${{ env.GOMODCACHE }}
|
||||||
|
GOCACHE: ${{ env.GOCACHE }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
out="${RUNNER_TEMP}/evobgp-bin"
|
out="${RUNNER_TEMP}/evobgp-bin"
|
||||||
@@ -321,13 +356,19 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Enable pnpm via corepack
|
- name: Export cache paths
|
||||||
run: corepack enable
|
run: sh scripts/ci/export-cache-env.sh
|
||||||
- id: pnpm-hash
|
- id: pnpm-hash
|
||||||
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
||||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
- id: pnpm-cache
|
||||||
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ~/.local/share/pnpm/store
|
path: |
|
||||||
|
${{ env.PNPM_STORE_DIR }}
|
||||||
|
${{ env.COREPACK_HOME }}
|
||||||
|
node_modules
|
||||||
|
apps/web/node_modules
|
||||||
|
packages/ui/node_modules
|
||||||
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
pnpm-${{ runner.os }}-
|
pnpm-${{ runner.os }}-
|
||||||
@@ -335,9 +376,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BASE_SHA: ${{ inputs.base_sha }}
|
BASE_SHA: ${{ inputs.base_sha }}
|
||||||
HEAD_SHA: ${{ inputs.head_sha }}
|
HEAD_SHA: ${{ inputs.head_sha }}
|
||||||
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
pnpm install --frozen-lockfile
|
sh scripts/ci/pnpm-ci.sh
|
||||||
pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA"
|
pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA"
|
||||||
|
|
||||||
docker-check:
|
docker-check:
|
||||||
@@ -366,12 +408,14 @@ jobs:
|
|||||||
working-directory: deploy/docker
|
working-directory: deploy/docker
|
||||||
env:
|
env:
|
||||||
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
||||||
|
BUILDX_BAKE_FILE_RELATIVE_PATHS: "1"
|
||||||
run: docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl --print default
|
run: docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl --print default
|
||||||
- name: bake (no push)
|
- name: bake (no push)
|
||||||
if: inputs.allow_registry_login
|
if: inputs.allow_registry_login
|
||||||
working-directory: deploy/docker
|
working-directory: deploy/docker
|
||||||
env:
|
env:
|
||||||
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
||||||
|
BUILDX_BAKE_FILE_RELATIVE_PATHS: "1"
|
||||||
CACHE_REF_GO: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:go-buildcache
|
CACHE_REF_GO: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:go-buildcache
|
||||||
CACHE_REF_WEB: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:web-buildcache
|
CACHE_REF_WEB: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:web-buildcache
|
||||||
CACHE_REF_BIRDC: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:birdc-buildcache
|
CACHE_REF_BIRDC: git.shx.one/${{ github.repository_owner }}/evobgp-buildcache:birdc-buildcache
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function DashboardModulesGrid({
|
|||||||
description="Поиск, сортировка и быстрый переход к настройке"
|
description="Поиск, сортировка и быстрый переход к настройке"
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
actions={
|
actions={
|
||||||
<Button variant="outline" size="sm" render={<Link to="/modules/new" />}>
|
<Button variant="outline" size="sm" render={<Link to="/modules" search={{ create: true }} />}>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Создать
|
Создать
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ const ACTIONS: QuickActionItem[] = [
|
|||||||
id: 'new-module',
|
id: 'new-module',
|
||||||
title: 'Создать модуль',
|
title: 'Создать модуль',
|
||||||
description: 'Новый модуль маршрутизации и источники префиксов.',
|
description: 'Новый модуль маршрутизации и источники префиксов.',
|
||||||
to: '/modules/new',
|
to: '/modules',
|
||||||
|
search: { create: true },
|
||||||
icon: <Plus aria-hidden />,
|
icon: <Plus aria-hidden />,
|
||||||
iconClassName: 'text-primary',
|
iconClassName: 'text-primary',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export function LookupAddStep({
|
|||||||
<AlertTitle>Нет подходящего модуля</AlertTitle>
|
<AlertTitle>Нет подходящего модуля</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Создайте модуль типа {wantedType}, затем повторите добавление.{' '}
|
Создайте модуль типа {wantedType}, затем повторите добавление.{' '}
|
||||||
<Button variant="link" size="sm" className="h-auto p-0" render={<Link to="/modules/new" />}>
|
<Button variant="link" size="sm" className="h-auto p-0" render={<Link to="/modules" search={{ create: true }} />}>
|
||||||
Перейти к модулям
|
Перейти к модулям
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
|
import { Checkbox } from '@evobgp/ui/components/checkbox'
|
||||||
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { FormDrawer } from '@/components/form-drawer'
|
||||||
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
|
import { SelectField } from '@/components/select-field'
|
||||||
|
import { dohProfileShortLabel } from '@/lib/modules/helpers'
|
||||||
|
import { dohPolicyRu, moduleTypeRu } from '@/lib/ui-labels'
|
||||||
|
import { useCreateModuleMutation } from '@/queries/modules'
|
||||||
|
import type {
|
||||||
|
BgpCommunity,
|
||||||
|
DohProfile,
|
||||||
|
DohResolverPolicy,
|
||||||
|
ModuleCreate,
|
||||||
|
ModuleRow,
|
||||||
|
ModuleType,
|
||||||
|
} from '@/types/api'
|
||||||
|
|
||||||
|
interface ModuleCreateDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
communities: BgpCommunity[]
|
||||||
|
dohProfiles: DohProfile[]
|
||||||
|
onCreated?: (mod: ModuleRow) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see https://reui.io/preview/base/form-7 */
|
||||||
|
/** @see https://reui.io/preview/base/sheet-8 */
|
||||||
|
|
||||||
|
const MODULE_TYPE_ITEMS: { value: ModuleType; label: string }[] = [
|
||||||
|
{ value: 'IP_RANGES', label: moduleTypeRu('IP_RANGES') },
|
||||||
|
{ value: 'AS_PREFIXES', label: moduleTypeRu('AS_PREFIXES') },
|
||||||
|
{ value: 'CDN_CIDRS', label: moduleTypeRu('CDN_CIDRS') },
|
||||||
|
{ value: 'DOMAINS', label: moduleTypeRu('DOMAINS') },
|
||||||
|
]
|
||||||
|
|
||||||
|
const DOH_POLICY_ITEMS: { value: DohResolverPolicy; label: string }[] = [
|
||||||
|
{ value: 'primary_only', label: dohPolicyRu('primary_only') },
|
||||||
|
{ value: 'failover', label: dohPolicyRu('failover') },
|
||||||
|
{ value: 'union', label: dohPolicyRu('union') },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function ModuleCreateDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
communities,
|
||||||
|
dohProfiles,
|
||||||
|
onCreated,
|
||||||
|
}: ModuleCreateDialogProps) {
|
||||||
|
const createMutation = useCreateModuleMutation()
|
||||||
|
|
||||||
|
const [type, setType] = useState<ModuleType>('IP_RANGES')
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [enabled, setEnabled] = useState(true)
|
||||||
|
const [priority, setPriority] = useState('0')
|
||||||
|
const [refreshIntervalSec, setRefreshIntervalSec] = useState('')
|
||||||
|
const [cronExpr, setCronExpr] = useState('')
|
||||||
|
const [defaultCommunityId, setDefaultCommunityId] = useState<string | null>(null)
|
||||||
|
const [dohResolverPolicy, setDohResolverPolicy] = useState<DohResolverPolicy>('primary_only')
|
||||||
|
const [dohProfileIds, setDohProfileIds] = useState<string[]>([])
|
||||||
|
|
||||||
|
const isDomains = type === 'DOMAINS'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
setType('IP_RANGES')
|
||||||
|
setName('')
|
||||||
|
setEnabled(true)
|
||||||
|
setPriority('0')
|
||||||
|
setRefreshIntervalSec('')
|
||||||
|
setCronExpr('')
|
||||||
|
setDefaultCommunityId(null)
|
||||||
|
setDohResolverPolicy('primary_only')
|
||||||
|
setDohProfileIds([])
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
function toggleDohProfile(id: string, checked: boolean) {
|
||||||
|
setDohProfileIds((prev) => {
|
||||||
|
if (checked) {
|
||||||
|
if (prev.includes(id)) return prev
|
||||||
|
return [...prev, id]
|
||||||
|
}
|
||||||
|
return prev.filter((x) => x !== id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const trimmedName = name.trim()
|
||||||
|
if (!trimmedName) {
|
||||||
|
toast.error('Укажите название модуля')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityNum = Number(priority)
|
||||||
|
if (!Number.isFinite(priorityNum) || !Number.isInteger(priorityNum)) {
|
||||||
|
toast.error('Приоритет должен быть целым числом')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let refresh: number | undefined
|
||||||
|
if (refreshIntervalSec.trim() !== '') {
|
||||||
|
const n = Number(refreshIntervalSec)
|
||||||
|
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
||||||
|
toast.error('Интервал обновления должен быть целым числом ≥ 0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
refresh = n
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: ModuleCreate = {
|
||||||
|
type,
|
||||||
|
name: trimmedName,
|
||||||
|
enabled,
|
||||||
|
priority: priorityNum,
|
||||||
|
}
|
||||||
|
if (refresh !== undefined) {
|
||||||
|
body.refresh_interval_sec = refresh
|
||||||
|
}
|
||||||
|
const cron = cronExpr.trim()
|
||||||
|
if (cron) {
|
||||||
|
body.cron_expr = cron
|
||||||
|
}
|
||||||
|
if (defaultCommunityId) {
|
||||||
|
body.default_community_id = defaultCommunityId
|
||||||
|
}
|
||||||
|
if (isDomains) {
|
||||||
|
body.doh_resolver_policy = dohResolverPolicy
|
||||||
|
body.doh_profile_ids = dohProfileIds
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await createMutation.mutateAsync(body)
|
||||||
|
onOpenChange(false)
|
||||||
|
onCreated?.(created)
|
||||||
|
} catch {
|
||||||
|
// toast in mutation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormDrawer
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Новый модуль"
|
||||||
|
description="Тип задаётся один раз. Записи добавляются на карточке модуля."
|
||||||
|
className="sm:max-w-md"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<LoadingButton type="button" loading={createMutation.isPending} onClick={() => void save()}>
|
||||||
|
Создать
|
||||||
|
</LoadingButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectField
|
||||||
|
id="mod-create-type"
|
||||||
|
label="Тип"
|
||||||
|
items={MODULE_TYPE_ITEMS}
|
||||||
|
value={type}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v) setType(v)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="mod-create-name">Название</Label>
|
||||||
|
<Input
|
||||||
|
id="mod-create-name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Имя модуля"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between gap-4 rounded-lg border border-border bg-muted/30 p-3">
|
||||||
|
<div className="grid min-w-0 flex-1 gap-1 pr-2">
|
||||||
|
<Label htmlFor="mod-create-enabled">Включён</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Выключенный модуль не участвует в обновлении и применении.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
id="mod-create-enabled"
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={(v) => setEnabled(v === true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="mod-create-priority">Приоритет</Label>
|
||||||
|
<Input
|
||||||
|
id="mod-create-priority"
|
||||||
|
type="number"
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) => setPriority(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="mod-create-interval">Интервал обновления (сек)</Label>
|
||||||
|
<Input
|
||||||
|
id="mod-create-interval"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
placeholder="пусто = по умолчанию"
|
||||||
|
value={refreshIntervalSec}
|
||||||
|
onChange={(e) => setRefreshIntervalSec(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="mod-create-cron">Cron (опционально)</Label>
|
||||||
|
<Input
|
||||||
|
id="mod-create-cron"
|
||||||
|
placeholder="0 * * * *"
|
||||||
|
value={cronExpr}
|
||||||
|
onChange={(e) => setCronExpr(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CommunitySelect
|
||||||
|
id="mod-create-community"
|
||||||
|
label="Community по умолчанию"
|
||||||
|
value={defaultCommunityId}
|
||||||
|
onValueChange={setDefaultCommunityId}
|
||||||
|
communities={communities}
|
||||||
|
nullable
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isDomains ? (
|
||||||
|
<>
|
||||||
|
<SelectField
|
||||||
|
id="mod-create-doh-policy"
|
||||||
|
label="Политика DoH"
|
||||||
|
items={DOH_POLICY_ITEMS}
|
||||||
|
value={dohResolverPolicy}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v) setDohResolverPolicy(v)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>DoH профили</Label>
|
||||||
|
{dohProfiles.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">Нет профилей в справочнике</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2 rounded-lg border border-border p-3">
|
||||||
|
{dohProfiles.map((p) => {
|
||||||
|
const checked = dohProfileIds.includes(p.id)
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={p.id}
|
||||||
|
htmlFor={`mod-create-doh-${p.id}`}
|
||||||
|
className="flex cursor-pointer items-start gap-3"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`mod-create-doh-${p.id}`}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(v) => toggleDohProfile(p.id, v === true)}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<span className="flex min-w-0 flex-col gap-0.5">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{dohProfileShortLabel(p.id, dohProfiles)}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground truncate text-xs" title={p.url}>
|
||||||
|
{p.url}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</FormDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,21 +1,28 @@
|
|||||||
import { Link } from '@tanstack/react-router'
|
|
||||||
import { Boxes, Plus } from 'lucide-react'
|
import { Boxes, Plus } from 'lucide-react'
|
||||||
|
|
||||||
import { IllustratedEmptyState } from '@/components/patterns/illustrated-empty-state'
|
import { IllustratedEmptyState } from '@/components/patterns/illustrated-empty-state'
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
|
|
||||||
/** empty-state-3 pattern for first module. */
|
/** empty-state-3 pattern for first module. */
|
||||||
export function ProjectsEmptyState() {
|
export function ProjectsEmptyState({
|
||||||
|
canCreate = true,
|
||||||
|
onCreate,
|
||||||
|
}: {
|
||||||
|
canCreate?: boolean
|
||||||
|
onCreate?: () => void
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<IllustratedEmptyState
|
<IllustratedEmptyState
|
||||||
icon={Boxes}
|
icon={Boxes}
|
||||||
title="Создайте первый модуль"
|
title="Создайте первый модуль"
|
||||||
description="Модули задают источники префиксов: AS, CDN, домены и IP-диапазоны."
|
description="Модули задают источники префиксов: AS, CDN, домены и IP-диапазоны."
|
||||||
action={
|
action={
|
||||||
<Button size="sm" render={<Link to="/modules/new" />}>
|
canCreate && onCreate ? (
|
||||||
<Plus />
|
<Button size="sm" onClick={onCreate}>
|
||||||
Новый модуль
|
<Plus />
|
||||||
</Button>
|
Новый модуль
|
||||||
|
</Button>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { toast } from 'sonner'
|
|||||||
import { apiJSON, apiMutate } from '@/lib/api-client'
|
import { apiJSON, apiMutate } from '@/lib/api-client'
|
||||||
import { overviewKeys } from '@/queries/overview'
|
import { overviewKeys } from '@/queries/overview'
|
||||||
import type {
|
import type {
|
||||||
|
ModuleCreate,
|
||||||
ModulePatch,
|
ModulePatch,
|
||||||
ModuleRow,
|
ModuleRow,
|
||||||
ModulesResponse,
|
ModulesResponse,
|
||||||
@@ -64,6 +65,18 @@ export function moduleEntriesQueryOptions(id: string, type: ModuleRow['type']) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateModuleMutation() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: ModuleCreate) => apiMutate<ModuleRow>('/v1/modules', 'POST', body),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.success('Модуль создан')
|
||||||
|
invalidateModules(qc, data.id)
|
||||||
|
},
|
||||||
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось создать модуль'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useUpdateModuleMutation() {
|
export function useUpdateModuleMutation() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link, createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Plus, RefreshCw } from 'lucide-react'
|
import { Plus, RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
@@ -6,18 +6,45 @@ import { Button } from '@evobgp/ui/components/button'
|
|||||||
|
|
||||||
import { FrameDataGrid } from '@/components/reui-kit'
|
import { FrameDataGrid } from '@/components/reui-kit'
|
||||||
import { ProjectsEmptyState } from '@/components/patterns/projects-empty-state'
|
import { ProjectsEmptyState } from '@/components/patterns/projects-empty-state'
|
||||||
|
import { ModuleCreateDialog } from '@/components/modules/module-create-dialog'
|
||||||
import { ModulesListGrid } from '@/components/modules/modules-list-grid'
|
import { ModulesListGrid } from '@/components/modules/modules-list-grid'
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
import { QueryState } from '@/components/query-state'
|
import { QueryState } from '@/components/query-state'
|
||||||
import { TableSkeleton } from '@/components/skeletons'
|
import { TableSkeleton } from '@/components/skeletons'
|
||||||
|
import { sessionCanWriteModules } from '@/lib/auth'
|
||||||
|
import { authSessionQueryOptions } from '@/queries/auth'
|
||||||
|
import {
|
||||||
|
directoriesCommunitiesQueryOptions,
|
||||||
|
directoriesDohQueryOptions,
|
||||||
|
} from '@/queries/directories'
|
||||||
import { modulesListQueryOptions } from '@/queries/modules'
|
import { modulesListQueryOptions } from '@/queries/modules'
|
||||||
|
|
||||||
|
function parseCreateFlag(value: unknown): boolean {
|
||||||
|
return value === true || value === '1' || value === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/modules/')({
|
export const Route = createFileRoute('/_auth/modules/')({
|
||||||
component: ModulesListComponent,
|
component: ModulesListComponent,
|
||||||
|
validateSearch: (search: Record<string, unknown>): { create?: boolean } => {
|
||||||
|
if (parseCreateFlag(search.create)) return { create: true }
|
||||||
|
return {}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function ModulesListComponent() {
|
function ModulesListComponent() {
|
||||||
|
const { create } = Route.useSearch()
|
||||||
|
const navigate = Route.useNavigate()
|
||||||
const query = useQuery(modulesListQueryOptions())
|
const query = useQuery(modulesListQueryOptions())
|
||||||
|
const sessionQ = useQuery(authSessionQueryOptions())
|
||||||
|
const canWrite = sessionCanWriteModules(sessionQ.data)
|
||||||
|
const communitiesQ = useQuery(directoriesCommunitiesQueryOptions())
|
||||||
|
const dohQ = useQuery(directoriesDohQueryOptions())
|
||||||
|
|
||||||
|
const createOpen = canWrite && create === true
|
||||||
|
|
||||||
|
function setCreateOpen(open: boolean) {
|
||||||
|
void navigate({ search: open ? { create: true } : {}, replace: true })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@@ -42,10 +69,12 @@ function ModulesListComponent() {
|
|||||||
<FrameDataGrid
|
<FrameDataGrid
|
||||||
title="Все модули"
|
title="Все модули"
|
||||||
actions={
|
actions={
|
||||||
<Button size="sm" render={<Link to="/modules/new" />}>
|
canWrite ? (
|
||||||
<Plus />
|
<Button size="sm" onClick={() => setCreateOpen(true)}>
|
||||||
Создать
|
<Plus />
|
||||||
</Button>
|
Создать
|
||||||
|
</Button>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<QueryState
|
<QueryState
|
||||||
@@ -54,7 +83,12 @@ function ModulesListComponent() {
|
|||||||
isError={query.isError}
|
isError={query.isError}
|
||||||
error={query.error}
|
error={query.error}
|
||||||
empty={query.data?.items?.length === 0}
|
empty={query.data?.items?.length === 0}
|
||||||
emptyContent={<ProjectsEmptyState />}
|
emptyContent={
|
||||||
|
<ProjectsEmptyState
|
||||||
|
canCreate={canWrite}
|
||||||
|
onCreate={() => setCreateOpen(true)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
skeleton={<TableSkeleton rows={6} cols={5} />}
|
skeleton={<TableSkeleton rows={6} cols={5} />}
|
||||||
onRetry={() => query.refetch()}
|
onRetry={() => query.refetch()}
|
||||||
>
|
>
|
||||||
@@ -66,6 +100,16 @@ function ModulesListComponent() {
|
|||||||
)}
|
)}
|
||||||
</QueryState>
|
</QueryState>
|
||||||
</FrameDataGrid>
|
</FrameDataGrid>
|
||||||
|
|
||||||
|
<ModuleCreateDialog
|
||||||
|
open={createOpen}
|
||||||
|
onOpenChange={setCreateOpen}
|
||||||
|
communities={communitiesQ.data?.items ?? []}
|
||||||
|
dohProfiles={dohQ.data?.items ?? []}
|
||||||
|
onCreated={(mod) => {
|
||||||
|
void navigate({ to: '/modules/$moduleId', params: { moduleId: mod.id } })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,7 @@
|
|||||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||||
import { Button } from '@evobgp/ui/components/button'
|
|
||||||
import { PageHeader } from '@/components/page-header'
|
|
||||||
import {
|
|
||||||
Frame,
|
|
||||||
FrameDescription,
|
|
||||||
FrameHeader,
|
|
||||||
FramePanel,
|
|
||||||
FrameTitle,
|
|
||||||
} from '@/components/reui/frame'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/modules/new')({
|
export const Route = createFileRoute('/_auth/modules/new')({
|
||||||
component: NewModuleComponent,
|
beforeLoad: () => {
|
||||||
|
throw redirect({ to: '/modules', search: { create: true } })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function NewModuleComponent() {
|
|
||||||
return (
|
|
||||||
<div className="mx-auto flex max-w-3xl flex-col gap-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Новый модуль"
|
|
||||||
description="Создание модуля — через API или будущая форма"
|
|
||||||
/>
|
|
||||||
<Frame dense spacing="sm">
|
|
||||||
<FrameHeader>
|
|
||||||
<FrameTitle>Создание через API</FrameTitle>
|
|
||||||
<FrameDescription>
|
|
||||||
Форма в UI появится позже. Сейчас модуль можно создать запросом ниже.
|
|
||||||
</FrameDescription>
|
|
||||||
</FrameHeader>
|
|
||||||
<FramePanel className="flex flex-col gap-3 text-sm text-muted-foreground">
|
|
||||||
<pre className="overflow-x-auto rounded-md border bg-muted/40 p-3 font-mono text-xs">
|
|
||||||
{`POST /v1/modules
|
|
||||||
{ "type": "DOMAINS", "name": "Мой список" }`}
|
|
||||||
</pre>
|
|
||||||
<Button variant="outline" size="sm" className="self-start" render={<Link to="/modules" />}>
|
|
||||||
Назад к списку
|
|
||||||
</Button>
|
|
||||||
</FramePanel>
|
|
||||||
</Frame>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ BuildKit кэширует `/go/pkg/mod`, `~/.cache/go-build` и pnpm store че
|
|||||||
|
|
||||||
| Образ | Runtime base | Заметка |
|
| Образ | Runtime base | Заметка |
|
||||||
|-------|----------------|---------|
|
|-------|----------------|---------|
|
||||||
| scheduler, ingest, render, deploy, node | `gcr.io/distroless/static-debian12:nonroot` | static Go (`CGO_ENABLED=0`), без shell |
|
| scheduler, ingest, render | `gcr.io/distroless/static-debian12:nonroot` | static Go (`CGO_ENABLED=0`), без shell |
|
||||||
| api, all | `debian:bookworm-slim` + `birdc` | только клиент birdc, без демона `bird` |
|
| api, all, deploy, node | `debian:bookworm-slim` + `bird` + `birdc` | `bird -p` (parse-check) и `birdc`; демон не запускается |
|
||||||
| agent | тот же Ubuntu+bird2, что bird2 | общие слои с `evobgp-bird2` |
|
| agent | тот же Ubuntu+bird2, что bird2 | общие слои с `evobgp-bird2` |
|
||||||
| bird2 | Ubuntu Noble + пакет bird2 | |
|
| bird2 | Ubuntu Noble + пакет bird2 | |
|
||||||
| web, web-all | `nginx:1.27-alpine` | `worker_processes 1` |
|
| web, web-all | `nginx:1.27-alpine` | `worker_processes 1` |
|
||||||
@@ -27,7 +27,7 @@ BuildKit кэширует `/go/pkg/mod`, `~/.cache/go-build` и pnpm store че
|
|||||||
- `git.shx.one/<owner>/evobgp-buildcache:go-buildcache` — **запись** только из `go-build-all`
|
- `git.shx.one/<owner>/evobgp-buildcache:go-buildcache` — **запись** только из `go-build-all`
|
||||||
- `git.shx.one/<owner>/evobgp-buildcache:web-buildcache` — **запись** только из `web-build`
|
- `git.shx.one/<owner>/evobgp-buildcache:web-buildcache` — **запись** только из `web-build`
|
||||||
- `git.shx.one/<owner>/evobgp-buildcache:birdc-buildcache` — **запись** только из `go-birdc`
|
- `git.shx.one/<owner>/evobgp-buildcache:birdc-buildcache` — **запись** только из `go-birdc`
|
||||||
- `git.shx.one/<owner>/evobgp-buildcache:base-*` — зеркало FROM-образов (`mirror-base-images.sh`), без rebuild
|
- `git.shx.one/<owner>/evobgp-buildcache:base-*` — зеркало FROM с **Docker Hub** (`docker.io/library/…`; distroless — `gcr.io`). Только `linux/amd64`; skip если тег уже в Gitea. Неуспешный copy не валит CD — bake берёт Docker Hub FROM для этой базы.
|
||||||
|
|
||||||
`pull = false` в bake: не перекачивать FROM, если слой уже в builder. Не запускайте `docker system prune -a` на runner.
|
`pull = false` в bake: не перекачивать FROM, если слой уже в builder. Не запускайте `docker system prune -a` на runner.
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl web-images
|
|||||||
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl --print default
|
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl --print default
|
||||||
```
|
```
|
||||||
|
|
||||||
В `docker-bake.hcl`: `context = "../.."` (корень репо), `dockerfile = "deploy/docker/…"` (путь от корня репо).
|
В `docker-bake.hcl`: `context = "../.."` резолвится **от cwd** (каталог `deploy/docker/`). `dockerfile` — путь от этого context (корень репо). Из корня репо не вызывать bake с `-f deploy/docker/docker-bake.hcl`: получится `lstat ../../deploy`. Можно задать `BUILDX_BAKE_FILE_RELATIVE_PATHS=1`.
|
||||||
|
|
||||||
Один образ (legacy, target `build-all`):
|
Один образ (legacy, target `build-all`):
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
# BIRD из репозитория Ubuntu (Noble 24.04 LTS) — актуальнее пакета Debian bookworm.
|
# BIRD из репозитория Ubuntu (Noble 24.04 LTS) — актуальнее пакета Debian bookworm.
|
||||||
# Stage bird2-base общий с evobgp-agent (одинаковые слои на speaker-VPS).
|
# Stage bird2-base общий с evobgp-agent (одинаковые слои на speaker-VPS).
|
||||||
ARG BASE_UBUNTU=public.ecr.aws/docker/library/ubuntu:noble
|
ARG BASE_UBUNTU=docker.io/library/ubuntu:noble
|
||||||
|
|
||||||
FROM ${BASE_UBUNTU} AS bird2-base
|
FROM ${BASE_UBUNTU} AS bird2-base
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Единая сборка образов EvoBGP (buildx bake: -f deploy/docker/docker-bake.hcl).
|
# Единая сборка образов EvoBGP (buildx bake: -f deploy/docker/docker-bake.hcl).
|
||||||
# context = "../.." — корень репозитория (относительно этого файла).
|
# context = "../.." — корень репо. Bake резолвит context от cwd (не от HCL),
|
||||||
|
# поэтому вызов из deploy/docker/ либо BUILDX_BAKE_FILE_RELATIVE_PATHS=1.
|
||||||
# Переменные Bake читаются из окружения (см. docs.docker.com/build/bake/variables/).
|
# Переменные Bake читаются из окружения (см. docs.docker.com/build/bake/variables/).
|
||||||
|
|
||||||
variable "REGISTRY" {
|
variable "REGISTRY" {
|
||||||
@@ -39,11 +40,11 @@ variable "CACHE_REF_BIRDC" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "BASE_GOLANG" {
|
variable "BASE_GOLANG" {
|
||||||
default = "public.ecr.aws/docker/library/golang:1.24-alpine"
|
default = "docker.io/library/golang:1.24-alpine"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "BASE_DEBIAN" {
|
variable "BASE_DEBIAN" {
|
||||||
default = "public.ecr.aws/docker/library/debian:bookworm-slim"
|
default = "docker.io/library/debian:bookworm-slim"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "BASE_DISTROLESS" {
|
variable "BASE_DISTROLESS" {
|
||||||
@@ -51,15 +52,15 @@ variable "BASE_DISTROLESS" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "BASE_NODE" {
|
variable "BASE_NODE" {
|
||||||
default = "public.ecr.aws/docker/library/node:22-alpine"
|
default = "docker.io/library/node:22-alpine"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "BASE_NGINX" {
|
variable "BASE_NGINX" {
|
||||||
default = "public.ecr.aws/docker/library/nginx:1.27-alpine"
|
default = "docker.io/library/nginx:1.27-alpine"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "BASE_UBUNTU" {
|
variable "BASE_UBUNTU" {
|
||||||
default = "public.ecr.aws/docker/library/ubuntu:noble"
|
default = "docker.io/library/ubuntu:noble"
|
||||||
}
|
}
|
||||||
|
|
||||||
function "go-cache-from" {
|
function "go-cache-from" {
|
||||||
@@ -235,13 +236,13 @@ target "evobgp-render" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
target "evobgp-deploy" {
|
target "evobgp-deploy" {
|
||||||
inherits = ["_go-runtime"]
|
inherits = ["_go-runtime-birdc"]
|
||||||
args = { BIN = "evobgp-deploy" }
|
args = { BIN = "evobgp-deploy" }
|
||||||
tags = image-tags("evobgp-deploy")
|
tags = image-tags("evobgp-deploy")
|
||||||
}
|
}
|
||||||
|
|
||||||
target "evobgp-node" {
|
target "evobgp-node" {
|
||||||
inherits = ["_go-runtime"]
|
inherits = ["_go-runtime-birdc"]
|
||||||
args = { BIN = "evobgp-node" }
|
args = { BIN = "evobgp-node" }
|
||||||
tags = image-tags("evobgp-node")
|
tags = image-tags("evobgp-node")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
# React + Vite статическая панель EvoBGP + nginx.
|
# React + Vite статическая панель EvoBGP + nginx.
|
||||||
# Финальный stage `web` ожидает bake-контекст web-artifacts (= target:web-build).
|
# Финальный stage `web` ожидает bake-контекст web-artifacts (= target:web-build).
|
||||||
ARG BASE_NODE=public.ecr.aws/docker/library/node:22-alpine
|
ARG BASE_NODE=docker.io/library/node:22-alpine
|
||||||
ARG BASE_NGINX=public.ecr.aws/docker/library/nginx:1.27-alpine
|
ARG BASE_NGINX=docker.io/library/nginx:1.27-alpine
|
||||||
|
|
||||||
FROM ${BASE_NODE} AS deps
|
FROM ${BASE_NODE} AS deps
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
# Универсальная сборка бинарей cmd/* (ARG BIN) или всех сразу (target build-all).
|
# Универсальная сборка бинарей cmd/* (ARG BIN) или всех сразу (target build-all).
|
||||||
# Воркеры (runtime): distroless static. api/all (runtime-birdc): debian-slim + birdc.
|
# Воркеры (runtime): distroless static. api/all/deploy/node (runtime-birdc): debian-slim + bird + birdc.
|
||||||
# CI: docker buildx bake -f deploy/docker/docker-bake.hcl
|
# CI: docker buildx bake -f deploy/docker/docker-bake.hcl
|
||||||
ARG BASE_GOLANG=public.ecr.aws/docker/library/golang:1.24-alpine
|
ARG BASE_GOLANG=docker.io/library/golang:1.24-alpine
|
||||||
ARG BASE_DEBIAN=public.ecr.aws/docker/library/debian:bookworm-slim
|
ARG BASE_DEBIAN=docker.io/library/debian:bookworm-slim
|
||||||
ARG BASE_DISTROLESS=gcr.io/distroless/static-debian12:nonroot
|
ARG BASE_DISTROLESS=gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
FROM ${BASE_GOLANG} AS deps
|
FROM ${BASE_GOLANG} AS deps
|
||||||
@@ -57,14 +57,15 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|||||||
&& BIRD_VERSION="${BIRD_VERSION}" /tmp/bird-from-source.sh \
|
&& BIRD_VERSION="${BIRD_VERSION}" /tmp/bird-from-source.sh \
|
||||||
&& rm -f /tmp/bird-from-source.sh
|
&& rm -f /tmp/bird-from-source.sh
|
||||||
|
|
||||||
# scheduler / ingest / render / deploy / node — static Go, без shell.
|
# scheduler / ingest / render — static Go, без shell.
|
||||||
FROM ${BASE_DISTROLESS} AS runtime
|
FROM ${BASE_DISTROLESS} AS runtime
|
||||||
ARG BIN=evobgp-api
|
ARG BIN=evobgp-api
|
||||||
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp
|
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["/usr/local/bin/evobgp"]
|
ENTRYPOINT ["/usr/local/bin/evobgp"]
|
||||||
|
|
||||||
# api / all — birdc (readline + ncurses). Только клиент birdc, без демона bird.
|
# api / all / deploy / node — bird -p (parse-check) + birdc configure.
|
||||||
|
# Демон BIRD в этом контейнере не запускается; процесс bird — в образе evobgp-bird2.
|
||||||
FROM ${BASE_DEBIAN} AS runtime-birdc
|
FROM ${BASE_DEBIAN} AS runtime-birdc
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
@@ -74,6 +75,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
ARG BIN=evobgp-api
|
ARG BIN=evobgp-api
|
||||||
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp
|
COPY --from=build-all /out/${BIN} /usr/local/bin/evobgp
|
||||||
|
COPY --from=birdc /usr/local/sbin/bird /usr/local/sbin/bird
|
||||||
COPY --from=birdc /usr/local/sbin/birdc /usr/local/sbin/birdc
|
COPY --from=birdc /usr/local/sbin/birdc /usr/local/sbin/birdc
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["/usr/local/bin/evobgp"]
|
ENTRYPOINT ["/usr/local/bin/evobgp"]
|
||||||
|
|||||||
@@ -2,27 +2,81 @@
|
|||||||
# Copy-by-digest зеркало базовых образов в Gitea Container Registry (без rebuild).
|
# Copy-by-digest зеркало базовых образов в Gitea Container Registry (без rebuild).
|
||||||
# Требует: docker login в REGISTRY host, docker buildx.
|
# Требует: docker login в REGISTRY host, docker buildx.
|
||||||
# REGISTRY=git.shx.one/<owner> (без суффикса /evobgp-buildcache)
|
# REGISTRY=git.shx.one/<owner> (без суффикса /evobgp-buildcache)
|
||||||
|
#
|
||||||
|
# Источник library-образов — Docker Hub (docker.io), не public.ecr.aws (429 Too Many Requests).
|
||||||
|
# Если тег уже есть в Gitea — skip (без повторного pull).
|
||||||
|
# Копируем только linux/amd64 — bake platforms совпадает, multi-arch индекс не нужен.
|
||||||
|
# Шаг CD не должен падать: неуспешный copy оставляет bake на Docker Hub FROM для этой базы.
|
||||||
set -eu
|
set -eu
|
||||||
REGISTRY="${REGISTRY:?REGISTRY required (git.shx.one/<owner>)}"
|
REGISTRY="${REGISTRY:?REGISTRY required (git.shx.one/<owner>)}"
|
||||||
CACHE_REPO="${REGISTRY}/evobgp-buildcache"
|
CACHE_REPO="${REGISTRY}/evobgp-buildcache"
|
||||||
|
ENV_FILE="${MIRROR_ENV_FILE:-}"
|
||||||
|
PLATFORM="${MIRROR_PLATFORM:-linux/amd64}"
|
||||||
|
RETRIES="${MIRROR_RETRIES:-4}"
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
cleanup() { rm -f "$tmp"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
dest_exists() {
|
||||||
|
docker buildx imagetools inspect "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_retry() {
|
||||||
|
src="$1"
|
||||||
|
dest="$2"
|
||||||
|
n=0
|
||||||
|
while [ "$n" -lt "$RETRIES" ]; do
|
||||||
|
n=$((n + 1))
|
||||||
|
if docker buildx imagetools create --platform "$PLATFORM" --tag "$dest" "$src"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
delay=$((n * 25))
|
||||||
|
echo "mirror retry ${n}/${RETRIES}, sleep ${delay}s: ${dest}"
|
||||||
|
sleep "$delay"
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
mirror() {
|
mirror() {
|
||||||
src="$1"
|
src="$1"
|
||||||
tag="$2"
|
tag="$2"
|
||||||
echo "mirror ${src} -> ${CACHE_REPO}:${tag}"
|
var="$3"
|
||||||
docker buildx imagetools create --tag "${CACHE_REPO}:${tag}" "${src}"
|
dest="${CACHE_REPO}:${tag}"
|
||||||
|
if dest_exists "$dest"; then
|
||||||
|
echo "skip (already in registry): ${dest}"
|
||||||
|
printf '%s=%s\n' "$var" "$dest" >>"$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "mirror ${src} -> ${dest} (${PLATFORM})"
|
||||||
|
if copy_retry "$src" "$dest"; then
|
||||||
|
printf '%s=%s\n' "$var" "$dest" >>"$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "warn: ${dest} not mirrored — bake uses Docker Hub FROM for ${var}"
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
mirror "public.ecr.aws/docker/library/golang:1.24-alpine" "base-golang-1.24-alpine"
|
mirror "docker.io/library/golang:1.24-alpine" "base-golang-1.24-alpine" BASE_GOLANG
|
||||||
mirror "public.ecr.aws/docker/library/debian:bookworm-slim" "base-debian-bookworm-slim"
|
sleep 8
|
||||||
mirror "public.ecr.aws/docker/library/node:22-alpine" "base-node-22-alpine"
|
mirror "docker.io/library/debian:bookworm-slim" "base-debian-bookworm-slim" BASE_DEBIAN
|
||||||
mirror "public.ecr.aws/docker/library/nginx:1.27-alpine" "base-nginx-1.27-alpine"
|
sleep 8
|
||||||
mirror "public.ecr.aws/docker/library/ubuntu:noble" "base-ubuntu-noble"
|
mirror "docker.io/library/node:22-alpine" "base-node-22-alpine" BASE_NODE
|
||||||
mirror "gcr.io/distroless/static-debian12:nonroot" "base-distroless-static-debian12-nonroot"
|
sleep 8
|
||||||
|
mirror "docker.io/library/nginx:1.27-alpine" "base-nginx-1.27-alpine" BASE_NGINX
|
||||||
|
sleep 8
|
||||||
|
mirror "docker.io/library/ubuntu:noble" "base-ubuntu-noble" BASE_UBUNTU
|
||||||
|
sleep 8
|
||||||
|
mirror "gcr.io/distroless/static-debian12:nonroot" "base-distroless-static-debian12-nonroot" BASE_DISTROLESS
|
||||||
|
|
||||||
echo "BASE_GOLANG=${CACHE_REPO}:base-golang-1.24-alpine"
|
echo "----- mirrored BASE_* -----"
|
||||||
echo "BASE_DEBIAN=${CACHE_REPO}:base-debian-bookworm-slim"
|
cat "$tmp"
|
||||||
echo "BASE_NODE=${CACHE_REPO}:base-node-22-alpine"
|
if [ -n "$ENV_FILE" ]; then
|
||||||
echo "BASE_NGINX=${CACHE_REPO}:base-nginx-1.27-alpine"
|
env_dir="$(dirname "$ENV_FILE")"
|
||||||
echo "BASE_UBUNTU=${CACHE_REPO}:base-ubuntu-noble"
|
if [ -d "$env_dir" ]; then
|
||||||
echo "BASE_DISTROLESS=${CACHE_REPO}:base-distroless-static-debian12-nonroot"
|
cp "$tmp" "$ENV_FILE"
|
||||||
|
echo "wrote ${ENV_FILE}"
|
||||||
|
else
|
||||||
|
echo "warn: MIRROR_ENV_FILE dir missing (${env_dir}) — skip write"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ docker login git.shx.one
|
|||||||
|
|
||||||
| Образ | Назначение | Страница пакета (пример) | Pull |
|
| Образ | Назначение | Страница пакета (пример) | Pull |
|
||||||
|--------|------------|--------------------------|------|
|
|--------|------------|--------------------------|------|
|
||||||
| `evobgp-api` | HTTP API (с `birdc` в образе) | [packages/…/evobgp-api](https://git.shx.one/denozord/-/packages/container/evobgp-api/latest) | `docker pull git.shx.one/denozord/evobgp-api:latest` |
|
| `evobgp-api` | HTTP API (с `bird`/`birdc` в образе для parse-check) | [packages/…/evobgp-api](https://git.shx.one/denozord/-/packages/container/evobgp-api/latest) | `docker pull git.shx.one/denozord/evobgp-api:latest` |
|
||||||
| `evobgp-all` | Монолит microVPS: API + in-process воркеры scheduler/ingest/render/deploy | [packages/…/evobgp-all](https://git.shx.one/denozord/-/packages/container/evobgp-all/latest) | `docker pull git.shx.one/denozord/evobgp-all:latest` |
|
| `evobgp-all` | Монолит microVPS: API + in-process воркеры scheduler/ingest/render/deploy | [packages/…/evobgp-all](https://git.shx.one/denozord/-/packages/container/evobgp-all/latest) | `docker pull git.shx.one/denozord/evobgp-all:latest` |
|
||||||
| `evobgp-scheduler` | Планировщик (reference) | [packages/…/evobgp-scheduler](https://git.shx.one/denozord/-/packages/container/evobgp-scheduler/latest) | `docker pull git.shx.one/denozord/evobgp-scheduler:latest` |
|
| `evobgp-scheduler` | Планировщик (reference) | [packages/…/evobgp-scheduler](https://git.shx.one/denozord/-/packages/container/evobgp-scheduler/latest) | `docker pull git.shx.one/denozord/evobgp-scheduler:latest` |
|
||||||
| `evobgp-ingest` | Ingest CDN / ETag | [packages/…/evobgp-ingest](https://git.shx.one/denozord/-/packages/container/evobgp-ingest/latest) | `docker pull git.shx.one/denozord/evobgp-ingest:latest` |
|
| `evobgp-ingest` | Ingest CDN / ETag | [packages/…/evobgp-ingest](https://git.shx.one/denozord/-/packages/container/evobgp-ingest/latest) | `docker pull git.shx.one/denozord/evobgp-ingest:latest` |
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
func TestParallelModuleRefresh_CoalescesDeployApply(t *testing.T) {
|
func TestParallelModuleRefresh_CoalescesDeployApply(t *testing.T) {
|
||||||
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
|
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
|
||||||
t.Setenv("EVOBGP_BIRD_ACTIVE_DIR", "") // skip bird binary path in deploy_apply
|
t.Setenv("EVOBGP_BIRD_ACTIVE_DIR", "") // skip bird binary path in deploy_apply
|
||||||
|
t.Setenv("EVOBGP_JOB_MAX_CONCURRENT", "8")
|
||||||
|
|
||||||
m := store.NewMemory()
|
m := store.NewMemory()
|
||||||
m.SeedDemo()
|
m.SeedDemo()
|
||||||
@@ -35,8 +36,15 @@ func TestParallelModuleRefresh_CoalescesDeployApply(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hold workers until both jobs are enqueued so inflightRefresh=2 before either
|
||||||
|
// finishModuleRefreshSuccess. Otherwise a fast ingest can finalize+deploy before
|
||||||
|
// the second Enqueue — sequential refreshes correctly produce two deploy_apply jobs.
|
||||||
|
start := make(chan struct{})
|
||||||
wk := &Worker{Store: m}
|
wk := &Worker{Store: m}
|
||||||
reg := NewRegistry(wk.Process)
|
reg := NewRegistry(func(j *Job) {
|
||||||
|
<-start
|
||||||
|
wk.Process(j)
|
||||||
|
})
|
||||||
wk.Registry = reg
|
wk.Registry = reg
|
||||||
|
|
||||||
mid1 := modIP
|
mid1 := modIP
|
||||||
@@ -47,6 +55,7 @@ func TestParallelModuleRefresh_CoalescesDeployApply(t *testing.T) {
|
|||||||
if _, _, err := reg.Enqueue(tenant, KindModuleRefresh, nil, &mid2, map[string]any{"module_id": mod2.ID}); err != nil {
|
if _, _, err := reg.Enqueue(tenant, KindModuleRefresh, nil, &mid2, map[string]any{"module_id": mod2.ID}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
close(start)
|
||||||
|
|
||||||
waitSucceededJobsByKindCount(t, reg, tenant, KindModuleRefresh, 2)
|
waitSucceededJobsByKindCount(t, reg, tenant, KindModuleRefresh, 2)
|
||||||
|
|
||||||
|
|||||||
@@ -414,6 +414,9 @@ func (m *Memory) ListModules(tenantID string) []*Module {
|
|||||||
}
|
}
|
||||||
return out[i].Name < out[j].Name
|
return out[i].Name < out[j].Name
|
||||||
})
|
})
|
||||||
|
for i := range out {
|
||||||
|
out[i] = cloneModule(out[i])
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +449,7 @@ func (m *Memory) GetModule(tenantID, moduleID string) (*Module, error) {
|
|||||||
if mod.TenantID != tenantID {
|
if mod.TenantID != tenantID {
|
||||||
return nil, ErrTenantScope
|
return nil, ErrTenantScope
|
||||||
}
|
}
|
||||||
return mod, nil
|
return cloneModule(mod), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) GetRevision(tenantID, revisionID string) (*Revision, error) {
|
func (m *Memory) GetRevision(tenantID, revisionID string) (*Revision, error) {
|
||||||
@@ -684,3 +687,24 @@ func (m *Memory) ListRevisions(tenantID, moduleID string, cursor string, limit i
|
|||||||
}
|
}
|
||||||
return page, nextCursor, hasMore
|
return page, nextCursor, hasMore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneStringPtr(s *string) *string {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := *s
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneModule(m *Module) *Module {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cp := *m
|
||||||
|
cp.DefaultCommunityID = cloneStringPtr(m.DefaultCommunityID)
|
||||||
|
cp.DohProfileID = cloneStringPtr(m.DohProfileID)
|
||||||
|
cp.DohProfileIDs = append([]string(nil), m.DohProfileIDs...)
|
||||||
|
cp.LastRefreshedAt = cloneTime(m.LastRefreshedAt)
|
||||||
|
cp.DeletedAt = cloneTime(m.DeletedAt)
|
||||||
|
return &cp
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m *Memory) CreateModule(tenantID string, in *Module) (*Module, error) {
|
|||||||
}
|
}
|
||||||
NormalizeModuleDoh(mod)
|
NormalizeModuleDoh(mod)
|
||||||
m.modules[id] = mod
|
m.modules[id] = mod
|
||||||
return mod, nil
|
return cloneModule(mod), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*Module, error) {
|
func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*Module, error) {
|
||||||
@@ -74,12 +74,14 @@ func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*M
|
|||||||
mod.DefaultCommunityID = &v
|
mod.DefaultCommunityID = &v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ApplyModuleDohPatch(mod, patch)
|
if patch.DohProfileIDs != nil || patch.DohProfileID != nil || patch.DohResolverPolicy != nil {
|
||||||
|
ApplyModuleDohPatch(mod, patch)
|
||||||
|
}
|
||||||
if patch.LastRefreshedAt != nil {
|
if patch.LastRefreshedAt != nil {
|
||||||
t := patch.LastRefreshedAt.UTC()
|
t := patch.LastRefreshedAt.UTC()
|
||||||
mod.LastRefreshedAt = &t
|
mod.LastRefreshedAt = &t
|
||||||
}
|
}
|
||||||
return mod, nil
|
return cloneModule(mod), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) SoftDeleteModule(tenantID, moduleID string) error {
|
func (m *Memory) SoftDeleteModule(tenantID, moduleID string) error {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Абсолютные пути для actions/cache: на act_runner «~» часто не раскрывается → cache miss.
|
||||||
|
set -eu
|
||||||
|
: "${GITHUB_ENV:?GITHUB_ENV required (Gitea/GitHub Actions)}"
|
||||||
|
|
||||||
|
HOME_DIR="${HOME:-}"
|
||||||
|
if [ -z "$HOME_DIR" ]; then
|
||||||
|
HOME_DIR="$(getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6 || true)"
|
||||||
|
fi
|
||||||
|
HOME_DIR="${HOME_DIR:-/root}"
|
||||||
|
|
||||||
|
if command -v go >/dev/null 2>&1; then
|
||||||
|
GOMODCACHE="$(go env GOMODCACHE)"
|
||||||
|
GOCACHE="$(go env GOCACHE)"
|
||||||
|
GOPATH="$(go env GOPATH)"
|
||||||
|
else
|
||||||
|
GOMODCACHE="${HOME_DIR}/go/pkg/mod"
|
||||||
|
GOCACHE="${HOME_DIR}/.cache/go-build"
|
||||||
|
GOPATH="${HOME_DIR}/go"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PNPM_STORE_DIR="${HOME_DIR}/.pnpm-store"
|
||||||
|
COREPACK_HOME="${HOME_DIR}/.cache/node/corepack"
|
||||||
|
GOBIN="${GOPATH}/bin"
|
||||||
|
GOLANGCI_LINT_CACHE="${HOME_DIR}/.cache/golangci-lint"
|
||||||
|
|
||||||
|
mkdir -p "$PNPM_STORE_DIR" "$COREPACK_HOME" "$GOMODCACHE" "$GOCACHE" "$GOBIN" "$GOLANGCI_LINT_CACHE"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "HOME_DIR=${HOME_DIR}"
|
||||||
|
echo "PNPM_STORE_DIR=${PNPM_STORE_DIR}"
|
||||||
|
echo "COREPACK_HOME=${COREPACK_HOME}"
|
||||||
|
echo "GOMODCACHE=${GOMODCACHE}"
|
||||||
|
echo "GOCACHE=${GOCACHE}"
|
||||||
|
echo "GOPATH=${GOPATH}"
|
||||||
|
echo "GOBIN=${GOBIN}"
|
||||||
|
echo "GOLANGCI_LINT_CACHE=${GOLANGCI_LINT_CACHE}"
|
||||||
|
} >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
echo "cache-env HOME_DIR=${HOME_DIR}"
|
||||||
|
echo "cache-env PNPM_STORE_DIR=${PNPM_STORE_DIR}"
|
||||||
|
echo "cache-env GOMODCACHE=${GOMODCACHE}"
|
||||||
|
echo "cache-env GOCACHE=${GOCACHE}"
|
||||||
|
echo "cache-env GOBIN=${GOBIN}"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Бинарь golangci-lint в GOBIN (кэш actions/cache), без goinstall и без hashFiles.
|
||||||
|
set -eu
|
||||||
|
VER="${GOLANGCI_LINT_VERSION:-v1.64.8}"
|
||||||
|
: "${GOBIN:?GOBIN required — run scripts/ci/export-cache-env.sh after setup-go}"
|
||||||
|
|
||||||
|
mkdir -p "$GOBIN"
|
||||||
|
export PATH="${GOBIN}:${PATH}"
|
||||||
|
if [ -n "${GOLANGCI_LINT_CACHE:-}" ]; then
|
||||||
|
mkdir -p "$GOLANGCI_LINT_CACHE"
|
||||||
|
export GOLANGCI_LINT_CACHE
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "${GOBIN}/golangci-lint" ]; then
|
||||||
|
echo "install golangci-lint ${VER} -> ${GOBIN}"
|
||||||
|
curl -sSfL "https://raw.githubusercontent.com/golangci/golangci-lint/${VER}/install.sh" \
|
||||||
|
| sh -s -- -b "$GOBIN" "$VER"
|
||||||
|
fi
|
||||||
|
golangci-lint version
|
||||||
|
golangci-lint run
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# pnpm install из кэша Gitea (store + node_modules). Без повторного download при hit.
|
||||||
|
set -eu
|
||||||
|
: "${PNPM_STORE_DIR:?PNPM_STORE_DIR required — run scripts/ci/export-cache-env.sh first}"
|
||||||
|
|
||||||
|
export COREPACK_HOME="${COREPACK_HOME:-${HOME:-/root}/.cache/node/corepack}"
|
||||||
|
mkdir -p "$PNPM_STORE_DIR" "$COREPACK_HOME"
|
||||||
|
corepack enable
|
||||||
|
pnpm config set store-dir "$PNPM_STORE_DIR"
|
||||||
|
|
||||||
|
echo "pnpm store: $(pnpm store path)"
|
||||||
|
echo "PNPM_CACHE_HIT=${PNPM_CACHE_HIT:-}"
|
||||||
|
|
||||||
|
if [ "${PNPM_CACHE_HIT:-}" = "true" ]; then
|
||||||
|
if pnpm install --frozen-lockfile --offline; then
|
||||||
|
echo "pnpm install --offline (cache hit)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "offline install failed — prefer-offline"
|
||||||
|
fi
|
||||||
|
pnpm install --frozen-lockfile --prefer-offline
|
||||||
Reference in New Issue
Block a user