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
37 changes: 37 additions & 0 deletions .github/workflows/preview-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Pull request preview smoke

on:
pull_request:
paths:
- ".github/workflows/preview-smoke.yml"
- "charts/**"
- "scripts/check-chart.ps1"
- "scripts/check-preview.ps1"
- "scripts/smoke-preview.ps1"
- "Taskfile.yml"

permissions:
contents: read

jobs:
preview:
name: Preview namespace smoke
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: azure/setup-helm@v5.0.1
with:
version: v4.2.4
- name: Установить k3d
env:
K3D_VERSION: v5.9.0
run: |
curl -fsSLo /tmp/k3d-install.sh "https://raw.githubusercontent.com/k3d-io/k3d/${K3D_VERSION}/install.sh"
TAG="${K3D_VERSION}" bash /tmp/k3d-install.sh
- name: Проверить preview namespace
shell: pwsh
run: >-
./scripts/smoke-preview.ps1
-PullRequestNumber "${{ github.event.pull_request.number }}"
-ClusterName "pa-preview-${{ github.run_id }}"
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

`deploy` управляет окружением, а black-box сценарии принадлежат репозиторию `test-lab`. Не добавляй
сюда HTTP-проверки продуктового поведения; вызывай единый сценарий через `run-test-lab.ps1`.
`charts/preview` управляет только namespace guardrails. Сервисы во временном окружении устанавливаются
тем же `charts/service`, который используется в остальных Kubernetes-окружениях.

## Команды

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Compose-профиль `apps`: Channel Gateway, Agent Runtime, Action Service, MCP Gateway и Calendar MCP;
- Compose-профиль `observe`: OpenTelemetry, Prometheus, Grafana, Tempo и Loki;
- безопасный общий chart `charts/service`;
- изолированный PR preview namespace с quota, limits и default-deny сетью;
- Argo CD bootstrap `charts/gitops`;
- каталог окружений и генератор нового сервиса;
- быстрые проверки в каждом PR и полный k3d smoke по расписанию.
Expand Down Expand Up @@ -117,6 +118,17 @@ task verify
./scripts/smoke-k3d.ps1
```

Проверить модель временного окружения для номера pull request:

```powershell
task preview:smoke PR=123
```

Команда создаёт отдельный k3d-кластер и namespace `portable-agent-pr-123`, применяет guardrails,
устанавливает тестовый сервис и всегда удаляет только созданный кластер. Сейчас это ephemeral
CI-preview без публичного URL. После подключения общего Kubernetes-кластера тот же `charts/preview`
будет создавать долгоживущий namespace, а ingress и DNS останутся ответственностью платформы.

Подготовить delivery-файлы нового сервиса:

```powershell
Expand Down
7 changes: 7 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ tasks:
cmds:
- '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/start-local.ps1 -Observe'

preview:smoke:
desc: Проверить изолированное preview-окружение для номера PR
requires:
vars: [PR]
cmds:
- '{{.POWERSHELL}} {{.POWERSHELL_ARGS}} -File ./scripts/smoke-preview.ps1 -PullRequestNumber {{.PR}}'

