From 35a2bfa719a00e438c9a032757968f01647d746b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 13 Aug 2026 21:51:36 +0400 Subject: [PATCH] =?UTF-8?q?chore:=20git=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE,=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=83=D1=88=D0=B0=20=D1=81=D0=B8=D0=BD=D0=BA=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=92=D0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/copilot-instructions.md | 2 +- .github/pravila.md | 13 +- BENCHMARK-COMPARISON.md | 88 -- ONBOARDING.md | 168 --- PLAN.md | 1693 ------------------------------- 5 files changed, 8 insertions(+), 1956 deletions(-) delete mode 100644 BENCHMARK-COMPARISON.md delete mode 100644 ONBOARDING.md delete mode 100644 PLAN.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9888aa0..6a023ea 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,5 +16,5 @@ - Без додумывания, без «логичных следующих шагов». ## ЕСЛИ ПРАВИШЬ КОД -- Сначала прочитай /home/naeel/nubes/SQS-service/.github/pravila.md (там: rsync на ВМ, git, docker, ssh, деплой). +- Сначала прочитай /home/naeel/nubes/SQS-service/.github/pravila.md (там: git локально + после пуша синк на ВМ, docker/ssh на ВМ, деплой). diff --git a/.github/pravila.md b/.github/pravila.md index 00f54b0..6bc526f 100644 --- a/.github/pravila.md +++ b/.github/pravila.md @@ -38,7 +38,7 @@ /home/naeel/nubes/SQS-service/ \ naeel@5.172.178.213:~/terra/SQS-service/ -3. Git (add/commit/push) выполнять ТОЛЬКО НА ВМ (через SSH) ПОСЛЕ синхронизации +3. Git (add/commit/push) выполнять ЛОКАЛЬНО. После пуша — ОБЯЗАТЕЛЬНО синк на ВМ (rsync). 4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ 5. Перед запуском любой команды на ВМ обязательно убедиться, что синхронизация (rsync) выполнена 6. SCP, sshfs, remote_dev и маунты больше НЕ используются @@ -46,8 +46,9 @@ Пример: 1. Редактируешь локально -2. rsync на ВМ -3. Выполняешь команды через SSH на ВМ +2. git add/commit/push ЛОКАЛЬНО +3. rsync на ВМ (после пуша) +4. Выполняешь команды через SSH на ВМ ## SSH @@ -57,7 +58,7 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА' ``` -Запрещено локально: `go`, `docker`, `kubectl`, `helm`, `terraform`, `curl/wget`, `git push/pull`, любые скрипты проекта. +Запрещено локально: `go`, `docker`, `kubectl`, `helm`, `terraform`, `curl/wget`, любые скрипты проекта. (git — разрешён локально) ## Документация @@ -68,8 +69,8 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout= ## Git ⛔⛔⛔ АБСОЛЮТНОЕ ПРАВИЛО: -- Git — ТОЛЬКО НА ВМ (через SSH) ПОСЛЕ синхронизации rsync. НИКОГДА локально. -- Разрешены ТОЛЬКО операции: `git add`, `git commit`, `git push` (на ВМ). +- Git — ЛОКАЛЬНО в /home/naeel/nubes/SQS-service. После push — синк на ВМ (rsync). +- Разрешены операции: `git add`, `git commit`, `git push` (локально). - ЗАПРЕЩЕНО: git pull, git fetch, git rebase, git merge, git reset, git stash, git checkout — что угодно кроме add/commit/push. - Если push отклонён — СТОП, доложить пользователю. Не лезть в pull/merge/rebase самостоятельно. diff --git a/BENCHMARK-COMPARISON.md b/BENCHMARK-COMPARISON.md deleted file mode 100644 index 789d088..0000000 --- a/BENCHMARK-COMPARISON.md +++ /dev/null @@ -1,88 +0,0 @@ -# Сравнительный анализ shared-sqs vs Yandex MQ - -**Дата:** 2026-04-12 06:05 UTC - -Итог сравнительных тестов shared-sqs и Yandex MQ. - -## Главное - -- shared-sqs показал конкурентоспособные результаты относительно Yandex MQ. -- На основных API-операциях shared-sqs в текущем прогоне быстрее или на уровне Yandex MQ. -- На batch-операциях shared-sqs также выглядит сильно. -- Проверкой охвачены все 17 поддерживаемых SQS-операций. - -## Короткий вывод - -Если смотреть на практические сценарии отправки, чтения и batch-обработки сообщений, shared-sqs уже выглядит как сильная реализация с хорошей latency и без явного проигрыша managed-сервису. - -## Самые важные цифры - -Формат: `min / avg / max / p95`, миллисекунды. - -| Операция | Yandex MQ | shared-sqs | Что это значит | -|---|---:|---:|---| -| GetQueueUrl | 766 / 1346 / 2266 / 2060 | 739 / 752 / 770 / 760 | shared-sqs намного стабильнее | -| GetQueueAttributes | 804 / 820 / 847 / 828 | 738 / 752 / 768 / 765 | shared-sqs быстрее | -| SendMessage 1KB | 804 / 811 / 824 / 823 | 748 / 760 / 770 / 767 | shared-sqs быстрее | -| SendMessage 10KB | 916 / 967 / 996 / 987 | 880 / 913 / 965 / 951 | shared-sqs быстрее | -| SendMessage 32KB | 905 / 930 / 948 / 947 | 883 / 904 / 939 / 928 | shared-sqs быстрее | -| SendMessageBatch 10 | 784 / 803 / 827 / 820 | 718 / 746 / 782 / 759 | shared-sqs быстрее | -| ReceiveMessage | 782 / 826 / 869 / 864 | 738 / 748 / 777 / 751 | shared-sqs быстрее | -| DeleteMessage | 783 / 800 / 823 / 814 | 727 / 742 / 757 / 755 | shared-sqs быстрее | -| DeleteMessageBatch 10 | 818 / 845 / 872 / 864 | 742 / 783 / 821 / 810 | shared-sqs быстрее | -| ChangeMessageVisibilityBatch 10 | 806 / 829 / 848 / 841 | 803 / 824 / 838 / 836 | почти паритет, но shared-sqs чуть быстрее | - -## Покрытие тестов по всем операциям - -Ниже показано покрытие по каждой из 17 операций. - -Обозначения: - -- `Сравнение latency` — операция вошла в прямое сравнение shared-sqs и Yandex MQ. -- `Функциональная проверка` — операция отдельно проверялась на корректную работу. -- `Функционально проверено` — операция подтверждена в shared-sqs, без отдельной публичной latency-таблицы. - -| Операция | Статус проверки | Комментарий | -|---|---|---| -| CreateQueue | Функциональная проверка | Использовалась в setup и проверялась как часть queue lifecycle | -| DeleteQueue | Функциональная проверка | Проверялась как часть queue lifecycle | -| GetQueueAttributes | Сравнение latency | Полноценное сравнение с Yandex MQ | -| GetQueueUrl | Сравнение latency | Полноценное сравнение с Yandex MQ | -| ListQueues | Сравнение latency | Полноценное сравнение с Yandex MQ | -| PurgeQueue | Сравнение latency | Контрольный замер, без многократного цикла | -| SetQueueAttributes | Сравнение latency | Полноценное сравнение с Yandex MQ | -| SendMessage | Сравнение latency | Сравнение для 1KB, 10KB и 32KB | -| SendMessageBatch | Сравнение latency | Batch из 10 сообщений | -| ReceiveMessage | Сравнение latency | Полноценное сравнение с Yandex MQ | -| DeleteMessage | Сравнение latency | Полноценное сравнение с Yandex MQ | -| DeleteMessageBatch | Сравнение latency | Batch delete на 10 сообщений | -| ChangeMessageVisibility | Сравнение latency | Полноценное сравнение с Yandex MQ | -| ChangeMessageVisibilityBatch | Сравнение latency | Batch visibility на 10 сообщений | -| TagQueue | Функционально проверено | Операция поддерживается и отдельно проверена | -| UntagQueue | Функционально проверено | Операция поддерживается и отдельно проверена | -| ListQueueTags | Функционально проверено | Операция поддерживается и отдельно проверена | - -Итог по покрытию: - -- `10` операций вошли в прямое latency-сравнение. -- `4` queue/control-plane операции дополнительно подтверждены функциональными сценариями. -- `3` tag-операции отдельно подтверждены функционально. - -## Throughput - -Тест: `SendMessage 1KB`, `5` воркеров по `10` сообщений. - -| Провайдер | Успешно | Общее время | Грубая оценка | -|---|---:|---:|---:| -| Yandex MQ | 50 / 50 | 9116 ms | ~5 msg/s | -| shared-sqs | 50 / 50 | 8580 ms | ~5 msg/s | - -Практический смысл: - -- По грубой оценке `msg/s` здесь паритет. -- По общему времени shared-sqs завершает тест немного быстрее. -- Основное ограничение этого сценария задаётся AWS CLI, а не backend обоих сервисов. - -## Финальный вывод для пользователя - -shared-sqs уже выглядит достаточно сильным решением: latency на уровне или ниже Yandex MQ, batch-операции быстрые, а в общей рабочей зоне сервис показывает уверенные результаты. \ No newline at end of file diff --git a/ONBOARDING.md b/ONBOARDING.md deleted file mode 100644 index 717f588..0000000 --- a/ONBOARDING.md +++ /dev/null @@ -1,168 +0,0 @@ -# ONBOARDING.md — Памятка для нового чата / агента -# Дата: 2026-04-10 -# Автор: GitHub Copilot (Claude Opus 4.6) - ---- - -## Что это за проект - -**SQS-service** — multi-tenant сервис очередей сообщений, совместимый с AWS SQS API. -Единственный в open source проект такого рода (подтверждено поиском по GitHub: 0 результатов по `multi-tenant sqs compatible`). - -Ближайшие аналоги — ElasticMQ (Scala) и GoAws (Go), но оба **single-tenant** и предназначены только для local dev/testing. - ---- - -## Откуда взялся код - -Основан на **GoAws** (https://github.com/Admiral-Piett/goaws). -Первый коммит в `sless`: `shared-sqs: Этап 1 — клон GoAWS, удаление SNS, go build OK` -Затем поверх GoAws построены: мультитенантность, auth, admin API, WebUI, Redis persistence. - ---- - -## Текущая версия - -**v0.1.14** — работает в кластере iot-naeel - -| Параметр | Значение | -|----------|---------| -| Endpoint | `https://qu.kube5s.ru` | -| IP | `185.247.187.151` | -| Admin token | `sqs-admin-7a7d8bd0c060a75c198d48680f34077a` | -| Demo Access Key | `SSAK-demo-shared-sqs` | -| Demo Secret Key | `demo-secret-key-shared-sqs-ngcloud-2026` | -| Docker image | `naeel/shared-sqs:v0.1.14` | -| Redis | `rfrm-redisk8s.54467f74-67f5-4ca7-9b4a-bdd50e8c23d6.svc.cluster.local:6379` | -| Kubeconfig | `/home/naeel/.kube/iot-naeel.yaml` (на ВМ) | -| Namespace | `shared-sqs` | - ---- - -## Инфраструктура — ВАЖНО - -### ВМ и монтирование - -- **ВМ**: `naeel@5.172.178.213` -- **SSH-ключ**: `~/.ssh/naeel_vm_id_ed25519` -- **Реальный путь на ВМ**: `/home/naeel/terra/SQS-service` -- **Смонтировано как**: `/home/naeel/remote_dev/SQS-service` (sshfs) - -### ⛔ КРИТИЧЕСКОЕ ПРАВИЛО - -`/home/naeel/remote_dev/SQS-service` — это **СМОНТИРОВАННАЯ ПАПКА**. -Физически файлы на ВМ. **ВСЕ команды выполнять ТОЛЬКО через SSH на ВМ:** - -```bash -ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ - naeel@5.172.178.213 'cd /home/naeel/terra/SQS-service && <КОМАНДА>' -``` - -Через IDE — ТОЛЬКО редактирование файлов. Терминальные команды — ТОЛЬКО SSH. - ---- - -## Архитектура кода - -``` -app/ -├── cmd/goaws.go — точка входа, HTTP сервер -├── cmd/seed.go — загрузка демо-данных -├── models/ — Queue, Message, Tenant, SyncQueues (глобальный мьютекс!) -├── gosqs/ — реализация SQS API (CreateQueue, SendMessage, ReceiveMessage и т.д.) -├── admin/admin.go — Admin API: создание/получение тенантов -├── tenant/tenant_store.go — хранилище тенантов (access key → tenant) -├── auth/auth_middleware.go — AWS Signature V4 парсинг credential -├── persistence/redis.go — Redis write-through adapter -├── router/router.go — маршруты HTTP -├── ui/index.html — WebUI (встроен через embed.go) -└── utils/, interfaces/ — утилиты -``` - ---- - -## Известные проблемы (не трогать пока не попросят) - -1. **Глобальный мьютекс** `models.SyncQueues` — Lock/Unlock на ВСЕ операции. Bottleneck при нагрузке. -2. **Нет DLQ** (Dead Letter Queue) -3. **Long Polling naïve** — sleep loop вместо push -4. **Нет rate limiting** per tenant -5. **Нет метрик** (Prometheus) -6. **Single pod** — нет горизонтального масштабирования - ---- - -## Что делать дальше (от пользователя) - -### 1. Статистика для биллинга - -Нужен сбор метрик по каждому тенанту: -- Количество отправленных/полученных сообщений -- Количество созданных очередей -- Объём данных (bytes in/out) -- Время жизни сообщений - -**Где встраивать**: `app/gosqs/send_message.go`, `app/gosqs/receive_message.go`, `app/gosqs/create_queue.go` -**Где хранить**: Новый пакет `app/billing/` или расширить `app/persistence/redis.go` -**Формат**: Redis HINCRBY per tenant per day, или отдельная таблица в PostgreSQL - -### 2. Защита конфиденциальности - -- Тела сообщений не должны логироваться -- Admin API должен быть доступен только из внутренней сети (или по отдельному порту) -- Тенанты не должны видеть друг друга (уже реализовано на уровне auth) -- Рассмотреть шифрование тел сообщений at rest (в Redis) -- Audit log: кто, когда, что делал - -**Где встраивать**: middleware в `app/router/router.go`, новый пакет `app/audit/` - -### 3. Рекомендации по порядку - -1. Сначала **биллинг** — простой счётчик в Redis, можно сделать за 1 сессию -2. Затем **audit log** — middleware логирует tenant_id + action + timestamp -3. Затем **encryption at rest** — обёртка над Redis get/set -4. В последнюю очередь — rate limiting (зависит от биллинга) - ---- - -## Тесты - -| Скрипт | Описание | Как запускать | -|--------|----------|--------------| -| `tests/quick_test.sh` | 6 базовых тестов | `bash tests/quick_test.sh` | -| `tests/hardcore_test.sh` | Суровые тесты | `bash tests/hardcore_test.sh` | -| `tests/shared_sqs_test.sh` | E2E тесты | `bash tests/shared_sqs_test.sh` | - -**ВАЖНО**: В `tests/quick_test.sh` используются demo credentials (`SSAK-demo-shared-sqs`). -В `tests/hardcore_test.sh` может быть admin token — НЕ КОММИТИТЬ в публичные репы! - ---- - -## Git workflow - -- Репо: `https://gitea.services.ngcloud.ru/Nail/SQS-service` -- Ветка: `main` -- Коммитить и пушить после каждого завершённого этапа (правило из copilot-instructions) -- Все команды git — через SSH на ВМ - ---- - -## Сборка и деплой - -```bash -# На ВМ: -cd /home/naeel/terra/SQS-service - -# Сборка -docker build -t naeel/shared-sqs:v0.1.15 . -docker push naeel/shared-sqs:v0.1.15 - -# Деплой -export KUBECONFIG=/home/naeel/.kube/iot-naeel.yaml -kubectl set image deployment/shared-sqs shared-sqs=naeel/shared-sqs:v0.1.15 -n shared-sqs -kubectl rollout status deployment/shared-sqs -n shared-sqs -``` - ---- - -*Создано: 2026-04-10. Обновлять при значимых изменениях.* diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index fdcb021..0000000 --- a/PLAN.md +++ /dev/null @@ -1,1693 +0,0 @@ -# shared-sqs — План реализации - -**Дата:** 2026-04-09 -**Исполнитель:** Claude Sonnet (или другой агент) -**Подготовил:** Claude Opus 4 (анализ GoAWS, архитектура, ловушки) - ---- - -## 1. ЧТО ЭТО - -Multi-tenant SQS-совместимый сервис на базе форка [GoAWS](https://github.com/Admiral-Piett/goaws) (Go, MIT, 835 stars). - -**Отличие от sqs-operator:** sqs-operator деплоит каждому тенанту ОТДЕЛЬНЫЙ pod с ElasticMQ (~300MB RAM каждый). shared-sqs — ОДИН pod обслуживает ВСЕХ тенантов (~50MB RAM base). - -**Что shared-sqs делает:** -- SQS-совместимый API (CreateQueue, SendMessage, ReceiveMessage, DeleteMessage и т.д.) -- Аутентификация по AccessKeyId (из AWS Authorization header) -- Изоляция очередей между тенантами (тенант видит ТОЛЬКО свои очереди) -- Admin API для управления тенантами (CRUD) -- Работает с AWS CLI и AWS SDK без модификаций - ---- - -## 2. АРХИТЕКТУРА GoAWS (то, что форкаем) - -### 2.1 Структура исходников -``` -app/ -├── cmd/goaws.go # Entry point (~40 LOC): флаги, загрузка конфига, HTTP сервер -├── conf/ # Загрузка YAML конфига -├── gosqs/ # SQS handlers (ЯДРО — ~20 файлов) -│ ├── create_queue.go # CreateQueueV1() -│ ├── send_message.go # SendMessageV1() -│ ├── receive_message.go # ReceiveMessageV1() -│ ├── delete_message.go # DeleteMessageV1() -│ ├── delete_message_batch.go -│ ├── delete_queue.go -│ ├── get_queue_attributes.go -│ ├── get_queue_url.go -│ ├── list_queues.go -│ ├── purge_queue.go -│ ├── send_message_batch.go -│ ├── set_queue_attributes.go -│ ├── change_message_visibility.go -│ ├── queue_attributes.go # Helpers для атрибутов -│ └── gosqs.go # PeriodicTasks (visibility timeout, DLQ, dedup) -├── gosns/ # SNS handlers — НЕ НУЖНЫ, УДАЛИТЬ -├── models/ -│ ├── globals.go # SyncQueues, SyncTopics — глобальные map + RWMutex -│ ├── models.go # Queue, SqsMessage, Topic structs -│ ├── configuration.go # Environment, EnvQueue, config structs -│ ├── constants.go -│ ├── conversions.go # Парсинг тел запросов -│ ├── errors.go # AWS-совместимые ошибки -│ ├── helpers.go -│ ├── requests.go # Request structs (CreateQueueRequest, SendMessageRequest и т.д.) -│ └── responses.go # Response structs (XML + JSON) -├── router/ -│ └── router.go # gorilla/mux, actionHandler, routingTableV1 -├── interfaces/ # AbstractResponseBody interface -├── utils/ # Hash, MD5, REQUEST_TRANSFORMER -├── mocks/ # Тестовые моки -├── fixtures/ # Тестовые данные -├── servertest/ -└── test/ -``` - -### 2.2 Критические архитектурные точки - -**Глобальный state** (`models/globals.go`): -```go -var SyncQueues = struct { - sync.RWMutex - Queues map[string]*Queue -}{Queues: make(map[string]*Queue)} -``` -Все очереди храняться В ОДНОМ map. Ключ = имя очереди (string). - -**Роутинг** (`router/router.go`): -```go -r.HandleFunc("/", actionHandler) -r.HandleFunc("/{account}", actionHandler) -r.HandleFunc("/queue/{queueName}", actionHandler) -r.HandleFunc("/{account}/{queueName}", actionHandler) -``` -Все запросы идут в `actionHandler`, который извлекает `Action` из: -- Query param `Action=CreateQueue` (AWS Query Protocol) -- Header `X-Amz-Target: AmazonSQS.CreateQueue` (AWS JSON Protocol) - -**Dispatch table** (`router/router.go`): -```go -var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - "CreateQueue": sqs.CreateQueueV1, - "SendMessage": sqs.SendMessageV1, - "ReceiveMessage": sqs.ReceiveMessageV1, - // ... все SQS + SNS actions -} -``` - -**URL конструкция** (в create_queue.go): -```go -queueUrl := "http://" + host + ":" + port + "/" + accountID + "/" + queueName -queueArn := "arn:aws:sqs:" + region + ":" + accountID + ":" + queueName -``` -`accountID` берётся из `models.CurrentEnvironment.AccountID` — ГЛОБАЛЬНАЯ переменная (одна на всех). - -**Зависимости** (go.mod): -- `gorilla/mux v1.8.0` — роутер -- `gorilla/schema v1.4.1` — form decoder -- `google/uuid v1.6.0` — UUID генерация -- `sirupsen/logrus` — логирование -- `ghodss/yaml` — YAML парсинг -- `aws/aws-sdk-go v1.47.3` — только для тестов - ---- - -## 3. ПЛАН ИЗМЕНЕНИЙ - -### 3.0 Общие правила работы - -**КРИТИЧНО — все команды ТОЛЬКО через SSH:** -``` -ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 КОМАНДА -``` - -**Go файлы редактировать ТОЛЬКО через Python patch скрипты на VM**, НЕ через sshfs/VS Code. - -**Markdown/yaml/conf файлы** можно создавать через `cat > file << EOF` по SSH. - -**Перед go build** — убедиться что бинарник в `.gitignore`. - -### Этап 1: Клон GoAWS + чистка (1 час) - -**Действия:** - -1. Клонировать GoAWS в `shared-sqs/`: -```bash -cd ~/terra/sless/shared-sqs -git clone https://github.com/Admiral-Piett/goaws.git _upstream -# Копируем ТОЛЬКО нужное: -cp -r _upstream/app ./app -cp _upstream/go.mod ./go.mod -cp _upstream/go.sum ./go.sum -cp _upstream/Dockerfile ./Dockerfile -rm -rf _upstream -``` - -2. Поменять module name в go.mod: -``` -module shared-sqs -go 1.22 -``` -(Повысить версию Go с 1.18 до 1.22+) - -3. Обновить все import paths: - - Заменить `github.com/Admiral-Piett/goaws/app/` → `shared-sqs/app/` - - Это во ВСЕХ .go файлах - -4. УДАЛИТЬ всё связанное с SNS: - - `app/gosns/` — целиком - - Из `router/router.go` — убрать все SNS записи из `routingTableV1` - - Из `models/globals.go` — убрать `SyncTopics` - - Из `models/models.go` — убрать `Topic`, `Subscription`, `SNSMessage`, `FilterPolicy` - - Из `models/configuration.go` — убрать `EnvTopic`, `EnvSubsciption` - - Из `models/requests.go` и `responses.go` — убрать SNS-related structs - -5. УДАЛИТЬ тестовые/mock директории (мы напишем свои тесты): - - `app/mocks/` - - `app/fixtures/` - - `app/servertest/` - - `app/test/` - - `app/smoke_tests/` (если скопировалась) - -6. Проверить что компилируется: -```bash -cd ~/terra/sless/shared-sqs -go mod tidy -go build -o shared-sqs app/cmd/goaws.go -``` - -7. Проверить что стартует: -```bash -./shared-sqs -debug -# В другом окне: curl http://localhost:4100/health -# Ожидание: 200 OK -``` - -**Тест прохождения этапа:** `go build` успешен, `/health` возвращает 200. - ---- - -### Этап 2: Tenant Model + хранилище (30 мин) - -**Создать файл `app/tenant/tenant.go`:** - -```go -package tenant - -import ( - "crypto/rand" - "encoding/hex" - "sync" - "time" -) - -// Tenant — модель тенанта shared-sqs -type Tenant struct { - ID string // уникальный идентификатор (генерируется) - Name string // имя тенанта (для отображения) - AccessKey string // аналог AWS AccessKeyId - SecretKey string // аналог AWS SecretAccessKey - MaxQueues int // лимит очередей (0 = безлимит) - CreatedAt time.Time - Active bool -} - -// TenantStore — in-memory хранилище тенантов -type TenantStore struct { - sync.RWMutex - // byID — поиск по ID - byID map[string]*Tenant - // byAccessKey — поиск по AccessKey (для auth) - byAccessKey map[string]*Tenant -} - -// NewTenantStore — создаёт пустое хранилище -func NewTenantStore() *TenantStore { ... } - -// Create — создаёт нового тенанта, генерирует ключи -func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) { ... } - -// GetByAccessKey — поиск тенанта по AccessKeyId (для auth middleware) -func (s *TenantStore) GetByAccessKey(accessKey string) (*Tenant, bool) { ... } - -// GetByID — поиск по ID -func (s *TenantStore) GetByID(id string) (*Tenant, bool) { ... } - -// Delete — удаление тенанта -func (s *TenantStore) Delete(id string) bool { ... } - -// List — список всех тенантов -func (s *TenantStore) List() []*Tenant { ... } -``` - -**Генерация ключей (БЕЗОПАСНАЯ):** -```go -func generateAccessKey() string { - // Формат: SSAK-{random hex 12} (SS = Shared SQS) - b := make([]byte, 12) - rand.Read(b) - return "SSAK-" + hex.EncodeToString(b) -} - -func generateSecretKey() string { - // 32 байта random → 64 hex символа - b := make([]byte, 32) - rand.Read(b) - return hex.EncodeToString(b) -} -``` - -**ЛОВУШКА #1:** Используй `crypto/rand`, НЕ `math/rand` для ключей. `math/rand` предсказуем. - -**ЛОВУШКА #2:** Два индекса (byID и byAccessKey) — при Delete надо удалить из ОБОИХ. - -**Тест прохождения:** TenantStore создаёт/ищет/удаляет тенантов. Ключи уникальны. - ---- - -### Этап 3: Auth Middleware (30 мин) - -**Создать файл `app/auth/middleware.go`:** - -```go -package auth - -import ( - "context" - "net/http" - "strings" -) - -type contextKey string -const TenantContextKey contextKey = "tenant" - -// AuthMiddleware — извлекает AccessKeyId из AWS Authorization header -// и находит тенанта в store -func AuthMiddleware(store *tenant.TenantStore) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // /health и /admin/** — пропускаем (разная auth) - if r.URL.Path == "/health" { - next.ServeHTTP(w, r) - return - } - if strings.HasPrefix(r.URL.Path, "/admin/") { - next.ServeHTTP(w, r) - return - } - - accessKeyId := extractAccessKeyId(r) - if accessKeyId == "" { - writeAuthError(w, "MissingAuthenticationToken") - return - } - - t, ok := store.GetByAccessKey(accessKeyId) - if !ok || !t.Active { - writeAuthError(w, "InvalidClientTokenId") - return - } - - ctx := context.WithValue(r.Context(), TenantContextKey, t) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} -``` - -**Извлечение AccessKeyId из Authorization header:** -```go -// extractAccessKeyId — извлекает AWS AccessKeyId из запроса -// Формат header: "AWS4-HMAC-SHA256 Credential={AccessKeyId}/{date}/{region}/sqs/aws4_request, ..." -func extractAccessKeyId(r *http.Request) string { - // Вариант 1: Authorization header (AWS Signature V4) - auth := r.Header.Get("Authorization") - if strings.HasPrefix(auth, "AWS4-HMAC-SHA256") { - // Ищем "Credential=" и берём всё до первого "/" - idx := strings.Index(auth, "Credential=") - if idx >= 0 { - rest := auth[idx+len("Credential="):] - slashIdx := strings.Index(rest, "/") - if slashIdx > 0 { - return rest[:slashIdx] - } - } - } - - // Вариант 2: Query parameter (presigned URLs) - if cred := r.URL.Query().Get("X-Amz-Credential"); cred != "" { - parts := strings.SplitN(cred, "/", 2) - if len(parts) > 0 { - return parts[0] - } - } - - return "" -} -``` - -**ЛОВУШКА #3:** AWS CLI ВСЕГДА отправляет Authorization header с Signature V4. Наш middleware должен УМЕТЬ парсить его, даже если мы НЕ ПРОВЕРЯЕМ подпись. - -**ЛОВУШКА #4:** Когда AWS SDK делает `ReceiveMessage` с long polling (WaitTimeSeconds > 0), соединение держится до 20 секунд. Auth middleware не должен ставить таймаут короче. - -**ЛОВУШКА #5:** AWS SDK может отправлять запросы с `X-Amz-Security-Token` (STS). Игнорировать этот header. - -**Тест прохождения:** Запрос с `Authorization: AWS4-HMAC-SHA256 Credential=SSAK-abc123.../...` → тенант найден в контексте. Запрос без header → 403. - ---- - -### Этап 4: Queue Isolation — САМЫЙ КРИТИЧНЫЙ (2 часа) - -Это ЯДРО всех изменений. Все SQS handlers должны работать с tenant-scoped очередями. - -**4.1 Изменение ключей в SyncQueues** - -Сейчас: `SyncQueues.Queues["my-queue"]` -Станет: `SyncQueues.Queues["SSAK-abc123:my-queue"]` - -Формат внутреннего ключа: `{tenantAccessKey}:{queueName}` - -Почему AccessKey а не TenantID: AccessKey уже есть в auth context, не надо лишний lookup. AccessKey уникален. - -**4.2 Helper функции (создать `app/gosqs/tenant_helpers.go`):** - -```go -// tenantQueueKey — внутренний ключ очереди в SyncQueues -func tenantQueueKey(tenantAccessKey, queueName string) string { - return tenantAccessKey + ":" + queueName -} - -// getTenantFromContext — извлекает тенанта из request context -func getTenantFromContext(r *http.Request) *tenant.Tenant { - t, _ := r.Context().Value(auth.TenantContextKey).(*tenant.Tenant) - return t -} - -// tenantQueueUrl — формирует URL очереди для тенанта -func tenantQueueUrl(t *tenant.Tenant, queueName string) string { - return "http://" + models.CurrentEnvironment.Host + ":" + - models.CurrentEnvironment.Port + "/" + t.ID + "/" + queueName -} - -// tenantQueueArn — формирует ARN очереди -func tenantQueueArn(t *tenant.Tenant, queueName string) string { - return "arn:aws:sqs:" + models.CurrentEnvironment.Region + ":" + t.ID + ":" + queueName -} -``` - -**4.3 Изменения в КАЖДОМ handler (подробно)** - -**create_queue.go — CreateQueueV1:** -``` -БЫЛО: - queueName := requestBody.QueueName - key := queueName - url := http://host:port/accountID/queueName - -СТАЛО: - t := getTenantFromContext(req) - queueName := requestBody.QueueName - key := tenantQueueKey(t.AccessKey, queueName) - url := tenantQueueUrl(t, queueName) - arn := tenantQueueArn(t, queueName) - // Проверка лимита очередей: - if t.MaxQueues > 0 { - count := countTenantQueues(t.AccessKey) - if count >= t.MaxQueues { - return error "LimitExceeded" - } - } - models.SyncQueues.Queues[key] = queue -``` - -**send_message.go — SendMessageV1:** -``` -БЫЛО: - queueName = lastSegmentOfUrl(queueUrl) - _, ok := models.SyncQueues.Queues[queueName] - -СТАЛО: - t := getTenantFromContext(req) - queueName = lastSegmentOfUrl(queueUrl) - key := tenantQueueKey(t.AccessKey, queueName) - _, ok := models.SyncQueues.Queues[key] -``` - -**receive_message.go — ReceiveMessageV1:** — аналогично send_message - -**delete_message.go — DeleteMessageV1:** — аналогично - -**delete_message_batch.go — DeleteMessageBatchV1:** — аналогично - -**delete_queue.go — DeleteQueueV1:** -``` -СТАЛО: - t := getTenantFromContext(req) - key := tenantQueueKey(t.AccessKey, queueName) - delete(models.SyncQueues.Queues, key) -``` - -**list_queues.go — ListQueuesV1:** -``` -БЫЛО: - for _, queue := range models.SyncQueues.Queues { - urls = append(urls, queue.URL) - } - -СТАЛО: - t := getTenantFromContext(req) - prefix := t.AccessKey + ":" - for key, queue := range models.SyncQueues.Queues { - if strings.HasPrefix(key, prefix) { - urls = append(urls, queue.URL) - } - } -``` - -**get_queue_url.go — GetQueueUrlV1:** — добавить tenant prefix к lookup - -**get_queue_attributes.go — GetQueueAttributesV1:** — добавить tenant prefix - -**set_queue_attributes.go — SetQueueAttributesV1:** — добавить tenant prefix - -**purge_queue.go — PurgeQueueV1:** — добавить tenant prefix - -**change_message_visibility.go — ChangeMessageVisibilityV1:** — добавить tenant prefix - -**send_message_batch.go — SendMessageBatchV1:** — добавить tenant prefix - -**gosqs.go — PeriodicTasks:** — НЕ ТРОГАТЬ. Он итерирует ВСЕ очереди по полному ключу, не по имени. Работает корректно. - -**ЛОВУШКА #6:** `SendMessageV1` извлекает queueName из QueueUrl через `strings.Split(url, "/")` и берёт ПОСЛЕДНИЙ сегмент. Если URL = `http://host:port/tenantID/myqueue`, последний сегмент = `myqueue` — это ПРАВИЛЬНО, не ломается. - -**ЛОВУШКА #7:** `getQueueFromPath()` в gosqs.go тоже парсит URL. Убедиться что при `/{tenantID}/{queueName}` парсинг берёт queueName, а не tenantID. - -**ЛОВУШКА #8:** FIFO очереди имеют имена вида `myqueue.fifo`. Ключ будет `SSAK-xxx:myqueue.fifo` — это OK, `.fifo` стоит в конце имени, не ключа. Проверить что `utils.HasFIFOQueueName()` получает `queueName`, а не `key`. - -**ЛОВУШКА #9:** RedrivePolicy содержит ARN target очереди. При парсинге ARN в DLQ setup — извлечь имя очереди из ARN, затем добавить tenant prefix для lookup. ОБА (основная и DLQ) должны принадлежать одному тенанту. - -**ЛОВУШКА #10:** `QueueUrl` в ответах CreateQueue и GetQueueUrl используется AWS SDK для всех последующих вызовов. Если формат URL неправильный — SDK не сможет Send/Receive. URL ОБЯЗАН содержать tenantID в пути: `http://host:port/{tenantID}/{queueName}`. - -**4.4 Изменения в роутере** - -В `router.go` — маршрут `/{account}/{queueName}` уже существует. `{account}` = наш `{tenantID}`. Но нужно добавить middleware: - -```go -func New(tenantStore *tenant.TenantStore) http.Handler { - r := mux.NewRouter() - r.HandleFunc("/health", health).Methods("GET") - - // Admin API — отдельная auth (bearer token) - admin := r.PathPrefix("/admin").Subrouter() - // ... admin routes (см. Этап 5) - - // SQS API — tenant auth - sqsRouter := r.PathPrefix("/").Subrouter() - sqsRouter.Use(auth.AuthMiddleware(tenantStore)) - sqsRouter.HandleFunc("/", actionHandler).Methods("GET", "POST") - sqsRouter.HandleFunc("/{account}", actionHandler).Methods("GET", "POST") - sqsRouter.HandleFunc("/queue/{queueName}", actionHandler).Methods("GET", "POST") - sqsRouter.HandleFunc("/{account}/{queueName}", actionHandler).Methods("GET", "POST") - - return r -} -``` - -**Тест прохождения этапа 4:** -```bash -# Создать тенанта через Admin API -curl -X POST http://localhost:4100/admin/tenants -H "Authorization: Bearer $ADMIN_TOKEN" \ - -d name:test-tenant -# → возвращает access_key, secret_key - -# Создать очередь как тенант -aws --endpoint-url http://localhost:4100 sqs create-queue --queue-name test-q -# Настроить AWS credentials = access_key/secret_key из шага выше -# → возвращает QueueUrl с tenantID - -# Отправить и получить сообщение -aws --endpoint-url http://localhost:4100 sqs send-message --queue-url $QUEUE_URL --message-body "hello" -aws --endpoint-url http://localhost:4100 sqs receive-message --queue-url $QUEUE_URL -# → сообщение получено - -# Второй тенант НЕ видит очереди первого -# Со вторым access_key: -aws --endpoint-url http://localhost:4100 sqs list-queues -# → пустой список -``` - ---- - -### Этап 5: Admin API (30 мин) - -**Создать файл `app/admin/admin.go`:** - -Эндпоинты: -``` -POST /admin/tenants — создать тенанта -GET /admin/tenants — список тенантов -GET /admin/tenants/{id} — детали тенанта -DELETE /admin/tenants/{id} — удалить тенанта (и ВСЕ его очереди!) -GET /admin/health — детальный health (кол-во тенантов, очередей, сообщений) -``` - -**Auth для Admin API:** Header `Authorization: Bearer {admin_token}`. Admin token задаётся через: -- Переменная окружения `SHARED_SQS_ADMIN_TOKEN` -- Или в конфиге YAML - -**ЛОВУШКА #11:** При DELETE тенанта — ОБЯЗАТЕЛЬНО удалить ВСЕ его очереди из SyncQueues. Иначе memory leak. Итерировать SyncQueues.Queues, удалить все ключи с prefix `{accessKey}:`. - -**ЛОВУШКА #12:** Admin API НЕ должен быть доступен через тот же ingress что SQS API. Либо другой path prefix, либо другой порт. Рекомендация: path prefix `/admin/`, защищённый bearer token. НЕ ЗАБЫТЬ в auth middleware пропускать `/admin/` пути. - -**Request/Response форматы:** - -POST /admin/tenants: -```json -Request: {"name": "customer-001", "max_queues": 50} -Response: {"id": "t-a1b2c3", "name": "customer-001", "access_key": "SSAK-...", "secret_key": "...", "max_queues": 50} -``` -Важно: secret_key показывается ТОЛЬКО при создании. В List/Get — не включать. - ---- - -### Этап 6: Entry Point + Configuration (20 мин) - -**Модифицировать `app/cmd/goaws.go` (переименовать в `app/cmd/main.go`):** - -```go -func main() { - // Флаги - var configFile string - var adminToken string - var port string - flag.StringVar(&configFile, "config", "", "config file") - flag.StringVar(&adminToken, "admin-token", "", "admin API token") - flag.StringVar(&port, "port", "4100", "listen port") - flag.Parse() - - // Admin token: flag > env > config - if adminToken == "" { - adminToken = os.Getenv("SHARED_SQS_ADMIN_TOKEN") - } - if adminToken == "" { - log.Fatal("admin token required: use --admin-token or SHARED_SQS_ADMIN_TOKEN env") - } - - // Инициализация - tenantStore := tenant.NewTenantStore() - - // Загрузка конфига (если указан) — может содержать pre-created тенантов - if configFile != "" { - conf.LoadConfig(configFile, tenantStore) - } - - // Роутер - r := router.New(tenantStore, adminToken) - - // Periodic tasks - quit := make(chan bool) - go gosqs.PeriodicTasks(1*time.Second, quit) - - // Graceful shutdown - // ... (signal handling, quit channel) - - log.Infof("shared-sqs listening on 0.0.0.0:%s", port) - log.Fatal(http.ListenAndServe("0.0.0.0:"+port, r)) -} -``` - -**ЛОВУШКА #13:** GoAWS не обрабатывает сигналы. ДОБАВИТЬ graceful shutdown (SIGTERM/SIGINT → закрыть quit channel → остановить PeriodicTasks → дождаться завершения). - ---- - -### Этап 7: Dockerfile + Kubernetes (30 мин) - -**Dockerfile:** -```dockerfile -FROM golang:1.22-alpine AS builder -WORKDIR /build -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 go build -o shared-sqs app/cmd/main.go - -FROM alpine:3.19 -RUN apk --no-cache add ca-certificates -COPY --from=builder /build/shared-sqs /usr/local/bin/shared-sqs -EXPOSE 4100 -ENTRYPOINT ["shared-sqs"] -``` - -**Kubernetes manifests (`deployments/k8s/`):** - -deployment.yaml: -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: shared-sqs - namespace: shared-sqs -spec: - replicas: 1 - strategy: - type: Recreate # НЕ RollingUpdate! Урок из ERR-SQS-06. - selector: - matchLabels: - app: shared-sqs - template: - spec: - containers: - - name: shared-sqs - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/shared-sqs:v0.1.0 - ports: - - containerPort: 4100 - env: - - name: SHARED_SQS_ADMIN_TOKEN - valueFrom: - secretKeyRef: - name: shared-sqs-admin - key: token - resources: - requests: - memory: "64Mi" - cpu: "50m" - limits: - memory: "256Mi" - cpu: "500m" - livenessProbe: - httpGet: - path: /health - port: 4100 - readinessProbe: - httpGet: - path: /health - port: 4100 -``` - -**ЛОВУШКА #14:** strategy: Recreate, НЕ RollingUpdate. In-memory state не шарится между подами. При RollingUpdate новый pod стартует с пустым state, а старый ещё жив = split brain. - -service.yaml, ingress.yaml — стандартные. - -**Реестр:** `pearlharbor.registryk8s.services.ngcloud.ru/naeel/shared-sqs` - ---- - -### Этап 8: Makefile (10 мин) - -```makefile -IMAGE_REPO=pearlharbor.registryk8s.services.ngcloud.ru/naeel/shared-sqs -VERSION=v0.1.0 - -build: -CGO_ENABLED=0 go build -o shared-sqs app/cmd/main.go - -docker-build: -docker build -t $(IMAGE_REPO):$(VERSION) . - -docker-push: -docker push $(IMAGE_REPO):$(VERSION) - -test: -go test ./... - -run: -./shared-sqs --admin-token=dev-token-123 --port=4100 -debug - -clean: -rm -f shared-sqs -``` - ---- - -### Этап 9: Тесты (1 час) - -**Создать `tests/shared_sqs_test.sh`** — bash тесты (как в customer-sqs): - -1. Admin API: - - Создать тенанта - - Список тенантов - - Удалить тенанта - - Попытка без admin token → 401 - -2. Изоляция: - - Тенант A создаёт очередь "q1" - - Тенант B создаёт очередь "q1" (ДОЛЖНА создаться — разные тенанты!) - - Тенант A видит только свою q1 - - Тенант B видит только свою q1 - - Тенант A отправляет сообщение → Тенант B НЕ получает его - -3. CRUD: стандартный Create → Send → Receive → Delete flow - -4. Лимиты: тенант с max_queues=2 не может создать 3-ю очередь - ---- - -## 4. ИТОГОВАЯ СТРУКТУРА ПРОЕКТА - -``` -shared-sqs/ -├── app/ -│ ├── cmd/ -│ │ └── main.go # Entry point -│ ├── admin/ -│ │ └── admin_handlers.go # Admin API handlers -│ ├── auth/ -│ │ └── auth_middleware.go # Auth middleware -│ ├── tenant/ -│ │ └── tenant_store.go # Tenant model + in-memory store -│ ├── gosqs/ -│ │ ├── create_queue.go # Modified: tenant-scoped -│ │ ├── send_message.go # Modified: tenant-scoped -│ │ ├── receive_message.go # Modified: tenant-scoped -│ │ ├── delete_message.go # Modified: tenant-scoped -│ │ ├── ... (все handlers) -│ │ ├── tenant_helpers.go # NEW: tenant key/url/arn helpers -│ │ └── gosqs.go # PeriodicTasks (без изменений) -│ ├── models/ -│ │ ├── globals.go # Без SyncTopics -│ │ ├── models.go # Без SNS structs -│ │ ├── configuration.go # Упрощённый -│ │ ├── requests.go # Только SQS requests -│ │ └── responses.go # Только SQS responses -│ ├── router/ -│ │ └── router.go # С middleware и admin routes -│ ├── interfaces/ -│ └── utils/ -├── deployments/ -│ └── k8s/ -│ ├── deployment.yaml -│ ├── service.yaml -│ └── ingress.yaml -├── tests/ -│ └── shared_sqs_test.sh -├── doc/ -│ └── PLAN.md # Этот файл -├── Dockerfile -├── Makefile -├── go.mod -├── go.sum -├── .gitignore -└── README.md -``` - ---- - -## 5. ВСЕ ЛОВУШКИ (СВОДКА) - -| # | Ловушка | Где | Последствие если пропустить | -|---|---------|-----|---------------------------| -| 1 | `crypto/rand` vs `math/rand` для ключей | tenant.go | Предсказуемые ключи = уязвимость | -| 2 | Delete tenant: удалить из ОБОИХ индексов (byID + byAccessKey) | tenant.go | Orphaned данные | -| 3 | AWS CLI отправляет Signature V4 — нужно парсить | middleware.go | SDK не работает | -| 4 | Long polling до 20 сек — не ставить короткий таймаут | middleware.go | ReceiveMessage обрывается | -| 5 | X-Amz-Security-Token — игнорировать | middleware.go | Ложная ошибка auth | -| 6 | URL parsing: последний сегмент = queueName | send_message.go | Берёт tenantID вместо queueName | -| 7 | getQueueFromPath() парсит URL — проверить с новым форматом | gosqs.go | Неправильное имя очереди | -| 8 | FIFO: HasFIFOQueueName() должен получить queueName, не key | create_queue.go | FIFO не работает | -| 9 | DLQ ARN → queue name → tenant prefix | set_queue_attributes.go | Чужая DLQ или not found | -| 10 | QueueUrl в ответах ОБЯЗАН содержать tenantID | create_queue.go, get_queue_url.go | SDK не может send/receive | -| 11 | DELETE tenant → удалить ВСЕ очереди | admin.go | Memory leak | -| 12 | Admin API под отдельной auth (bearer token) | router.go | Тенант = admin | -| 13 | Graceful shutdown (SIGTERM) | main.go | Потеря данных при restart | -| 14 | Deployment strategy: Recreate, НЕ RollingUpdate | deployment.yaml | Split brain | -| 15 | .gitignore: бинарник shared-sqs | .gitignore | Бинарник в git | -| 16 | go mod tidy после чистки SNS | go.mod | Лишние зависимости | -| 17 | Все import paths: goaws → shared-sqs | *.go | Не компилируется | - ---- - -## 6. ПОРЯДОК ВЫПОЛНЕНИЯ - -1. Этап 1 → go build → /health работает -2. Этап 2 → tenant store готов (можно юнит-тестом проверить) -3. Этап 3 → middleware готов -4. Этап 4 → САМЫЙ БОЛЬШОЙ. Делать handler за handler, каждый раз проверяя go build -5. Этап 5 → Admin API -6. Этап 6 → Wiring всего вместе в main.go -7. Этап 7 → Docker + K8s -8. Этап 8 → Makefile -9. Этап 9 → Тесты - -**После каждого этапа: `go build` должен проходить. НЕ НАКАПЛИВАТЬ ошибки компиляции.** - ---- - -## 7. ЧЕГО НЕ ДЕЛАЕМ (scope out) - -- ❌ Persistence (восстановление после рестарта) — in-memory OK для MVP -- ❌ AWS Signature V4 verification — только извлечение AccessKeyId -- ❌ SNS — удаляем полностью -- ❌ HTTPS — TLS на ingress, не в приложении -- ❌ Rate limiting — можно добавить позже -- ❌ Metrics/Prometheus — можно добавить позже -- ❌ UI — нет UI, только API -- ❌ Scale-to-zero — один pod всегда работает -ENDOFPLAN cat > ~/terra/sless/shared-sqs/PLAN.md << 'ENDOFPLAN' -# shared-sqs — План реализации - -**Дата:** 2026-04-09 -**Исполнитель:** Claude Sonnet (или другой агент) -**Подготовил:** Claude Opus 4 (анализ GoAWS, архитектура, ловушки) - ---- - -## 1. ЧТО ЭТО - -Multi-tenant SQS-совместимый сервис на базе форка [GoAWS](https://github.com/Admiral-Piett/goaws) (Go, MIT, 835 stars). - -**Отличие от sqs-operator:** sqs-operator деплоит каждому тенанту ОТДЕЛЬНЫЙ pod с ElasticMQ (~300MB RAM каждый). shared-sqs — ОДИН pod обслуживает ВСЕХ тенантов (~50MB RAM base). - -**Что shared-sqs делает:** -- SQS-совместимый API (CreateQueue, SendMessage, ReceiveMessage, DeleteMessage и т.д.) -- Аутентификация по AccessKeyId (из AWS Authorization header) -- Изоляция очередей между тенантами (тенант видит ТОЛЬКО свои очереди) -- Admin API для управления тенантами (CRUD) -- Работает с AWS CLI и AWS SDK без модификаций - ---- - -## 2. АРХИТЕКТУРА GoAWS (то, что форкаем) - -### 2.1 Структура исходников -``` -app/ -├── cmd/goaws.go # Entry point (~40 LOC): флаги, загрузка конфига, HTTP сервер -├── conf/ # Загрузка YAML конфига -├── gosqs/ # SQS handlers (ЯДРО — ~20 файлов) -│ ├── create_queue.go # CreateQueueV1() -│ ├── send_message.go # SendMessageV1() -│ ├── receive_message.go # ReceiveMessageV1() -│ ├── delete_message.go # DeleteMessageV1() -│ ├── delete_message_batch.go -│ ├── delete_queue.go -│ ├── get_queue_attributes.go -│ ├── get_queue_url.go -│ ├── list_queues.go -│ ├── purge_queue.go -│ ├── send_message_batch.go -│ ├── set_queue_attributes.go -│ ├── change_message_visibility.go -│ ├── queue_attributes.go # Helpers для атрибутов -│ └── gosqs.go # PeriodicTasks (visibility timeout, DLQ, dedup) -├── gosns/ # SNS handlers — НЕ НУЖНЫ, УДАЛИТЬ -├── models/ -│ ├── globals.go # SyncQueues, SyncTopics — глобальные map + RWMutex -│ ├── models.go # Queue, SqsMessage, Topic structs -│ ├── configuration.go # Environment, EnvQueue, config structs -│ ├── constants.go -│ ├── conversions.go # Парсинг тел запросов -│ ├── errors.go # AWS-совместимые ошибки -│ ├── helpers.go -│ ├── requests.go # Request structs (CreateQueueRequest, SendMessageRequest и т.д.) -│ └── responses.go # Response structs (XML + JSON) -├── router/ -│ └── router.go # gorilla/mux, actionHandler, routingTableV1 -├── interfaces/ # AbstractResponseBody interface -├── utils/ # Hash, MD5, REQUEST_TRANSFORMER -├── mocks/ # Тестовые моки -├── fixtures/ # Тестовые данные -├── servertest/ -└── test/ -``` - -### 2.2 Критические архитектурные точки - -**Глобальный state** (`models/globals.go`): -```go -var SyncQueues = struct { - sync.RWMutex - Queues map[string]*Queue -}{Queues: make(map[string]*Queue)} -``` -Все очереди храняться В ОДНОМ map. Ключ = имя очереди (string). - -**Роутинг** (`router/router.go`): -```go -r.HandleFunc("/", actionHandler) -r.HandleFunc("/{account}", actionHandler) -r.HandleFunc("/queue/{queueName}", actionHandler) -r.HandleFunc("/{account}/{queueName}", actionHandler) -``` -Все запросы идут в `actionHandler`, который извлекает `Action` из: -- Query param `Action=CreateQueue` (AWS Query Protocol) -- Header `X-Amz-Target: AmazonSQS.CreateQueue` (AWS JSON Protocol) - -**Dispatch table** (`router/router.go`): -```go -var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - "CreateQueue": sqs.CreateQueueV1, - "SendMessage": sqs.SendMessageV1, - "ReceiveMessage": sqs.ReceiveMessageV1, - // ... все SQS + SNS actions -} -``` - -**URL конструкция** (в create_queue.go): -```go -queueUrl := "http://" + host + ":" + port + "/" + accountID + "/" + queueName -queueArn := "arn:aws:sqs:" + region + ":" + accountID + ":" + queueName -``` -`accountID` берётся из `models.CurrentEnvironment.AccountID` — ГЛОБАЛЬНАЯ переменная (одна на всех). - -**Зависимости** (go.mod): -- `gorilla/mux v1.8.0` — роутер -- `gorilla/schema v1.4.1` — form decoder -- `google/uuid v1.6.0` — UUID генерация -- `sirupsen/logrus` — логирование -- `ghodss/yaml` — YAML парсинг -- `aws/aws-sdk-go v1.47.3` — только для тестов - ---- - -## 3. ПЛАН ИЗМЕНЕНИЙ - -### 3.0 Общие правила работы - -**КРИТИЧНО — все команды ТОЛЬКО через SSH:** -``` -ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 КОМАНДА -``` - -**Go файлы редактировать ТОЛЬКО через Python patch скрипты на VM**, НЕ через sshfs/VS Code. - -**Markdown/yaml/conf файлы** можно создавать через `cat > file << EOF` по SSH. - -**Перед go build** — убедиться что бинарник в `.gitignore`. - -### Этап 1: Клон GoAWS + чистка (1 час) - -**Действия:** - -1. Клонировать GoAWS в `shared-sqs/`: -```bash -cd ~/terra/sless/shared-sqs -git clone https://github.com/Admiral-Piett/goaws.git _upstream -# Копируем ТОЛЬКО нужное: -cp -r _upstream/app ./app -cp _upstream/go.mod ./go.mod -cp _upstream/go.sum ./go.sum -cp _upstream/Dockerfile ./Dockerfile -rm -rf _upstream -``` - -2. Поменять module name в go.mod: -``` -module shared-sqs -go 1.22 -``` -(Повысить версию Go с 1.18 до 1.22+) - -3. Обновить все import paths: - - Заменить `github.com/Admiral-Piett/goaws/app/` → `shared-sqs/app/` - - Это во ВСЕХ .go файлах - -4. УДАЛИТЬ всё связанное с SNS: - - `app/gosns/` — целиком - - Из `router/router.go` — убрать все SNS записи из `routingTableV1` - - Из `models/globals.go` — убрать `SyncTopics` - - Из `models/models.go` — убрать `Topic`, `Subscription`, `SNSMessage`, `FilterPolicy` - - Из `models/configuration.go` — убрать `EnvTopic`, `EnvSubsciption` - - Из `models/requests.go` и `responses.go` — убрать SNS-related structs - -5. УДАЛИТЬ тестовые/mock директории (мы напишем свои тесты): - - `app/mocks/` - - `app/fixtures/` - - `app/servertest/` - - `app/test/` - - `app/smoke_tests/` (если скопировалась) - -6. Проверить что компилируется: -```bash -cd ~/terra/sless/shared-sqs -go mod tidy -go build -o shared-sqs app/cmd/goaws.go -``` - -7. Проверить что стартует: -```bash -./shared-sqs -debug -# В другом окне: curl http://localhost:4100/health -# Ожидание: 200 OK -``` - -**Тест прохождения этапа:** `go build` успешен, `/health` возвращает 200. - ---- - -### Этап 2: Tenant Model + хранилище (30 мин) - -**Создать файл `app/tenant/tenant.go`:** - -```go -package tenant - -import ( - "crypto/rand" - "encoding/hex" - "sync" - "time" -) - -// Tenant — модель тенанта shared-sqs -type Tenant struct { - ID string // уникальный идентификатор (генерируется) - Name string // имя тенанта (для отображения) - AccessKey string // аналог AWS AccessKeyId - SecretKey string // аналог AWS SecretAccessKey - MaxQueues int // лимит очередей (0 = безлимит) - CreatedAt time.Time - Active bool -} - -// TenantStore — in-memory хранилище тенантов -type TenantStore struct { - sync.RWMutex - // byID — поиск по ID - byID map[string]*Tenant - // byAccessKey — поиск по AccessKey (для auth) - byAccessKey map[string]*Tenant -} - -// NewTenantStore — создаёт пустое хранилище -func NewTenantStore() *TenantStore { ... } - -// Create — создаёт нового тенанта, генерирует ключи -func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) { ... } - -// GetByAccessKey — поиск тенанта по AccessKeyId (для auth middleware) -func (s *TenantStore) GetByAccessKey(accessKey string) (*Tenant, bool) { ... } - -// GetByID — поиск по ID -func (s *TenantStore) GetByID(id string) (*Tenant, bool) { ... } - -// Delete — удаление тенанта -func (s *TenantStore) Delete(id string) bool { ... } - -// List — список всех тенантов -func (s *TenantStore) List() []*Tenant { ... } -``` - -**Генерация ключей (БЕЗОПАСНАЯ):** -```go -func generateAccessKey() string { - // Формат: SSAK-{random hex 12} (SS = Shared SQS) - b := make([]byte, 12) - rand.Read(b) - return "SSAK-" + hex.EncodeToString(b) -} - -func generateSecretKey() string { - // 32 байта random → 64 hex символа - b := make([]byte, 32) - rand.Read(b) - return hex.EncodeToString(b) -} -``` - -**ЛОВУШКА #1:** Используй `crypto/rand`, НЕ `math/rand` для ключей. `math/rand` предсказуем. - -**ЛОВУШКА #2:** Два индекса (byID и byAccessKey) — при Delete надо удалить из ОБОИХ. - -**Тест прохождения:** TenantStore создаёт/ищет/удаляет тенантов. Ключи уникальны. - ---- - -### Этап 3: Auth Middleware (30 мин) - -**Создать файл `app/auth/middleware.go`:** - -```go -package auth - -import ( - "context" - "net/http" - "strings" -) - -type contextKey string -const TenantContextKey contextKey = "tenant" - -// AuthMiddleware — извлекает AccessKeyId из AWS Authorization header -// и находит тенанта в store -func AuthMiddleware(store *tenant.TenantStore) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // /health и /admin/** — пропускаем (разная auth) - if r.URL.Path == "/health" { - next.ServeHTTP(w, r) - return - } - if strings.HasPrefix(r.URL.Path, "/admin/") { - next.ServeHTTP(w, r) - return - } - - accessKeyId := extractAccessKeyId(r) - if accessKeyId == "" { - writeAuthError(w, "MissingAuthenticationToken") - return - } - - t, ok := store.GetByAccessKey(accessKeyId) - if !ok || !t.Active { - writeAuthError(w, "InvalidClientTokenId") - return - } - - ctx := context.WithValue(r.Context(), TenantContextKey, t) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} -``` - -**Извлечение AccessKeyId из Authorization header:** -```go -// extractAccessKeyId — извлекает AWS AccessKeyId из запроса -// Формат header: "AWS4-HMAC-SHA256 Credential={AccessKeyId}/{date}/{region}/sqs/aws4_request, ..." -func extractAccessKeyId(r *http.Request) string { - // Вариант 1: Authorization header (AWS Signature V4) - auth := r.Header.Get("Authorization") - if strings.HasPrefix(auth, "AWS4-HMAC-SHA256") { - // Ищем "Credential=" и берём всё до первого "/" - idx := strings.Index(auth, "Credential=") - if idx >= 0 { - rest := auth[idx+len("Credential="):] - slashIdx := strings.Index(rest, "/") - if slashIdx > 0 { - return rest[:slashIdx] - } - } - } - - // Вариант 2: Query parameter (presigned URLs) - if cred := r.URL.Query().Get("X-Amz-Credential"); cred != "" { - parts := strings.SplitN(cred, "/", 2) - if len(parts) > 0 { - return parts[0] - } - } - - return "" -} -``` - -**ЛОВУШКА #3:** AWS CLI ВСЕГДА отправляет Authorization header с Signature V4. Наш middleware должен УМЕТЬ парсить его, даже если мы НЕ ПРОВЕРЯЕМ подпись. - -**ЛОВУШКА #4:** Когда AWS SDK делает `ReceiveMessage` с long polling (WaitTimeSeconds > 0), соединение держится до 20 секунд. Auth middleware не должен ставить таймаут короче. - -**ЛОВУШКА #5:** AWS SDK может отправлять запросы с `X-Amz-Security-Token` (STS). Игнорировать этот header. - -**Тест прохождения:** Запрос с `Authorization: AWS4-HMAC-SHA256 Credential=SSAK-abc123.../...` → тенант найден в контексте. Запрос без header → 403. - ---- - -### Этап 4: Queue Isolation — САМЫЙ КРИТИЧНЫЙ (2 часа) - -Это ЯДРО всех изменений. Все SQS handlers должны работать с tenant-scoped очередями. - -**4.1 Изменение ключей в SyncQueues** - -Сейчас: `SyncQueues.Queues["my-queue"]` -Станет: `SyncQueues.Queues["SSAK-abc123:my-queue"]` - -Формат внутреннего ключа: `{tenantAccessKey}:{queueName}` - -Почему AccessKey а не TenantID: AccessKey уже есть в auth context, не надо лишний lookup. AccessKey уникален. - -**4.2 Helper функции (создать `app/gosqs/tenant_helpers.go`):** - -```go -// tenantQueueKey — внутренний ключ очереди в SyncQueues -func tenantQueueKey(tenantAccessKey, queueName string) string { - return tenantAccessKey + ":" + queueName -} - -// getTenantFromContext — извлекает тенанта из request context -func getTenantFromContext(r *http.Request) *tenant.Tenant { - t, _ := r.Context().Value(auth.TenantContextKey).(*tenant.Tenant) - return t -} - -// tenantQueueUrl — формирует URL очереди для тенанта -func tenantQueueUrl(t *tenant.Tenant, queueName string) string { - return "http://" + models.CurrentEnvironment.Host + ":" + - models.CurrentEnvironment.Port + "/" + t.ID + "/" + queueName -} - -// tenantQueueArn — формирует ARN очереди -func tenantQueueArn(t *tenant.Tenant, queueName string) string { - return "arn:aws:sqs:" + models.CurrentEnvironment.Region + ":" + t.ID + ":" + queueName -} -``` - -**4.3 Изменения в КАЖДОМ handler (подробно)** - -**create_queue.go — CreateQueueV1:** -``` -БЫЛО: - queueName := requestBody.QueueName - key := queueName - url := http://host:port/accountID/queueName - -СТАЛО: - t := getTenantFromContext(req) - queueName := requestBody.QueueName - key := tenantQueueKey(t.AccessKey, queueName) - url := tenantQueueUrl(t, queueName) - arn := tenantQueueArn(t, queueName) - // Проверка лимита очередей: - if t.MaxQueues > 0 { - count := countTenantQueues(t.AccessKey) - if count >= t.MaxQueues { - return error "LimitExceeded" - } - } - models.SyncQueues.Queues[key] = queue -``` - -**send_message.go — SendMessageV1:** -``` -БЫЛО: - queueName = lastSegmentOfUrl(queueUrl) - _, ok := models.SyncQueues.Queues[queueName] - -СТАЛО: - t := getTenantFromContext(req) - queueName = lastSegmentOfUrl(queueUrl) - key := tenantQueueKey(t.AccessKey, queueName) - _, ok := models.SyncQueues.Queues[key] -``` - -**receive_message.go — ReceiveMessageV1:** — аналогично send_message - -**delete_message.go — DeleteMessageV1:** — аналогично - -**delete_message_batch.go — DeleteMessageBatchV1:** — аналогично - -**delete_queue.go — DeleteQueueV1:** -``` -СТАЛО: - t := getTenantFromContext(req) - key := tenantQueueKey(t.AccessKey, queueName) - delete(models.SyncQueues.Queues, key) -``` - -**list_queues.go — ListQueuesV1:** -``` -БЫЛО: - for _, queue := range models.SyncQueues.Queues { - urls = append(urls, queue.URL) - } - -СТАЛО: - t := getTenantFromContext(req) - prefix := t.AccessKey + ":" - for key, queue := range models.SyncQueues.Queues { - if strings.HasPrefix(key, prefix) { - urls = append(urls, queue.URL) - } - } -``` - -**get_queue_url.go — GetQueueUrlV1:** — добавить tenant prefix к lookup - -**get_queue_attributes.go — GetQueueAttributesV1:** — добавить tenant prefix - -**set_queue_attributes.go — SetQueueAttributesV1:** — добавить tenant prefix - -**purge_queue.go — PurgeQueueV1:** — добавить tenant prefix - -**change_message_visibility.go — ChangeMessageVisibilityV1:** — добавить tenant prefix - -**send_message_batch.go — SendMessageBatchV1:** — добавить tenant prefix - -**gosqs.go — PeriodicTasks:** — НЕ ТРОГАТЬ. Он итерирует ВСЕ очереди по полному ключу, не по имени. Работает корректно. - -**ЛОВУШКА #6:** `SendMessageV1` извлекает queueName из QueueUrl через `strings.Split(url, "/")` и берёт ПОСЛЕДНИЙ сегмент. Если URL = `http://host:port/tenantID/myqueue`, последний сегмент = `myqueue` — это ПРАВИЛЬНО, не ломается. - -**ЛОВУШКА #7:** `getQueueFromPath()` в gosqs.go тоже парсит URL. Убедиться что при `/{tenantID}/{queueName}` парсинг берёт queueName, а не tenantID. - -**ЛОВУШКА #8:** FIFO очереди имеют имена вида `myqueue.fifo`. Ключ будет `SSAK-xxx:myqueue.fifo` — это OK, `.fifo` стоит в конце имени, не ключа. Проверить что `utils.HasFIFOQueueName()` получает `queueName`, а не `key`. - -**ЛОВУШКА #9:** RedrivePolicy содержит ARN target очереди. При парсинге ARN в DLQ setup — извлечь имя очереди из ARN, затем добавить tenant prefix для lookup. ОБА (основная и DLQ) должны принадлежать одному тенанту. - -**ЛОВУШКА #10:** `QueueUrl` в ответах CreateQueue и GetQueueUrl используется AWS SDK для всех последующих вызовов. Если формат URL неправильный — SDK не сможет Send/Receive. URL ОБЯЗАН содержать tenantID в пути: `http://host:port/{tenantID}/{queueName}`. - -**4.4 Изменения в роутере** - -В `router.go` — маршрут `/{account}/{queueName}` уже существует. `{account}` = наш `{tenantID}`. Но нужно добавить middleware: - -```go -func New(tenantStore *tenant.TenantStore) http.Handler { - r := mux.NewRouter() - r.HandleFunc("/health", health).Methods("GET") - - // Admin API — отдельная auth (bearer token) - admin := r.PathPrefix("/admin").Subrouter() - // ... admin routes (см. Этап 5) - - // SQS API — tenant auth - sqsRouter := r.PathPrefix("/").Subrouter() - sqsRouter.Use(auth.AuthMiddleware(tenantStore)) - sqsRouter.HandleFunc("/", actionHandler).Methods("GET", "POST") - sqsRouter.HandleFunc("/{account}", actionHandler).Methods("GET", "POST") - sqsRouter.HandleFunc("/queue/{queueName}", actionHandler).Methods("GET", "POST") - sqsRouter.HandleFunc("/{account}/{queueName}", actionHandler).Methods("GET", "POST") - - return r -} -``` - -**Тест прохождения этапа 4:** -```bash -# Создать тенанта через Admin API -curl -X POST http://localhost:4100/admin/tenants -H "Authorization: Bearer $ADMIN_TOKEN" \ - -d max_queues:10 -# → возвращает access_key, secret_key - -# Создать очередь как тенант -aws --endpoint-url http://localhost:4100 sqs create-queue --queue-name test-q -# Настроить AWS credentials = access_key/secret_key из шага выше -# → возвращает QueueUrl с tenantID - -# Отправить и получить сообщение -aws --endpoint-url http://localhost:4100 sqs send-message --queue-url $QUEUE_URL --message-body "hello" -aws --endpoint-url http://localhost:4100 sqs receive-message --queue-url $QUEUE_URL -# → сообщение получено - -# Второй тенант НЕ видит очереди первого -# Со вторым access_key: -aws --endpoint-url http://localhost:4100 sqs list-queues -# → пустой список -``` - ---- - -### Этап 5: Admin API (30 мин) - -**Создать файл `app/admin/admin.go`:** - -Эндпоинты: -``` -POST /admin/tenants — создать тенанта -GET /admin/tenants — список тенантов -GET /admin/tenants/{id} — детали тенанта -DELETE /admin/tenants/{id} — удалить тенанта (и ВСЕ его очереди!) -GET /admin/health — детальный health (кол-во тенантов, очередей, сообщений) -``` - -**Auth для Admin API:** Header `Authorization: Bearer {admin_token}`. Admin token задаётся через: -- Переменная окружения `SHARED_SQS_ADMIN_TOKEN` -- Или в конфиге YAML - -**ЛОВУШКА #11:** При DELETE тенанта — ОБЯЗАТЕЛЬНО удалить ВСЕ его очереди из SyncQueues. Иначе memory leak. Итерировать SyncQueues.Queues, удалить все ключи с prefix `{accessKey}:`. - -**ЛОВУШКА #12:** Admin API НЕ должен быть доступен через тот же ingress что SQS API. Либо другой path prefix, либо другой порт. Рекомендация: path prefix `/admin/`, защищённый bearer token. НЕ ЗАБЫТЬ в auth middleware пропускать `/admin/` пути. - -**Request/Response форматы:** - -POST /admin/tenants: -```json -Request: {"name": "customer-001", "max_queues": 50} -Response: {"id": "t-a1b2c3", "name": "customer-001", "access_key": "SSAK-...", "secret_key": "...", "max_queues": 50} -``` -Важно: secret_key показывается ТОЛЬКО при создании. В List/Get — не включать. - ---- - -### Этап 6: Entry Point + Configuration (20 мин) - -**Модифицировать `app/cmd/goaws.go` (переименовать в `app/cmd/main.go`):** - -```go -func main() { - // Флаги - var configFile string - var adminToken string - var port string - flag.StringVar(&configFile, "config", "", "config file") - flag.StringVar(&adminToken, "admin-token", "", "admin API token") - flag.StringVar(&port, "port", "4100", "listen port") - flag.Parse() - - // Admin token: flag > env > config - if adminToken == "" { - adminToken = os.Getenv("SHARED_SQS_ADMIN_TOKEN") - } - if adminToken == "" { - log.Fatal("admin token required: use --admin-token or SHARED_SQS_ADMIN_TOKEN env") - } - - // Инициализация - tenantStore := tenant.NewTenantStore() - - // Загрузка конфига (если указан) — может содержать pre-created тенантов - if configFile != "" { - conf.LoadConfig(configFile, tenantStore) - } - - // Роутер - r := router.New(tenantStore, adminToken) - - // Periodic tasks - quit := make(chan bool) - go gosqs.PeriodicTasks(1*time.Second, quit) - - // Graceful shutdown - // ... (signal handling, quit channel) - - log.Infof("shared-sqs listening on 0.0.0.0:%s", port) - log.Fatal(http.ListenAndServe("0.0.0.0:"+port, r)) -} -``` - -**ЛОВУШКА #13:** GoAWS не обрабатывает сигналы. ДОБАВИТЬ graceful shutdown (SIGTERM/SIGINT → закрыть quit channel → остановить PeriodicTasks → дождаться завершения). - ---- - -### Этап 7: Dockerfile + Kubernetes (30 мин) - -**Dockerfile:** -```dockerfile -FROM golang:1.22-alpine AS builder -WORKDIR /build -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 go build -o shared-sqs app/cmd/main.go - -FROM alpine:3.19 -RUN apk --no-cache add ca-certificates -COPY --from=builder /build/shared-sqs /usr/local/bin/shared-sqs -EXPOSE 4100 -ENTRYPOINT ["shared-sqs"] -``` - -**Kubernetes manifests (`deployments/k8s/`):** - -deployment.yaml: -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: shared-sqs - namespace: shared-sqs -spec: - replicas: 1 - strategy: - type: Recreate # НЕ RollingUpdate! Урок из ERR-SQS-06. - selector: - matchLabels: - app: shared-sqs - template: - spec: - containers: - - name: shared-sqs - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/shared-sqs:v0.1.0 - ports: - - containerPort: 4100 - env: - - name: SHARED_SQS_ADMIN_TOKEN - valueFrom: - secretKeyRef: - name: shared-sqs-admin - key: token - resources: - requests: - memory: "64Mi" - cpu: "50m" - limits: - memory: "256Mi" - cpu: "500m" - livenessProbe: - httpGet: - path: /health - port: 4100 - readinessProbe: - httpGet: - path: /health - port: 4100 -``` - -**ЛОВУШКА #14:** strategy: Recreate, НЕ RollingUpdate. In-memory state не шарится между подами. При RollingUpdate новый pod стартует с пустым state, а старый ещё жив = split brain. - -service.yaml, ingress.yaml — стандартные. - -**Реестр:** `pearlharbor.registryk8s.services.ngcloud.ru/naeel/shared-sqs` - ---- - -### Этап 8: Makefile (10 мин) - -```makefile -IMAGE_REPO=pearlharbor.registryk8s.services.ngcloud.ru/naeel/shared-sqs -VERSION=v0.1.0 - -build: -CGO_ENABLED=0 go build -o shared-sqs app/cmd/main.go - -docker-build: -docker build -t $(IMAGE_REPO):$(VERSION) . - -docker-push: -docker push $(IMAGE_REPO):$(VERSION) - -test: -go test ./... - -run: -./shared-sqs --admin-token=dev-token-123 --port=4100 -debug - -clean: -rm -f shared-sqs -``` - ---- - -### Этап 9: Тесты (1 час) - -**Создать `tests/shared_sqs_test.sh`** — bash тесты (как в customer-sqs): - -1. Admin API: - - Создать тенанта - - Список тенантов - - Удалить тенанта - - Попытка без admin token → 401 - -2. Изоляция: - - Тенант A создаёт очередь "q1" - - Тенант B создаёт очередь "q1" (ДОЛЖНА создаться — разные тенанты!) - - Тенант A видит только свою q1 - - Тенант B видит только свою q1 - - Тенант A отправляет сообщение → Тенант B НЕ получает его - -3. CRUD: стандартный Create → Send → Receive → Delete flow - -4. Лимиты: тенант с max_queues=2 не может создать 3-ю очередь - ---- - -## 4. ИТОГОВАЯ СТРУКТУРА ПРОЕКТА - -``` -shared-sqs/ -├── app/ -│ ├── cmd/ -│ │ └── main.go # Entry point -│ ├── admin/ -│ │ └── admin_handlers.go # Admin API handlers -│ ├── auth/ -│ │ └── auth_middleware.go # Auth middleware -│ ├── tenant/ -│ │ └── tenant_store.go # Tenant model + in-memory store -│ ├── gosqs/ -│ │ ├── create_queue.go # Modified: tenant-scoped -│ │ ├── send_message.go # Modified: tenant-scoped -│ │ ├── receive_message.go # Modified: tenant-scoped -│ │ ├── delete_message.go # Modified: tenant-scoped -│ │ ├── ... (все handlers) -│ │ ├── tenant_helpers.go # NEW: tenant key/url/arn helpers -│ │ └── gosqs.go # PeriodicTasks (без изменений) -│ ├── models/ -│ │ ├── globals.go # Без SyncTopics -│ │ ├── models.go # Без SNS structs -│ │ ├── configuration.go # Упрощённый -│ │ ├── requests.go # Только SQS requests -│ │ └── responses.go # Только SQS responses -│ ├── router/ -│ │ └── router.go # С middleware и admin routes -│ ├── interfaces/ -│ └── utils/ -├── deployments/ -│ └── k8s/ -│ ├── deployment.yaml -│ ├── service.yaml -│ └── ingress.yaml -├── tests/ -│ └── shared_sqs_test.sh -├── doc/ -│ └── PLAN.md # Этот файл -├── Dockerfile -├── Makefile -├── go.mod -├── go.sum -├── .gitignore -└── README.md -``` - ---- - -## 5. ВСЕ ЛОВУШКИ (СВОДКА) - -| # | Ловушка | Где | Последствие если пропустить | -|---|---------|-----|---------------------------| -| 1 | `crypto/rand` vs `math/rand` для ключей | tenant.go | Предсказуемые ключи = уязвимость | -| 2 | Delete tenant: удалить из ОБОИХ индексов (byID + byAccessKey) | tenant.go | Orphaned данные | -| 3 | AWS CLI отправляет Signature V4 — нужно парсить | middleware.go | SDK не работает | -| 4 | Long polling до 20 сек — не ставить короткий таймаут | middleware.go | ReceiveMessage обрывается | -| 5 | X-Amz-Security-Token — игнорировать | middleware.go | Ложная ошибка auth | -| 6 | URL parsing: последний сегмент = queueName | send_message.go | Берёт tenantID вместо queueName | -| 7 | getQueueFromPath() парсит URL — проверить с новым форматом | gosqs.go | Неправильное имя очереди | -| 8 | FIFO: HasFIFOQueueName() должен получить queueName, не key | create_queue.go | FIFO не работает | -| 9 | DLQ ARN → queue name → tenant prefix | set_queue_attributes.go | Чужая DLQ или not found | -| 10 | QueueUrl в ответах ОБЯЗАН содержать tenantID | create_queue.go, get_queue_url.go | SDK не может send/receive | -| 11 | DELETE tenant → удалить ВСЕ очереди | admin.go | Memory leak | -| 12 | Admin API под отдельной auth (bearer token) | router.go | Тенант = admin | -| 13 | Graceful shutdown (SIGTERM) | main.go | Потеря данных при restart | -| 14 | Deployment strategy: Recreate, НЕ RollingUpdate | deployment.yaml | Split brain | -| 15 | .gitignore: бинарник shared-sqs | .gitignore | Бинарник в git | -| 16 | go mod tidy после чистки SNS | go.mod | Лишние зависимости | -| 17 | Все import paths: goaws → shared-sqs | *.go | Не компилируется | - ---- - -## 6. ПОРЯДОК ВЫПОЛНЕНИЯ - -1. Этап 1 → go build → /health работает -2. Этап 2 → tenant store готов (можно юнит-тестом проверить) -3. Этап 3 → middleware готов -4. Этап 4 → САМЫЙ БОЛЬШОЙ. Делать handler за handler, каждый раз проверяя go build -5. Этап 5 → Admin API -6. Этап 6 → Wiring всего вместе в main.go -7. Этап 7 → Docker + K8s -8. Этап 8 → Makefile -9. Этап 9 → Тесты - -**После каждого этапа: `go build` должен проходить. НЕ НАКАПЛИВАТЬ ошибки компиляции.** - ---- - -## 7. ЧЕГО НЕ ДЕЛАЕМ (scope out) - -- ❌ Persistence (восстановление после рестарта) — in-memory OK для MVP -- ❌ AWS Signature V4 verification — только извлечение AccessKeyId -- ❌ SNS — удаляем полностью -- ❌ HTTPS — TLS на ingress, не в приложении -- ❌ Rate limiting — можно добавить позже -- ❌ Metrics/Prometheus — можно добавить позже -- ❌ UI — нет UI, только API -- ❌ Scale-to-zero — один pod всегда работает