CI/CD: ci, deploy-ift и deploy-stage через EventHubDevOps; фиксы calendar view, archive, API-тесты, traefik-coraza
Deploy IFT (core) / deploy-ift-core (push) Failing after 22m58s
CI / test (push) Failing after 22m59s

This commit is contained in:
2026-07-08 14:50:59 +03:00
parent 4cb6056917
commit f4746df4a2
15 changed files with 247 additions and 53 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# ВРЕМЕННО ОТКЛЮЧЕНО — CI/CD требует донастройки (см. EventHubBack#23). # Устарело — заменено на ci.yml и deploy-ift.yml / deploy-stage.yml (EventHubDevOps).
# Ручной запуск: Actions → Run workflow # Ручной запуск: Actions → Run workflow
name: Test & Deploy to Snapdeploy name: Test & Deploy to Snapdeploy (legacy)
on: on:
workflow_dispatch: workflow_dispatch:
# on: # on:
+34
View File
@@ -0,0 +1,34 @@
name: CI
on:
push:
branches: [master, main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build eventhub image
uses: docker/build-push-action@v4
with:
context: .
file: docker/Dockerfile
load: true
tags: eventhub:latest
- name: Build API test image
uses: docker/build-push-action@v4
with:
context: .
file: docker/ApiTests.Dockerfile
load: true
tags: eventhub-api-tests:latest
- name: Run API tests (local CT)
run: docker run --rm eventhub-api-tests:latest
+65
View File
@@ -0,0 +1,65 @@
name: Deploy IFT (core)
on:
push:
branches: [master, main]
workflow_dispatch:
jobs:
deploy-ift-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to registry
uses: docker/login-action@v2
with:
registry: git.sabilin.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push eventhub
uses: docker/build-push-action@v4
with:
context: .
file: docker/Dockerfile
push: true
tags: git.sabilin.com/eventhub/eventhub:ift
- name: Build and push traefik-coraza
uses: docker/build-push-action@v4
with:
context: docker/traefik
file: docker/traefik/Dockerfile
push: true
tags: git.sabilin.com/eventhub/traefik-coraza:ift
- name: Build and push fallback
uses: docker/build-push-action@v4
with:
context: docker/fallback
file: docker/fallback/Dockerfile
push: true
tags: git.sabilin.com/eventhub/fallback:ift
- name: Build and push bot-emulator
uses: docker/build-push-action@v4
with:
context: .
file: test/emulate_users/Dockerfile
push: true
tags: git.sabilin.com/eventhub/bot-emulator-users:ift
- name: Deploy core on IFT via SSH
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.IFT_SSH_HOST }}
username: ${{ secrets.IFT_SSH_USER }}
key: ${{ secrets.IFT_SSH_PRIVATE_KEY }}
script: |
export REGISTRY_USER='${{ secrets.REGISTRY_USER }}'
export REGISTRY_PASSWORD='${{ secrets.REGISTRY_PASSWORD }}'
/opt/eventhub-ift/devops/scripts/deploy-service.sh ift core
+13 -15
View File
@@ -1,15 +1,13 @@
# ВРЕМЕННО ОТКЛЮЧЕНО — CI/CD требует донастройки. name: Deploy stage (core)
# Ручной запуск: Actions → Run workflow
name: Deploy to Stage
on: on:
push:
tags:
- 'v*'
workflow_dispatch: workflow_dispatch:
# on:
# push:
# tags:
# - 'v*'
jobs: jobs:
deploy-stage: deploy-stage-core:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -17,7 +15,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to Gitea Container Registry - name: Login to registry
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: git.sabilin.com registry: git.sabilin.com
@@ -34,18 +32,18 @@ jobs:
- name: Build and push bot-emulator - name: Build and push bot-emulator
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: ./docker/bot-emulator-users context: .
file: test/emulate_users/Dockerfile
push: true push: true
tags: git.sabilin.com/eventhub/bot-emulator-users:stage tags: git.sabilin.com/eventhub/bot-emulator-users:stage
- name: Deploy to stage server - name: Deploy core on stage via SSH
uses: appleboy/ssh-action@v0.1.5 uses: appleboy/ssh-action@v0.1.5
with: with:
host: ${{ secrets.STAGE_SSH_HOST }} host: ${{ secrets.STAGE_SSH_HOST }}
username: ${{ secrets.STAGE_SSH_USER }} username: ${{ secrets.STAGE_SSH_USER }}
key: ${{ secrets.STAGE_SSH_PRIVATE_KEY }} key: ${{ secrets.STAGE_SSH_PRIVATE_KEY }}
script: | script: |
cd /opt/eventhub-stage export REGISTRY_USER='${{ secrets.REGISTRY_USER }}'
docker pull git.sabilin.com/eventhub/eventhub:stage export REGISTRY_PASSWORD='${{ secrets.REGISTRY_PASSWORD }}'
docker pull git.sabilin.com/eventhub/bot-emulator-users:stage /opt/eventhub-stage/devops/scripts/deploy-service.sh stage core
docker stack deploy -c docker-compose.stage.yml eventhub-stage --with-registry-auth
+3
View File
@@ -1,6 +1,9 @@
# Основной образ EventHub (единственный для нод) # Основной образ EventHub (единственный для нод)
docker build -t eventhub:latest -f docker/Dockerfile . docker build -t eventhub:latest -f docker/Dockerfile .
# Traefik + Coraza WAF (wasm встроен в образ, без plugins.traefik.io)
docker build -t traefik-coraza:latest -f docker/traefik/Dockerfile docker/traefik
# Сервис заглушка # Сервис заглушка
docker build -t fallback:latest -f docker/fallback/Dockerfile docker/fallback docker build -t fallback:latest -f docker/fallback/Dockerfile docker/fallback
+3 -6
View File
@@ -4,7 +4,7 @@ version: "3.8"
services: services:
# ================== Балансировщик ================== # ================== Балансировщик ==================
traefik: traefik:
image: traefik:latest image: traefik-coraza:latest
command: command:
# - "--log.level=DEBUG" # - "--log.level=DEBUG"
- "--api.insecure=true" - "--api.insecure=true"
@@ -21,8 +21,7 @@ services:
- "--accesslog=true" - "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log" - "--accesslog.filepath=/var/log/traefik/access.log"
- "--accesslog.format=json" - "--accesslog.format=json"
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik" - "--experimental.localPlugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
- "--experimental.plugins.coraza.version=v0.3.0"
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
@@ -32,7 +31,6 @@ services:
- "./traefik/certs:/etc/traefik/certs:ro" - "./traefik/certs:/etc/traefik/certs:ro"
- "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro" - "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
- "traefik-logs:/var/log/traefik" - "traefik-logs:/var/log/traefik"
- "traefik-plugins:/plugins-storage"
networks: networks:
- eventhub-net - eventhub-net
deploy: deploy:
@@ -226,5 +224,4 @@ volumes:
prometheus-data: prometheus-data:
grafana-data: grafana-data:
traefik-logs: traefik-logs:
loglynx-data: loglynx-data:
traefik-plugins:
+3 -4
View File
@@ -2,7 +2,7 @@
services: services:
# ================== Балансировщик (HTTPS/WSS, WAF, логи) ================== # ================== Балансировщик (HTTPS/WSS, WAF, логи) ==================
traefik: traefik:
image: traefik:latest image: traefik-coraza:latest
user: "0:1001" # группа docker на хосте user: "0:1001" # группа docker на хосте
command: command:
- "--api.insecure=true" - "--api.insecure=true"
@@ -20,9 +20,8 @@ services:
- "--accesslog=true" - "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log" - "--accesslog.filepath=/var/log/traefik/access.log"
- "--accesslog.format=json" - "--accesslog.format=json"
# Coraza WAF # Coraza WAF (локальный wasm из образа)
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik" - "--experimental.localPlugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
- "--experimental.plugins.coraza.version=v0.2.0"
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
+11
View File
@@ -0,0 +1,11 @@
FROM traefik:v3.2.0
ARG CORAZA_VERSION=v0.3.0
ARG CORAZA_MODULE=github.com/jcchavezs/coraza-http-wasm-traefik
RUN apk add --no-cache curl unzip \
&& mkdir -p /plugins-local/src/github.com/jcchavezs/coraza-http-wasm-traefik \
&& curl -fsSL -o /tmp/coraza.zip \
"https://github.com/jcchavezs/coraza-http-wasm-traefik/releases/download/${CORAZA_VERSION}/coraza-http-wasm-${CORAZA_VERSION}.zip" \
&& unzip -q /tmp/coraza.zip -d /plugins-local/src/github.com/jcchavezs/coraza-http-wasm-traefik \
&& rm /tmp/coraza.zip
+3 -3
View File
@@ -4,9 +4,9 @@
-export([fetch/3]). -export([fetch/3]).
fetch(CalendarId, Year, Month) -> fetch(CalendarId, Year, Month) ->
Start = {Year, Month, 1, 0, 0, 0}, Start = {{Year, Month, 1}, {0, 0, 0}},
End = {Year, Month, 31, 23, 59, 59}, End = {{Year, Month, calendar:last_day_of_the_month(Year, Month)}, {23, 59, 59}},
mnesia:dirty_select(event, [{#event{calendar_id = CalendarId, mnesia:dirty_select(event, [{#event{calendar_id = CalendarId,
start_time = '$1', _ = '_'}, start_time = '$1', _ = '_'},
[{'>=','$1', Start},{'=<','$1', End}], [{'>=', '$1', {const, Start}}, {'=<', '$1', {const, End}}],
['$_']}]). ['$_']}]).
+40 -6
View File
@@ -16,7 +16,7 @@ init([]) ->
{ok, #{}}. {ok, #{}}.
handle_call({get_node, Day}, _From, State) -> handle_call({get_node, Day}, _From, State) ->
Node = list_to_atom("eventhub_archive_" ++ Day ++ "@" ++ host()), Node = archive_node_name(Day),
case maps:find(Node, State) of case maps:find(Node, State) of
{ok, _Info} -> % переменная не используется, ок {ok, _Info} -> % переменная не используется, ок
{reply, {ok, Node}, update_access(State, Node)}; {reply, {ok, Node}, update_access(State, Node)};
@@ -55,14 +55,48 @@ update_access(State, Node) ->
State#{Node => Info#{timer => Ref, last_access => erlang:monotonic_time()}}. State#{Node => Info#{timer => Ref, last_access => erlang:monotonic_time()}}.
start_archive_node(Day) -> start_archive_node(Day) ->
Node = list_to_atom("eventhub_archive_" ++ Day ++ "@" ++ host()), Node = archive_node_name(Day),
case slave:start(host(), Node) of case start_archive_peer(Node) of
{ok, _} -> {ok, _} ->
rpc:call(Node, mnesia, start, []), case ensure_archive_node_ready(Node) of
{ok, Node}; ok -> {ok, Node};
Error -> Error {error, Reason} -> {error, Reason}
end;
{error, Reason} ->
{error, Reason}
end. end.
start_archive_peer(Node) ->
case os:getenv("CLUSTER_MODE") of
"true" ->
case peer:start_link(#{name => Node, host => host()}) of
{ok, _} -> {ok, Node};
{error, {already_started, _}} -> {ok, Node};
Error -> Error
end;
_ ->
CookieStr = atom_to_list(erlang:get_cookie()),
case slave:start(host(), Node, "-setcookie " ++ CookieStr) of
{ok, _} -> {ok, Node};
Error -> Error
end
end.
ensure_archive_node_ready(Node) ->
case rpc:call(Node, mnesia, start, [], 5000) of
ok ->
case rpc:call(Node, code, ensure_loaded, [archive_fetcher], 5000) of
{module, archive_fetcher} -> ok;
{error, Reason} -> {error, Reason};
{badrpc, Reason} -> {error, Reason}
end;
{badrpc, Reason} -> {error, Reason};
Other -> {error, Other}
end.
archive_node_name(Day) ->
list_to_atom("eventhub_archive_" ++ Day ++ "@" ++ host()).
stop_archive_node(Node) -> stop_archive_node(Node) ->
rpc:cast(Node, init, stop, []). rpc:cast(Node, init, stop, []).
+24 -4
View File
@@ -14,6 +14,8 @@
-include("records.hrl"). -include("records.hrl").
-define(ARCHIVE_CALL_TIMEOUT, 3000).
%%% cowboy_handler callback %%% cowboy_handler callback
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
init(Req, Opts) -> init(Req, Opts) ->
@@ -135,10 +137,28 @@ fetch_hot_events(CalendarId, Year, Month) ->
]). ]).
%% @private Извлекает архивные события через RPC на архивный узел. %% @private Извлекает архивные события через RPC на архивный узел.
%% При недоступности архива возвращает события из основной Mnesia.
-spec fetch_archive_events(binary(), integer(), integer()) -> list(#event{}). -spec fetch_archive_events(binary(), integer(), integer()) -> list(#event{}).
fetch_archive_events(CalendarId, Year, Month) -> fetch_archive_events(CalendarId, Year, Month) ->
DayStr = io_lib:format("~4..0B~2..0B", [Year, Month]), DayStr = lists:flatten(io_lib:format("~4..0B~2..0B", [Year, Month])),
case archive_manager:get_archive_node(lists:flatten(DayStr)) of case safe_get_archive_node(DayStr) of
{ok, Node} -> rpc:call(Node, archive_fetcher, fetch, [CalendarId, Year, Month]); {ok, Node} ->
{error, _} -> [] case rpc:call(Node, archive_fetcher, fetch, [CalendarId, Year, Month],
?ARCHIVE_CALL_TIMEOUT) of
Events when is_list(Events) -> Events;
_ -> fetch_hot_events(CalendarId, Year, Month)
end;
{error, _} ->
fetch_hot_events(CalendarId, Year, Month)
end.
-spec safe_get_archive_node(string()) -> {ok, node()} | {error, term()}.
safe_get_archive_node(Day) ->
try gen_server:call(archive_manager, {get_node, Day}, ?ARCHIVE_CALL_TIMEOUT) of
{ok, Node} when is_atom(Node) -> {ok, Node};
{error, Reason} -> {error, Reason};
Other -> {error, Other}
catch
exit:{timeout, _} -> {error, timeout};
exit:{noproc, _} -> {error, noproc}
end. end.
+32 -3
View File
@@ -41,7 +41,8 @@
client_delete/2, client_delete/2,
admin_patch/3]). admin_patch/3]).
-export([verify_user/2]). -export([verify_user/2]).
-export([future_date_iso8601/0]). -export([future_date_iso8601/0, iso8601_utc/1, future_month_range_iso8601/0,
next_month_start_iso8601/0, next_month_range_iso8601/0]).
%%%=================================================================== %%%===================================================================
%%% Конфигурация окружения (CT_MODE, ...) %%% Конфигурация окружения (CT_MODE, ...)
@@ -283,11 +284,39 @@ future_date() ->
-spec future_date_iso8601() -> binary(). -spec future_date_iso8601() -> binary().
future_date_iso8601() -> future_date_iso8601() ->
Seconds = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + 86400, iso8601_utc(future_date()).
{{Y,M,D},{H,Min,S}} = calendar:gregorian_seconds_to_datetime(Seconds),
-spec iso8601_utc(calendar:datetime()) -> binary().
iso8601_utc({{Y, M, D}, {H, Min, S}}) ->
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
[Y, M, D, H, Min, S])). [Y, M, D, H, Min, S])).
-spec future_month_range_iso8601() -> {binary(), binary()}.
future_month_range_iso8601() ->
{{Y, M, _D}, _} = future_date(),
month_range_iso8601(Y, M).
-spec next_month_start_iso8601() -> binary().
next_month_start_iso8601() ->
{{Y, M, _}, _} = future_date(),
{NY, NM} = next_month(Y, M),
iso8601_utc({{NY, NM, 1}, {10, 0, 0}}).
-spec next_month_range_iso8601() -> {binary(), binary()}.
next_month_range_iso8601() ->
{{Y, M, _}, _} = future_date(),
{NY, NM} = next_month(Y, M),
month_range_iso8601(NY, NM).
month_range_iso8601(Y, M) ->
From = iso8601_utc({{Y, M, 1}, {0, 0, 0}}),
LastDay = calendar:last_day_of_the_month(Y, M),
To = iso8601_utc({{Y, M, LastDay}, {23, 59, 59}}),
{From, To}.
next_month(Y, 12) -> {Y + 1, 1};
next_month(Y, M) -> {Y, M + 1}.
-spec register_and_login(binary(), binary()) -> binary(). -spec register_and_login(binary(), binary()) -> binary().
register_and_login(Email, Password) -> register_and_login(Email, Password) ->
% 1. Регистрируем пользователя % 1. Регистрируем пользователя
+8 -5
View File
@@ -43,7 +43,9 @@ test_create_single_event(Token, CalId) ->
%% @doc GET /v1/calendars/:calendar_id/events?from=...&to=... список с фильтром. %% @doc GET /v1/calendars/:calendar_id/events?from=...&to=... список с фильтром.
test_list_events_with_dates(Token, CalId) -> test_list_events_with_dates(Token, CalId) ->
ct:pal(" TEST: List events with date range"), ct:pal(" TEST: List events with date range"),
Path = <<"/v1/calendars/", CalId/binary, "/events?from=2026-05-01T00:00:00Z&to=2026-07-01T00:00:00Z">>, {From, To} = api_test_runner:future_month_range_iso8601(),
Path = <<"/v1/calendars/", CalId/binary, "/events?from=", From/binary,
"&to=", To/binary>>,
Events = api_test_runner:client_get(Path, Token), Events = api_test_runner:client_get(Path, Token),
?assert(is_list(Events)), ?assert(is_list(Events)),
?assert(length(Events) >= 1), ?assert(length(Events) >= 1),
@@ -58,7 +60,7 @@ test_create_recurring_event(Token, CalId) ->
Path = <<"/v1/calendars/", CalId/binary, "/events">>, Path = <<"/v1/calendars/", CalId/binary, "/events">>,
Body = jsx:encode(#{ Body = jsx:encode(#{
title => <<"Weekly Meeting">>, title => <<"Weekly Meeting">>,
start_time => api_test_runner:future_date_iso8601(), start_time => api_test_runner:next_month_start_iso8601(),
duration => 60, duration => 60,
recurrence => #{ recurrence => #{
freq => <<"WEEKLY">>, freq => <<"WEEKLY">>,
@@ -70,11 +72,12 @@ test_create_recurring_event(Token, CalId) ->
#{<<"id">> := RecurringId} = jsx:decode(list_to_binary(RespBody), [return_maps]), #{<<"id">> := RecurringId} = jsx:decode(list_to_binary(RespBody), [return_maps]),
ct:pal(" Created recurring event ~s", [RecurringId]), ct:pal(" Created recurring event ~s", [RecurringId]),
% Запрашиваем вхождения на месяц {From, To} = api_test_runner:next_month_range_iso8601(),
OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z">>, OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=", From/binary,
"&to=", To/binary>>,
Occs = api_test_runner:client_get(OccPath, Token), Occs = api_test_runner:client_get(OccPath, Token),
?assert(is_list(Occs)), ?assert(is_list(Occs)),
?assert(length(Occs) >= 4), % минимум 4 недели в июне ?assert(length(Occs) >= 4), % минимум 4 недели в полном месяце
FirstOcc = hd(Occs), FirstOcc = hd(Occs),
?assert(maps:is_key(<<"start_time">>, FirstOcc)), ?assert(maps:is_key(<<"start_time">>, FirstOcc)),
ct:pal(" OK: ~p occurrences found", [length(Occs)]). ct:pal(" OK: ~p occurrences found", [length(Occs)]).
@@ -36,8 +36,9 @@ test() ->
duration => 30, duration => 30,
recurrence => #{freq => <<"WEEKLY">>, interval => 1}}), recurrence => #{freq => <<"WEEKLY">>, interval => 1}}),
% Получаем вхождения (ответ – список карт) {From, To} = api_test_runner:future_month_range_iso8601(),
OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=2026-06-01T00:00:00Z&to=2026-06-30T00:00:00Z">>, OccPath = <<"/v1/events/", RecurringId/binary, "/occurrences?from=", From/binary,
"&to=", To/binary>>,
Occurrences = api_test_runner:client_get(OccPath, OwnerToken), Occurrences = api_test_runner:client_get(OccPath, OwnerToken),
?assert(is_list(Occurrences)), ?assert(is_list(Occurrences)),
?assert(length(Occurrences) >= 1), ?assert(length(Occurrences) >= 1),
+3 -3
View File
@@ -93,9 +93,9 @@ test_tag_filter(Token) ->
test_date_filter(Token) -> test_date_filter(Token) ->
ct:pal(" TEST: Search with date range"), ct:pal(" TEST: Search with date range"),
Events = extract_events( {From, To} = api_test_runner:future_month_range_iso8601(),
api_test_runner:client_get( Path = <<"/v1/search?from=", From/binary, "&to=", To/binary>>,
<<"/v1/search?from=2026-06-01T00:00:00Z&to=2026-06-15T23:59:59Z">>, Token)), Events = extract_events(api_test_runner:client_get(Path, Token)),
?assert(is_list(Events)), ?assert(is_list(Events)),
?assert(length(Events) >= 1), ?assert(length(Events) >= 1),
ct:pal(" OK: ~p events", [length(Events)]). ct:pal(" OK: ~p events", [length(Events)]).