services:up:
desc: Собрать и поднять все локальные сервисы
cmds:
Expand Down
6 changes: 6 additions & 0 deletions charts/preview/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v2
name: preview
description: Guardrails for an isolated Portable Agent pull request namespace
type: application
version: 0.1.0
appVersion: "1.0"
14 changes: 14 additions & 0 deletions charts/preview/templates/limitrange.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: LimitRange
metadata:
name: preview-limits
namespace: {{ .Values.namespace.name | quote }}
spec:
limits:
- type: Container
defaultRequest:
cpu: {{ .Values.limits.defaultRequestCpu | quote }}
memory: {{ .Values.limits.defaultRequestMemory | quote }}
default:
cpu: {{ .Values.limits.defaultCpu | quote }}
memory: {{ .Values.limits.defaultMemory | quote }}
9 changes: 9 additions & 0 deletions charts/preview/templates/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.namespace.name | quote }}
labels:
portable-agent.io/environment: preview
portable-agent.io/pull-request: {{ .Values.namespace.pullRequest | quote }}
annotations:
portable-agent.io/expires-at: {{ .Values.namespace.expiresAt | quote }}
10 changes: 10 additions & 0 deletions charts/preview/templates/networkpolicy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: preview-default-deny
namespace: {{ .Values.namespace.name | quote }}
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
12 changes: 12 additions & 0 deletions charts/preview/templates/resourcequota.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: ResourceQuota
metadata:
name: preview-budget
namespace: {{ .Values.namespace.name | quote }}
spec:
hard:
pods: {{ .Values.quota.pods | quote }}
requests.cpu: {{ .Values.quota.requestsCpu | quote }}
requests.memory: {{ .Values.quota.requestsMemory | quote }}
limits.cpu: {{ .Values.quota.limitsCpu | quote }}
limits.memory: {{ .Values.quota.limitsMemory | quote }}
31 changes: 31 additions & 0 deletions charts/preview/values.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["namespace", "quota", "limits"],
"properties": {
"namespace": {
"type": "object",
"required": ["name", "pullRequest", "expiresAt"],
"properties": {
"name": {
"type": "string",
"maxLength": 63,
"pattern": "^portable-agent-pr-[1-9][0-9]*$"
},
"pullRequest": {"type": "string", "pattern": "^[1-9][0-9]*$"},
"expiresAt": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
}
}
},
"quota": {
"type": "object",
"required": ["pods", "requestsCpu", "requestsMemory", "limitsCpu", "limitsMemory"]
},
"limits": {
"type": "object",
"required": ["defaultRequestCpu", "defaultRequestMemory", "defaultCpu", "defaultMemory"]
}
}
}
17 changes: 17 additions & 0 deletions charts/preview/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace:
name: portable-agent-pr-1
pullRequest: "1"
expiresAt: "2030-01-01T00:00:00Z"

quota:
pods: "20"
requestsCpu: "4"
requestsMemory: 8Gi
limitsCpu: "8"
limitsMemory: 16Gi

limits:
defaultRequestCpu: 100m
defaultRequestMemory: 128Mi
defaultCpu: 500m
defaultMemory: 512Mi
5 changes: 5 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ services/catalog.json -> environments/<env>/services/<name>/values.yaml
кластер. Полный smoke создаёт только временный k3d-кластер и всегда удаляет лишь созданный им
кластер.

Для pull request используется отдельный namespace `portable-agent-pr-<number>`. Chart `preview`
создаёт Namespace, ResourceQuota, LimitRange и default-deny NetworkPolicy, после чего приложения
устанавливаются существующим `service` chart. Сейчас CI проверяет эту модель в одноразовом k3d;
постоянный кластер позже добавит ingress, DNS и автоматическое удаление по метке срока жизни.

## Локальный execution slice

```mermaid
Expand Down
26 changes: 26 additions & 0 deletions docs/decisions/0005-pr-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ADR 0005: временное окружение pull request

Статус: принято.

## Контекст

Изменения общих Helm charts нужно проверять до merge в окружении, похожем на Kubernetes. Постоянного
общего кластера пока нет, а отдельный способ установки сервисов для preview быстро разойдётся с
основными окружениями.

## Решение

Каждый preview получает namespace `portable-agent-pr-<number>`. Chart `preview` создаёт только
границы окружения: метки PR и срока жизни, ResourceQuota, LimitRange и default-deny NetworkPolicy.
Сервисы устанавливаются существующим `service` chart.

До появления общего кластера workflow создаёт одноразовый k3d-кластер, проверяет guardrails и один
smoke deployment, затем удаляет созданный кластер. Сценарий не принимает произвольное имя namespace
и не изменяет найденный ранее кластер.

## Последствия

- Helm-модель preview проверяется в каждом затрагивающем её PR;
- ограничения ресурсов и сети появляются до запуска приложений;
- сервисы не получают отдельные preview-шаблоны;
- публичный URL, TLS и очистка по времени будут добавлены только вместе с постоянным кластером.
9 changes: 9 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ Taskfile — единая точка входа. Внутренние PowerShell
`task down` не удаляет данные. `task reset` предназначен для явного пересоздания тестового состояния
и запрашивает подтверждение.

Перед изменением Helm chart можно локально проверить PR preview:

```powershell
task preview:smoke PR=123
```

Номер формирует безопасное имя `portable-agent-pr-123`. Скрипт отказывается менять уже существующий
k3d-кластер и удаляет только тот кластер, который создал сам.

Работа идёт по TDD:

1. проверка описывает ожидаемый Kubernetes-ресурс;
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Helm chart устанавливается и проверяется в одно
2. единый локальный JWT с отдельными audience сервисов;
3. общий Channel Gateway и Agent Runtime с закрытыми API и контрактом `2.2.0`;
4. воспроизводимый Compose с локальной сборкой всех приложений.
5. изолированный ephemeral preview namespace для инфраструктурных pull request.

Следующий инфраструктурный пакетpreview namespace для pull request. Следующий продуктовый этап —
первый переносимый виджет подтверждения.
Следующий продуктовый этаппервый переносимый виджет подтверждения. Публичный preview URL появится
после подключения общего Kubernetes-кластера и контроллера жизненного цикла окружений.
8 changes: 8 additions & 0 deletions docs/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@ Action Service с событием в тестовом календарном к

Первый Docker build скачивает Gradle и Python packages и может быть заметно медленнее повторных
запусков. BuildKit сохраняет слои для следующих сборок.

## Preview smoke не запускается

1. Выполни `task doctor`, затем проверь `helm version`, `kubectl version --client` и `k3d version`.
2. Используй положительный номер PR: `task preview:smoke PR=123`.
3. Если кластер `pa-preview-123` уже существует, скрипт намеренно его не меняет. Удали свой старый
тестовый кластер вручную либо передай другое имя напрямую в `smoke-preview.ps1`.
4. В CI namespace и кластер удаляются в `finally`, даже если установка сервиса завершилась ошибкой.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ nav:
- Платформа разработки: decisions/0002-developer-platform.md
- Единые команды разработчика: decisions/0003-taskfile-workflow.md
- Владение сквозными тестами: decisions/0004-test-lab-ownership.md
- Временное окружение PR: decisions/0005-pr-preview.md
1 change: 1 addition & 0 deletions scripts/check-chart.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ if (-not (Get-Command helm -ErrorAction SilentlyContinue)) {
foreach ($item in @(
@{ Name = "platform-base"; Path = "charts/platform-base"; Args = @("--set", "environment=smoke") },
@{ Name = "service"; Path = "charts/service"; Args = @("-f", "charts/service/tests/smoke-values.yaml") },
@{ Name = "preview"; Path = "charts/preview"; Args = @() },
@{ Name = "gitops"; Path = "charts/gitops"; Args = @("-f", "charts/gitops/tests/smoke-values.yaml") }
)) {
& helm lint $item.Path --strict @($item.Args)
Expand Down
3 changes: 3 additions & 0 deletions scripts/check-compose.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ if (Test-Path -LiteralPath "scripts/check-apps.ps1") {
throw "Продуктовый E2E не должен дублироваться в deploy."
}
$testLabRunner = Get-Content -Raw -LiteralPath $testLabRunnerPath
if ($testLabRunner -notmatch '\$containerIds\s*=\s*@\(\s*\r?\n\s*\(& docker ps') {
throw "Результат docker ps должен сохраняться массивом до обращения по индексу."
}
foreach ($required in @("TEST_LAB_PATH", "portable-agent-realm.json", "CALENDAR_TEST_API_KEY", "DOCKER_NETWORK", "docker inspect", "com.docker.compose.service=channel-gateway", "http://channel-gateway:8080", "http://action-service:8080", "http://calendar-mcp:8080", "task", "test:e2e")) {
if ($testLabRunner -notmatch [regex]::Escape($required)) {
throw "Адаптер test-lab не содержит $required."
Expand Down
1 change: 1 addition & 0 deletions scripts/check-config.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
$ErrorActionPreference = "Stop"
& "$PSScriptRoot/check-preview.ps1"
& "$PSScriptRoot/check-chart.ps1"
& "$PSScriptRoot/test-new-service.ps1"
if (Get-Command docker -ErrorAction SilentlyContinue) { & "$PSScriptRoot/check-compose.ps1" }
Expand Down
4 changes: 3 additions & 1 deletion scripts/check-docs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ $requiredFiles = @(
"docs/development.md",
"docs/runbook.md",
"docs/decisions/0001-deploy-repository.md",
"docs/decisions/0003-taskfile-workflow.md"
"docs/decisions/0003-taskfile-workflow.md",
"docs/decisions/0004-test-lab-ownership.md",
"docs/decisions/0005-pr-preview.md"
)

$missing = $requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) }
Expand Down
51 changes: 51 additions & 0 deletions scripts/check-preview.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
$ErrorActionPreference = "Stop"

$requiredFiles = @(
"charts/preview/Chart.yaml",
"charts/preview/values.yaml",
"charts/preview/values.schema.json",
"charts/preview/templates/namespace.yaml",
"charts/preview/templates/resourcequota.yaml",
"charts/preview/templates/limitrange.yaml",
"charts/preview/templates/networkpolicy.yaml",
"scripts/smoke-preview.ps1",
".github/workflows/preview-smoke.yml"
)
foreach ($file in $requiredFiles) {
if (-not (Test-Path -LiteralPath $file)) { throw "Required preview file is missing: $file" }
}

$namespace = Get-Content -Raw -LiteralPath "charts/preview/templates/namespace.yaml"
foreach ($required in @("kind: Namespace", "portable-agent.io/environment", "portable-agent.io/pull-request", "portable-agent.io/expires-at")) {
if ($namespace -notmatch [regex]::Escape($required)) { throw "Preview namespace does not contain $required." }
}

$quota = Get-Content -Raw -LiteralPath "charts/preview/templates/resourcequota.yaml"
foreach ($required in @("kind: ResourceQuota", "requests.cpu", "requests.memory", "limits.cpu", "limits.memory", "pods")) {
if ($quota -notmatch [regex]::Escape($required)) { throw "Preview quota does not contain $required." }
}

$limit = Get-Content -Raw -LiteralPath "charts/preview/templates/limitrange.yaml"
foreach ($required in @("kind: LimitRange", "defaultRequest", "default")) {
if ($limit -notmatch [regex]::Escape($required)) { throw "Preview limits do not contain $required." }
}

$policy = Get-Content -Raw -LiteralPath "charts/preview/templates/networkpolicy.yaml"
foreach ($required in @("kind: NetworkPolicy", "podSelector: {}", "policyTypes", "Ingress", "Egress")) {
if ($policy -notmatch [regex]::Escape($required)) { throw "Preview network policy does not contain $required." }
}

$smoke = Get-Content -Raw -LiteralPath "scripts/smoke-preview.ps1"
foreach ($required in @("PullRequestNumber", "portable-agent-pr-", "charts/preview", "charts/service", "ResourceQuota", "LimitRange", "NetworkPolicy", "k3d cluster delete")) {
if ($smoke -notmatch [regex]::Escape($required)) { throw "Preview smoke does not contain $required." }
}

$workflow = Get-Content -Raw -LiteralPath ".github/workflows/preview-smoke.yml"
foreach ($required in @("pull_request", "github.event.pull_request.number", "smoke-preview.ps1", "K3D_VERSION", "helm")) {
if ($workflow -notmatch [regex]::Escape($required)) { throw "Preview workflow does not contain $required." }
}

$taskfile = Get-Content -Raw -LiteralPath "Taskfile.yml"
if ($taskfile -notmatch '(?m)^ preview:smoke:') { throw "Taskfile does not contain preview:smoke." }

Write-Host "Preview structure checks passed."
10 changes: 6 additions & 4 deletions scripts/run-test-lab.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ function Get-LocalSetting([string]$Name) {

function Get-AppNetwork {
$projectName = Get-LocalSetting "COMPOSE_PROJECT_NAME"
$containerIds = @(& docker ps `
--filter "label=com.docker.compose.project=$projectName" `
--filter "label=com.docker.compose.service=channel-gateway" `
--format "{{.ID}}") | Where-Object { $_ }
$containerIds = @(
(& docker ps `
--filter "label=com.docker.compose.project=$projectName" `
--filter "label=com.docker.compose.service=channel-gateway" `
--format "{{.ID}}") | Where-Object { $_ }
)
if ($LASTEXITCODE -ne 0 -or $containerIds.Count -ne 1) {
throw "Expected one running Channel Gateway container in Compose project $projectName."
}
Expand Down
Loading
Loading