Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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

23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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

20 changes: 20 additions & 0 deletions .github/workflows/weekly-load.yml
Original file line number Diff line number Diff line change
@@ -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

9 changes: 9 additions & 0 deletions SERVICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Карточка репозитория

| Поле | Значение |
|---|---|
| Ответственность | системные, load и resilience-тесты |
| Бизнес-код | отсутствует |
| Основной инструмент | k6 |
| Chaos | отдельный ручной запуск, выключен по умолчанию |

12 changes: 12 additions & 0 deletions catalog-info.yaml
Original file line number Diff line number Diff line change
@@ -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

20 changes: 20 additions & 0 deletions chaos/pod-delay.yaml
Original file line number Diff line number Diff line change
@@ -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

28 changes: 28 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -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]
18 changes: 18 additions & 0 deletions fake/nginx.conf
Original file line number Diff line number Diff line change
@@ -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"}';
}
}

13 changes: 13 additions & 0 deletions scripts/check.ps1
Original file line number Diff line number Diff line change
@@ -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 прошли."

13 changes: 13 additions & 0 deletions scripts/run-chaos.ps1
Original file line number Diff line number Diff line change
@@ -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 не применился." }

19 changes: 19 additions & 0 deletions scripts/run-load.ps1
Original file line number Diff line number Diff line change
@@ -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 }
}

26 changes: 26 additions & 0 deletions tests/smoke.js
Original file line number Diff line number Diff line change
@@ -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'),
});
}