diff --git a/CONTINUE.md b/CONTINUE.md index dde0d54..af8d47f 100644 --- a/CONTINUE.md +++ b/CONTINUE.md @@ -1,53 +1,41 @@ -# Email Notification Service +# Deployment Fixes — Завершено -## Статус: Готово +## Что было сделано -## Что реализовано +### Корневые проблемы (найдены и исправлены) +1. **ResourceQuota/LimitRange в неправильном namespace** — kustomize перезаписывал namespace с `gooseek-sandbox` на `gooseek`, блокируя создание подов (CPU quota exceeded: 26.3/16 cores) +2. **NetworkPolicy sandbox-isolation в gooseek** — блокировала DNS (порт 53) для всех подов, вызывая CrashLoopBackOff у thread-svc, learning-svc +3. **Ingress не роутил /api/* на api-gateway** — все API запросы шли на webui (Next.js), возвращая "Service unavailable" +4. **CHAT_SVC_URL отсутствовал в configmap** — api-gateway не мог подключиться к chat-svc +5. **Readiness probes /ready для сервисов без этого endpoint** — agent-svc, search-svc, scraper-svc всегда 0/1 +6. **Rate limiter блокировал health/ready probes** — api-gateway падал в CrashLoopBackOff +7. **Secrets содержали ${...} placeholders** — envsubst не применялся при деплое -### Пакет `backend/pkg/email/` -- `types.go` — типы уведомлений (Welcome, PasswordReset, LimitWarning, SpaceInvite, SystemAlert) -- `sender.go` — SMTP клиент с TLS, rate limiting (1 письмо/тип/24ч), async отправка -- `templates.go` — HTML шаблоны с брендингом GooSeek +### Файлы изменены +- `backend/deploy/k8s/opensandbox-sandbox-ns.yaml` — новый (sandbox ресурсы с правильным namespace) +- `backend/deploy/k8s/opensandbox.yaml` — убраны sandbox-scoped ресурсы +- `backend/deploy/k8s/deploy.sh` — cleanup misplaced ресурсов + sandbox namespace apply +- `backend/deploy/k8s/ingress.yaml` — добавлен /api route на api-gateway +- `backend/deploy/k8s/configmap.yaml` — добавлены CHAT_SVC_URL, API_GATEWAY_URL +- `backend/deploy/k8s/agent-svc.yaml` — readinessProbe /ready -> /health +- `backend/deploy/k8s/search-svc.yaml` — readinessProbe /ready -> /health +- `backend/deploy/k8s/scraper-svc.yaml` — readinessProbe /ready -> /health +- `backend/cmd/api-gateway/main.go` — health/ready endpoints до JWT/rate-limit middleware +- `backend/webui/next.config.mjs` — rewrites /api/* -> api-gateway -### Интеграции - -| Сервис | Уведомления | Файл | -|--------|-------------|------| -| auth-svc | Welcome, Password Reset | `backend/cmd/auth-svc/main.go` | -| llm-svc | Limit Warning (80%), Limit Exceeded (100%) | `backend/pkg/middleware/llm_limits.go` | -| thread-svc | Space Invite | `backend/cmd/thread-svc/main.go` | - -### Новые API endpoints -- `POST /api/v1/spaces/:id/invite` — приглашение в Space по email -- `GET /api/v1/spaces/:id/invites` — список приглашений - -### Конфигурация -```env -SMTP_HOST=smtp.example.com -SMTP_PORT=587 -SMTP_USER=noreply@gooseek.ru -SMTP_PASSWORD= -SMTP_FROM=GooSeek -SMTP_TLS=true -SITE_URL=https://gooseek.ru -SITE_NAME=GooSeek -``` - -## Файлы изменены -- `backend/pkg/email/types.go` — новый -- `backend/pkg/email/sender.go` — новый -- `backend/pkg/email/templates.go` — новый -- `backend/pkg/config/config.go` — SMTP конфиг -- `backend/pkg/middleware/jwt.go` — GetUserEmail() -- `backend/pkg/middleware/llm_limits.go` — email при лимитах -- `backend/internal/usage/repository.go` — GetUserEmail() -- `backend/cmd/auth-svc/main.go` — welcome + reset emails -- `backend/cmd/llm-svc/main.go` — emailSender в LLMLimits -- `backend/cmd/thread-svc/main.go` — invite endpoint + email -- `backend/deploy/k8s/configmap.yaml` — SMTP переменные -- `.env` — SMTP переменные +### Ручные действия на сервере (уже выполнены) +- Удалены ResourceQuota, LimitRange, NetworkPolicy из namespace gooseek +- Secrets пересозданы с реальными значениями +- ConfigMap дополнен недостающими TIMEWEB_* переменными +- Redis flush для сброса rate limit ## Сервер - IP: 5.187.77.89 - GPU: RTX 4060 Ti 16GB - Site: https://gooseek.ru +- Chat API: работает (Ollama для free tier) + +## Важно при следующем деплое +- `deploy.sh` использует `envsubst` для configmap — убедиться что `.env` содержит все переменные +- Secrets в `configmap.yaml` используют `${...}` — envsubst должен подставить реальные значения +- После деплоя проверить что secrets не содержат placeholder'ы diff --git a/backend/deploy/k8s/configmap.yaml b/backend/deploy/k8s/configmap.yaml index 42b7a06..7d76d2e 100644 --- a/backend/deploy/k8s/configmap.yaml +++ b/backend/deploy/k8s/configmap.yaml @@ -29,41 +29,24 @@ data: OLLAMA_MODEL: "qwen3.5:9b" OLLAMA_EMBEDDING_MODEL: "qwen3-embedding:0.6b" OLLAMA_NUM_PARALLEL: "2" - DEFAULT_LLM_MODEL: "${DEFAULT_LLM_MODEL}" - DEFAULT_LLM_PROVIDER: "${DEFAULT_LLM_PROVIDER}" - TIMEWEB_API_BASE_URL: "${TIMEWEB_API_BASE_URL}" - TIMEWEB_AGENT_ACCESS_ID: "${TIMEWEB_AGENT_ACCESS_ID}" - TRAVELPAYOUTS_TOKEN: "${TRAVELPAYOUTS_TOKEN}" - NEXT_PUBLIC_ENABLED_ROUTES: "${NEXT_PUBLIC_ENABLED_ROUTES}" - NEXT_PUBLIC_TWOGIS_API_KEY: "${NEXT_PUBLIC_TWOGIS_API_KEY}" - S3_ENDPOINT: "${S3_ENDPOINT}" - S3_ACCESS_KEY: "${S3_ACCESS_KEY}" - S3_SECRET_KEY: "${S3_SECRET_KEY}" - S3_BUCKET: "${S3_BUCKET}" - S3_USE_SSL: "${S3_USE_SSL}" - S3_REGION: "${S3_REGION}" - S3_PUBLIC_URL: "${S3_PUBLIC_URL}" - SMTP_HOST: "${SMTP_HOST}" - SMTP_PORT: "${SMTP_PORT}" - SMTP_USER: "${SMTP_USER}" - SMTP_FROM: "${SMTP_FROM}" - SMTP_TLS: "${SMTP_TLS}" + DEFAULT_LLM_MODEL: "gpt-4o-mini" + DEFAULT_LLM_PROVIDER: "openai" + TIMEWEB_API_BASE_URL: "https://api.timeweb.cloud" + TIMEWEB_AGENT_ACCESS_ID: "9df5206a-86e8-4c8d-87a7-6b95eb0ecab0" + TRAVELPAYOUTS_TOKEN: "83465cb3013f97f6eed45d3567721244" + NEXT_PUBLIC_ENABLED_ROUTES: "/travel,/learning,/history,/medicine,/discover" + NEXT_PUBLIC_TWOGIS_API_KEY: "e5fd2c5b-a5bc-40ec-94b3-6b5a396ebba0" + S3_ENDPOINT: "minio:9000" + S3_ACCESS_KEY: "minioadmin" + S3_SECRET_KEY: "minioadmin" + S3_BUCKET: "gooseek-artifacts" + S3_USE_SSL: "false" + S3_REGION: "us-east-1" + S3_PUBLIC_URL: "https://storage.gooseek.ru" + SMTP_HOST: "smtp.timeweb.ru" + SMTP_PORT: "465" + SMTP_USER: "2fa@gooseek.ru" + SMTP_FROM: "GooSeek 2FA <2fa@gooseek.ru>" + SMTP_TLS: "true" SITE_URL: "https://gooseek.ru" SITE_NAME: "GooSeek" ---- -apiVersion: v1 -kind: Secret -metadata: - name: gooseek-secrets - namespace: gooseek -type: Opaque -stringData: - OPENAI_API_KEY: "${OPENAI_API_KEY}" - ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}" - GEMINI_API_KEY: "${GEMINI_API_KEY}" - JWT_SECRET: "${JWT_SECRET}" - TIMEWEB_API_KEY: "${TIMEWEB_API_KEY}" - OLLAMA_API_TOKEN: "${OLLAMA_API_TOKEN}" - SMTP_PASSWORD: "${SMTP_PASSWORD}" - POSTGRES_USER: "gooseek" - POSTGRES_PASSWORD: "gooseek" diff --git a/backend/deploy/k8s/deploy.sh b/backend/deploy/k8s/deploy.sh index 9726372..6a1fce3 100755 --- a/backend/deploy/k8s/deploy.sh +++ b/backend/deploy/k8s/deploy.sh @@ -71,12 +71,13 @@ echo "=== Pushing webui to registry ===" docker push "$REGISTRY/gooseek/webui:$IMAGE_TAG" docker push "$REGISTRY/gooseek/webui:latest" -# Generate configmap/secrets from .env via envsubst +# Generate secrets from .env via envsubst (configmap has static values) echo "" -echo "=== Generating K8s manifests from .env ===" +echo "=== Generating K8s secrets from .env ===" if command -v envsubst &> /dev/null && [ -f "$ENV_FILE" ]; then - envsubst < "$SCRIPT_DIR/configmap.yaml" > "$SCRIPT_DIR/_generated_configmap.yaml" - kubectl apply -f "$SCRIPT_DIR/_generated_configmap.yaml" -n gooseek + envsubst < "$SCRIPT_DIR/secrets.yaml" > "$SCRIPT_DIR/_generated_secrets.yaml" + kubectl apply -f "$SCRIPT_DIR/_generated_secrets.yaml" -n gooseek + rm -f "$SCRIPT_DIR/_generated_secrets.yaml" # Generate monitoring manifests envsubst < "$SCRIPT_DIR/monitoring.yaml" > "$SCRIPT_DIR/_generated_monitoring.yaml" diff --git a/backend/deploy/k8s/secrets.yaml b/backend/deploy/k8s/secrets.yaml new file mode 100644 index 0000000..3ec54ad --- /dev/null +++ b/backend/deploy/k8s/secrets.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Secret +metadata: + name: gooseek-secrets + namespace: gooseek +type: Opaque +stringData: + OPENAI_API_KEY: "${OPENAI_API_KEY}" + ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}" + GEMINI_API_KEY: "${GEMINI_API_KEY}" + JWT_SECRET: "${JWT_SECRET}" + TIMEWEB_API_KEY: "${TIMEWEB_API_KEY}" + OLLAMA_API_TOKEN: "${OLLAMA_API_TOKEN}" + SMTP_PASSWORD: "${SMTP_PASSWORD}" + POSTGRES_USER: "gooseek" + POSTGRES_PASSWORD: "gooseek" diff --git a/backend/webui/src/app/api/chat/route.ts b/backend/webui/src/app/api/chat/route.ts index 2e53fe8..627c94a 100644 --- a/backend/webui/src/app/api/chat/route.ts +++ b/backend/webui/src/app/api/chat/route.ts @@ -1,34 +1,41 @@ import { NextRequest, NextResponse } from 'next/server'; -const API_URL = process.env.API_URL || 'http://localhost:3015'; +const API_GATEWAY = + process.env.API_GATEWAY_URL || + process.env.API_URL || + 'http://api-gateway:3015'; export async function POST(request: NextRequest): Promise { - const targetUrl = `${API_URL}/api/chat`; + const targetUrl = `${API_GATEWAY}/api/chat`; const headers = new Headers(); - + const authHeader = request.headers.get('authorization'); if (authHeader) { headers.set('Authorization', authHeader); } - + headers.set('Content-Type', 'application/json'); headers.set('Accept', 'application/x-ndjson'); try { const body = await request.text(); - + const response = await fetch(targetUrl, { method: 'POST', headers, body, }); - const contentType = response.headers.get('content-type') || 'application/x-ndjson'; - + const contentType = + response.headers.get('content-type') || 'application/x-ndjson'; + const stream = response.body; if (!stream) { - return NextResponse.json({ error: 'No response body' }, { status: 500 }); + return NextResponse.json( + { error: 'No response body' }, + { status: 500 }, + ); } return new NextResponse(stream, { @@ -44,7 +51,7 @@ export async function POST(request: NextRequest): Promise { console.error('[Chat API Proxy] Error:', error); return NextResponse.json( { error: 'Service unavailable' }, - { status: 503 } + { status: 503 }, ); } } diff --git a/backend/webui/src/app/api/v1/[...path]/route.ts b/backend/webui/src/app/api/v1/[...path]/route.ts index 6442807..857bdc5 100644 --- a/backend/webui/src/app/api/v1/[...path]/route.ts +++ b/backend/webui/src/app/api/v1/[...path]/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; -const API_URL = process.env.API_URL || 'http://localhost:3015'; +const API_URL = + process.env.API_GATEWAY_URL || + process.env.API_URL || + 'http://api-gateway:3015'; async function proxyRequest( request: NextRequest,