From 62dae2c7819b63b6b66d10f85db680193f20e2af Mon Sep 17 00:00:00 2001 From: DanliaQwerty20 Date: Mon, 31 Aug 2026 01:52:51 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BB=D0=B8=D0=B3=D0=BE=D0=BD?= =?UTF-8?q?=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 10 ++++++++++ .github/workflows/ci.yml | 23 +++++++++++++++++++++++ .github/workflows/weekly-load.yml | 20 ++++++++++++++++++++ SERVICE.md | 9 +++++++++ catalog-info.yaml | 12 ++++++++++++ chaos/pod-delay.yaml | 20 ++++++++++++++++++++ compose.yaml | 28 ++++++++++++++++++++++++++++ fake/nginx.conf | 18 ++++++++++++++++++ scripts/check.ps1 | 13 +++++++++++++ scripts/run-chaos.ps1 | 13 +++++++++++++ scripts/run-load.ps1 | 19 +++++++++++++++++++ tests/smoke.js | 26 ++++++++++++++++++++++++++ 12 files changed, 211 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/weekly-load.yml create mode 100644 SERVICE.md create mode 100644 catalog-info.yaml create mode 100644 chaos/pod-delay.yaml create mode 100644 compose.yaml create mode 100644 fake/nginx.conf create mode 100644 scripts/check.ps1 create mode 100644 scripts/run-chaos.ps1 create mode 100644 scripts/run-load.ps1 create mode 100644 tests/smoke.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..83949ea --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +COMPOSE_PROJECT_NAME=portable-agent-test-lab +FAKE_IMAGE=nginxinc/nginx-unprivileged:1.29.4-alpine +K6_IMAGE=grafana/k6:2.2.0 +FAKE_PORT=18080 +TARGET_URL=http://fake-service:8080 +K6_USERS=5 +K6_TIME=20s +K6_P95_MS=300 +K6_ERROR_RATE=0.01 + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..86f3526 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + test: + name: test + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - name: Проверить конфигурацию + shell: pwsh + run: ./scripts/check.ps1 + - name: Запустить k6 smoke + shell: pwsh + run: ./scripts/run-load.ps1 + security: + uses: portable-agent/.github/.github/workflows/reusable-security.yml@main + diff --git a/.github/workflows/weekly-load.yml b/.github/workflows/weekly-load.yml new file mode 100644 index 0000000..1c518a1 --- /dev/null +++ b/.github/workflows/weekly-load.yml @@ -0,0 +1,20 @@ +name: Weekly load +on: + workflow_dispatch: + schedule: + - cron: "40 3 * * 1" +permissions: + contents: read +jobs: + load: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - name: Запустить расширенный локальный тест + shell: pwsh + env: + K6_USERS: "20" + K6_TIME: 2m + run: ./scripts/run-load.ps1 + diff --git a/SERVICE.md b/SERVICE.md new file mode 100644 index 0000000..308f2f1 --- /dev/null +++ b/SERVICE.md @@ -0,0 +1,9 @@ +# Карточка репозитория + +| Поле | Значение | +|---|---| +| Ответственность | системные, load и resilience-тесты | +| Бизнес-код | отсутствует | +| Основной инструмент | k6 | +| Chaos | отдельный ручной запуск, выключен по умолчанию | + diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..9b84374 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-lab + description: Системные и нагрузочные тесты Portable Agent + annotations: + github.com/project-slug: portable-agent/test-lab +spec: + type: test + lifecycle: experimental + owner: platform + diff --git a/chaos/pod-delay.yaml b/chaos/pod-delay.yaml new file mode 100644 index 0000000..3500f58 --- /dev/null +++ b/chaos/pod-delay.yaml @@ -0,0 +1,20 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: NetworkChaos +metadata: + name: test-delay + namespace: change-me + labels: + portable-agent.io/manual-test: "true" +spec: + action: delay + mode: one + selector: + namespaces: [change-me] + labelSelectors: + portable-agent.io/chaos-ready: "true" + delay: + latency: 200ms + correlation: "50" + jitter: 50ms + duration: 30s + diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..516785a --- /dev/null +++ b/compose.yaml @@ -0,0 +1,28 @@ +name: ${COMPOSE_PROJECT_NAME:-portable-agent-test-lab} +services: + fake-service: + image: ${FAKE_IMAGE:?set FAKE_IMAGE} + ports: + - "127.0.0.1:${FAKE_PORT:-18080}:8080" + volumes: + - ./fake/nginx.conf:/etc/nginx/conf.d/default.conf:ro + healthcheck: + test: [CMD, wget, -q, -O, /dev/null, http://127.0.0.1:8080/health] + interval: 2s + timeout: 2s + retries: 20 + k6: + image: ${K6_IMAGE:?set K6_IMAGE} + profiles: [test] + environment: + TARGET_URL: ${TARGET_URL:?set TARGET_URL} + K6_USERS: ${K6_USERS:-5} + K6_TIME: ${K6_TIME:-20s} + K6_P95_MS: ${K6_P95_MS:-300} + K6_ERROR_RATE: ${K6_ERROR_RATE:-0.01} + volumes: + - ./tests:/tests:ro + depends_on: + fake-service: + condition: service_healthy + command: [run, /tests/smoke.js] diff --git a/fake/nginx.conf b/fake/nginx.conf new file mode 100644 index 0000000..d711278 --- /dev/null +++ b/fake/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 8080; + server_name _; + location = /health { + default_type application/json; + return 200 '{"status":"UP"}'; + } + location = /api/test { + default_type application/json; + add_header X-Test-Lab true; + return 200 '{"result":"ok"}'; + } + location / { + default_type application/json; + return 404 '{"error":"not_found"}'; + } +} + diff --git a/scripts/check.ps1 b/scripts/check.ps1 new file mode 100644 index 0000000..5d43dd2 --- /dev/null +++ b/scripts/check.ps1 @@ -0,0 +1,13 @@ +$ErrorActionPreference = "Stop" +foreach ($file in @(".env.example", "compose.yaml", "tests/smoke.js", "chaos/pod-delay.yaml", "README.md", "AGENTS.md", "SERVICE.md")) { + if (-not (Test-Path $file)) { throw "Нет обязательного файла: $file" } +} +if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw "Docker не найден." } +& docker compose --env-file .env.example config --quiet +if ($LASTEXITCODE -ne 0) { throw "Compose config содержит ошибку." } +$script = Get-Content tests/smoke.js -Raw +foreach ($required in @("TARGET_URL", "thresholds", "http_req_failed", "http_req_duration")) { + if ($script -notmatch $required) { throw "В k6-тесте нет $required." } +} +Write-Host "Быстрые проверки test-lab прошли." + diff --git a/scripts/run-chaos.ps1 b/scripts/run-chaos.ps1 new file mode 100644 index 0000000..89eea77 --- /dev/null +++ b/scripts/run-chaos.ps1 @@ -0,0 +1,13 @@ +param( + [Parameter(Mandatory)][string]$Namespace, + [switch]$AllowChaos +) +$ErrorActionPreference = "Stop" +if (-not $AllowChaos) { throw "Нужен явный флаг -AllowChaos." } +if ($Namespace -match '(?i)prod|production') { throw "Chaos в production запрещён этим репозиторием." } +$label = & kubectl get namespace $Namespace -o 'jsonpath={.metadata.labels.portable-agent\.io/chaos-ready}' +if ($LASTEXITCODE -ne 0 -or $label -ne "true") { throw "У namespace нет метки portable-agent.io/chaos-ready=true." } +$yaml = (Get-Content chaos/pod-delay.yaml -Raw).Replace("change-me", $Namespace) +$yaml | & kubectl apply -f - +if ($LASTEXITCODE -ne 0) { throw "Chaos manifest не применился." } + diff --git a/scripts/run-load.ps1 b/scripts/run-load.ps1 new file mode 100644 index 0000000..74fc3ab --- /dev/null +++ b/scripts/run-load.ps1 @@ -0,0 +1,19 @@ +param([string]$TargetUrl = "") +$ErrorActionPreference = "Stop" +$envFiles = @("--env-file", ".env.example") +if (Test-Path .env) { $envFiles += @("--env-file", ".env") } +if ($TargetUrl) { + if ($TargetUrl -match '(?i)prod|production') { throw "Запуск против production запрещён." } + $env:TARGET_URL = $TargetUrl +} +try { + & docker compose @envFiles up -d --wait fake-service + if ($LASTEXITCODE -ne 0) { throw "Fake service не запустился." } + & docker compose @envFiles --profile test run --rm k6 + if ($LASTEXITCODE -ne 0) { throw "k6-пороги не выполнены." } +} +finally { + & docker compose @envFiles --profile test down | Out-Null + if ($TargetUrl) { Remove-Item Env:TARGET_URL -ErrorAction SilentlyContinue } +} + diff --git a/tests/smoke.js b/tests/smoke.js new file mode 100644 index 0000000..5343891 --- /dev/null +++ b/tests/smoke.js @@ -0,0 +1,26 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const target = __ENV.TARGET_URL; +if (!target) { + throw new Error('TARGET_URL is required'); +} + +export const options = { + vus: Number(__ENV.K6_USERS || 5), + duration: __ENV.K6_TIME || '20s', + thresholds: { + http_req_failed: [`rate<${__ENV.K6_ERROR_RATE || '0.01'}`], + http_req_duration: [`p(95)<${__ENV.K6_P95_MS || '300'}`], + checks: ['rate>0.99'], + }, +}; + +export default function () { + const response = http.get(`${target}/api/test`, { tags: { name: 'test' } }); + check(response, { + 'status is 200': (item) => item.status === 200, + 'response is json': (item) => item.headers['Content-Type']?.includes('application/json'), + }); +} + From 5ce3a855d0969b48f83622f490623e067fabfb49 Mon Sep 17 00:00:00 2001 From: DanliaQwerty20 Date: Mon, 31 Aug 2026 02:09:57 +0300 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC=20security=20w?= =?UTF-8?q?orkflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit