fix: resolve WebUI API proxy ECONNREFUSED and configmap placeholder issues
Some checks failed
Build and Deploy GooSeek / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy GooSeek / build-and-deploy (push) Has been cancelled
- Fix route handlers to use API_GATEWAY_URL env var with correct K8s fallback
instead of localhost:3015
- Replace ${} placeholders in configmap.yaml with actual static values to
prevent kustomize from overwriting envsubst-generated config
- Separate secrets into dedicated secrets.yaml for envsubst processing
- Update deploy.sh to only envsubst secrets (configmap now has static values)
Made-with: Cursor
This commit is contained in:
76
CONTINUE.md
76
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)
|
- `backend/deploy/k8s/opensandbox-sandbox-ns.yaml` — новый (sandbox ресурсы с правильным namespace)
|
||||||
- `sender.go` — SMTP клиент с TLS, rate limiting (1 письмо/тип/24ч), async отправка
|
- `backend/deploy/k8s/opensandbox.yaml` — убраны sandbox-scoped ресурсы
|
||||||
- `templates.go` — HTML шаблоны с брендингом GooSeek
|
- `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
|
||||||
|
|
||||||
### Интеграции
|
### Ручные действия на сервере (уже выполнены)
|
||||||
|
- Удалены ResourceQuota, LimitRange, NetworkPolicy из namespace gooseek
|
||||||
| Сервис | Уведомления | Файл |
|
- Secrets пересозданы с реальными значениями
|
||||||
|--------|-------------|------|
|
- ConfigMap дополнен недостающими TIMEWEB_* переменными
|
||||||
| auth-svc | Welcome, Password Reset | `backend/cmd/auth-svc/main.go` |
|
- Redis flush для сброса rate limit
|
||||||
| 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 <noreply@gooseek.ru>
|
|
||||||
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 переменные
|
|
||||||
|
|
||||||
## Сервер
|
## Сервер
|
||||||
- IP: 5.187.77.89
|
- IP: 5.187.77.89
|
||||||
- GPU: RTX 4060 Ti 16GB
|
- GPU: RTX 4060 Ti 16GB
|
||||||
- Site: https://gooseek.ru
|
- Site: https://gooseek.ru
|
||||||
|
- Chat API: работает (Ollama для free tier)
|
||||||
|
|
||||||
|
## Важно при следующем деплое
|
||||||
|
- `deploy.sh` использует `envsubst` для configmap — убедиться что `.env` содержит все переменные
|
||||||
|
- Secrets в `configmap.yaml` используют `${...}` — envsubst должен подставить реальные значения
|
||||||
|
- После деплоя проверить что secrets не содержат placeholder'ы
|
||||||
|
|||||||
@@ -29,41 +29,24 @@ data:
|
|||||||
OLLAMA_MODEL: "qwen3.5:9b"
|
OLLAMA_MODEL: "qwen3.5:9b"
|
||||||
OLLAMA_EMBEDDING_MODEL: "qwen3-embedding:0.6b"
|
OLLAMA_EMBEDDING_MODEL: "qwen3-embedding:0.6b"
|
||||||
OLLAMA_NUM_PARALLEL: "2"
|
OLLAMA_NUM_PARALLEL: "2"
|
||||||
DEFAULT_LLM_MODEL: "${DEFAULT_LLM_MODEL}"
|
DEFAULT_LLM_MODEL: "gpt-4o-mini"
|
||||||
DEFAULT_LLM_PROVIDER: "${DEFAULT_LLM_PROVIDER}"
|
DEFAULT_LLM_PROVIDER: "openai"
|
||||||
TIMEWEB_API_BASE_URL: "${TIMEWEB_API_BASE_URL}"
|
TIMEWEB_API_BASE_URL: "https://api.timeweb.cloud"
|
||||||
TIMEWEB_AGENT_ACCESS_ID: "${TIMEWEB_AGENT_ACCESS_ID}"
|
TIMEWEB_AGENT_ACCESS_ID: "9df5206a-86e8-4c8d-87a7-6b95eb0ecab0"
|
||||||
TRAVELPAYOUTS_TOKEN: "${TRAVELPAYOUTS_TOKEN}"
|
TRAVELPAYOUTS_TOKEN: "83465cb3013f97f6eed45d3567721244"
|
||||||
NEXT_PUBLIC_ENABLED_ROUTES: "${NEXT_PUBLIC_ENABLED_ROUTES}"
|
NEXT_PUBLIC_ENABLED_ROUTES: "/travel,/learning,/history,/medicine,/discover"
|
||||||
NEXT_PUBLIC_TWOGIS_API_KEY: "${NEXT_PUBLIC_TWOGIS_API_KEY}"
|
NEXT_PUBLIC_TWOGIS_API_KEY: "e5fd2c5b-a5bc-40ec-94b3-6b5a396ebba0"
|
||||||
S3_ENDPOINT: "${S3_ENDPOINT}"
|
S3_ENDPOINT: "minio:9000"
|
||||||
S3_ACCESS_KEY: "${S3_ACCESS_KEY}"
|
S3_ACCESS_KEY: "minioadmin"
|
||||||
S3_SECRET_KEY: "${S3_SECRET_KEY}"
|
S3_SECRET_KEY: "minioadmin"
|
||||||
S3_BUCKET: "${S3_BUCKET}"
|
S3_BUCKET: "gooseek-artifacts"
|
||||||
S3_USE_SSL: "${S3_USE_SSL}"
|
S3_USE_SSL: "false"
|
||||||
S3_REGION: "${S3_REGION}"
|
S3_REGION: "us-east-1"
|
||||||
S3_PUBLIC_URL: "${S3_PUBLIC_URL}"
|
S3_PUBLIC_URL: "https://storage.gooseek.ru"
|
||||||
SMTP_HOST: "${SMTP_HOST}"
|
SMTP_HOST: "smtp.timeweb.ru"
|
||||||
SMTP_PORT: "${SMTP_PORT}"
|
SMTP_PORT: "465"
|
||||||
SMTP_USER: "${SMTP_USER}"
|
SMTP_USER: "2fa@gooseek.ru"
|
||||||
SMTP_FROM: "${SMTP_FROM}"
|
SMTP_FROM: "GooSeek 2FA <2fa@gooseek.ru>"
|
||||||
SMTP_TLS: "${SMTP_TLS}"
|
SMTP_TLS: "true"
|
||||||
SITE_URL: "https://gooseek.ru"
|
SITE_URL: "https://gooseek.ru"
|
||||||
SITE_NAME: "GooSeek"
|
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"
|
|
||||||
|
|||||||
@@ -71,12 +71,13 @@ echo "=== Pushing webui to registry ==="
|
|||||||
docker push "$REGISTRY/gooseek/webui:$IMAGE_TAG"
|
docker push "$REGISTRY/gooseek/webui:$IMAGE_TAG"
|
||||||
docker push "$REGISTRY/gooseek/webui:latest"
|
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 ""
|
||||||
echo "=== Generating K8s manifests from .env ==="
|
echo "=== Generating K8s secrets from .env ==="
|
||||||
if command -v envsubst &> /dev/null && [ -f "$ENV_FILE" ]; then
|
if command -v envsubst &> /dev/null && [ -f "$ENV_FILE" ]; then
|
||||||
envsubst < "$SCRIPT_DIR/configmap.yaml" > "$SCRIPT_DIR/_generated_configmap.yaml"
|
envsubst < "$SCRIPT_DIR/secrets.yaml" > "$SCRIPT_DIR/_generated_secrets.yaml"
|
||||||
kubectl apply -f "$SCRIPT_DIR/_generated_configmap.yaml" -n gooseek
|
kubectl apply -f "$SCRIPT_DIR/_generated_secrets.yaml" -n gooseek
|
||||||
|
rm -f "$SCRIPT_DIR/_generated_secrets.yaml"
|
||||||
|
|
||||||
# Generate monitoring manifests
|
# Generate monitoring manifests
|
||||||
envsubst < "$SCRIPT_DIR/monitoring.yaml" > "$SCRIPT_DIR/_generated_monitoring.yaml"
|
envsubst < "$SCRIPT_DIR/monitoring.yaml" > "$SCRIPT_DIR/_generated_monitoring.yaml"
|
||||||
|
|||||||
16
backend/deploy/k8s/secrets.yaml
Normal file
16
backend/deploy/k8s/secrets.yaml
Normal file
@@ -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"
|
||||||
@@ -1,34 +1,41 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
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<NextResponse> {
|
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||||
const targetUrl = `${API_URL}/api/chat`;
|
const targetUrl = `${API_GATEWAY}/api/chat`;
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
|
|
||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = request.headers.get('authorization');
|
||||||
if (authHeader) {
|
if (authHeader) {
|
||||||
headers.set('Authorization', authHeader);
|
headers.set('Authorization', authHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.set('Content-Type', 'application/json');
|
headers.set('Content-Type', 'application/json');
|
||||||
headers.set('Accept', 'application/x-ndjson');
|
headers.set('Accept', 'application/x-ndjson');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.text();
|
const body = await request.text();
|
||||||
|
|
||||||
const response = await fetch(targetUrl, {
|
const response = await fetch(targetUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body,
|
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;
|
const stream = response.body;
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return NextResponse.json({ error: 'No response body' }, { status: 500 });
|
return NextResponse.json(
|
||||||
|
{ error: 'No response body' },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NextResponse(stream, {
|
return new NextResponse(stream, {
|
||||||
@@ -44,7 +51,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|||||||
console.error('[Chat API Proxy] Error:', error);
|
console.error('[Chat API Proxy] Error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Service unavailable' },
|
{ error: 'Service unavailable' },
|
||||||
{ status: 503 }
|
{ status: 503 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
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(
|
async function proxyRequest(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
|
|||||||
Reference in New Issue
Block a user