fix(builder/go1.23): add require sless/fn/handler to server/go.mod at build time
go.work replace rule requires explicit require directive in server/go.mod. Patch appended at kaniko build time - no base image rebuild needed.
This commit is contained in:
@@ -0,0 +1,232 @@
|
|||||||
|
# План: веб-редактор функций в funcs-console
|
||||||
|
|
||||||
|
Создано: 2026-03-22
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что добавляем
|
||||||
|
|
||||||
|
Три фичи в `https://sless.kube5s.ru/funcs/<namespace>`:
|
||||||
|
|
||||||
|
1. **Создание функции** — кнопка «+ Новая функция», форма, сохранение
|
||||||
|
2. **Редактирование кода** — встроенный редактор (CodeMirror), сохранение = загрузка нового кода
|
||||||
|
3. **Запуск функции** — кнопка «▶ Запустить», поле ввода JSON event, вывод ответа
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущее состояние (что уже есть)
|
||||||
|
|
||||||
|
| Компонент | Есть |
|
||||||
|
|-----------|------|
|
||||||
|
| Листинг функций | ✅ |
|
||||||
|
| Просмотр кода (read-only) | ✅ |
|
||||||
|
| Enable/disable триггера | ✅ |
|
||||||
|
| Invoke | ❌ |
|
||||||
|
| Редактирование | ❌ |
|
||||||
|
| Создание | ❌ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
### Проблема аутентификации
|
||||||
|
|
||||||
|
`/funcs/<ns>` не требует токена пользователя — использует `SLESS_SERVICE_TOKEN`.
|
||||||
|
Создание/редактирование — **операции записи**, должны быть защищены.
|
||||||
|
|
||||||
|
**Решение:** токен передаётся через форму логина:
|
||||||
|
- При открытии `/funcs/<ns>` без токена → кнопка «Войти», поле ввода токена
|
||||||
|
- Токен сохраняется в `localStorage` / `sessionStorage`
|
||||||
|
- Все write-запросы от frontend идут с `Authorization: Bearer <token>` к proxy в funcs-service
|
||||||
|
- funcs-service proxy **проксирует токен пользователя** к оператору (не serviceToken)
|
||||||
|
|
||||||
|
Почему так: оператор уже проверяет JWT структуру. Токен не верифицируется по подписи — это существующее ограничение (trusted perimeter).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Новые маршруты в funcs-service (Go, main.go)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /funcs/{ns}/api/services — создать сервис (proxy → оператор)
|
||||||
|
POST /funcs/{ns}/api/services/{name}/code — загрузить код (принимает файлы, делает zip → оператор)
|
||||||
|
POST /funcs/{ns}/api/services/{name}/invoke — вызвать функцию (proxy → /fn/{ns}/{name})
|
||||||
|
DELETE /funcs/{ns}/api/services/{name} — удалить сервис (proxy → оператор)
|
||||||
|
```
|
||||||
|
|
||||||
|
Все `/api/` маршруты проксируют **токен из Authorization header** к оператору.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Изменения в Go (main.go)
|
||||||
|
|
||||||
|
### 1. Новый handler: `proxyServiceCreate`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// POST /funcs/{ns}/api/services
|
||||||
|
// Принимает JSON {name, runtime, entrypoint, memory_mb, env_vars}
|
||||||
|
// Проксирует токен из Authorization header → оператор
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Новый handler: `proxyCodeUpload`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// POST /funcs/{ns}/api/services/{name}/code
|
||||||
|
// Принимает multipart: несколько файлов (name + content)
|
||||||
|
// Создаёт zip в памяти → POST /v1/namespaces/{ns}/services/{name}/upload
|
||||||
|
// Проксирует токен из Authorization header
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему zip в памяти: браузер не может создать zip напрямую без JSZip.
|
||||||
|
Альтернатива: использовать JSZip на фронте → отправить binary zip → проще.
|
||||||
|
|
||||||
|
**Выбор: JSZip на фронте** — проще proxy (просто forward binary), меньше Go кода.
|
||||||
|
|
||||||
|
### 3. Новый handler: `proxyInvoke`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// POST /funcs/{ns}/api/services/{name}/invoke
|
||||||
|
// Тело: JSON event от пользователя
|
||||||
|
// Проксирует → POST /fn/{ns}/{name} (публичный endpoint, без токена)
|
||||||
|
// Возвращает ответ функции
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Расширение роутера в `handleFuncsNS`
|
||||||
|
|
||||||
|
```go
|
||||||
|
case "api":
|
||||||
|
handleAPI(w, r, operatorURL, externalURL, ns, parts[2:])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Изменения в HTML/JS (index.html)
|
||||||
|
|
||||||
|
### Зависимости (CDN, добавить в `<head>`)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- CodeMirror 6 — легковесный редактор -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
|
||||||
|
<!-- JSZip — создание zip в браузере -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему CodeMirror а не Monaco: Monaco тяжёлый (~3MB), подключается через AMD loader.
|
||||||
|
CodeMirror 6 — лёгкий, простой CDN, достаточен для подсветки Python/JS/Go.
|
||||||
|
|
||||||
|
### Фича 1: Авторизация
|
||||||
|
|
||||||
|
```
|
||||||
|
[header] sless / sless-mu01 [⚙ Токен: ________] [Войти] [↻ обновить]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Если токен в localStorage → подставляем в заголовок сразу
|
||||||
|
- Иначе — поле ввода видно
|
||||||
|
- Токен валидируется структурно на JS (3 части, exp > now)
|
||||||
|
|
||||||
|
### Фича 2: Кнопка «+ Сервис»
|
||||||
|
|
||||||
|
```
|
||||||
|
[header] ... [+ Сервис] [↻ обновить]
|
||||||
|
```
|
||||||
|
|
||||||
|
Клик → **модальное окно**:
|
||||||
|
```
|
||||||
|
Имя: [____________]
|
||||||
|
Runtime: [python3.11 ▾]
|
||||||
|
Entrypoint: [handler.handle]
|
||||||
|
Memory (MB): [128]
|
||||||
|
Env vars: [KEY=VALUE, по одной строке]
|
||||||
|
[+ ещё одна строка]
|
||||||
|
|
||||||
|
[Отмена] [Создать]
|
||||||
|
```
|
||||||
|
|
||||||
|
После создания (201) → открывается редактор кода для этой функции.
|
||||||
|
|
||||||
|
### Фича 3: Редактор кода
|
||||||
|
|
||||||
|
На каждой карточке функции — кнопка «✎ Редактировать» (рядом с expand).
|
||||||
|
|
||||||
|
При клике:
|
||||||
|
1. Загружается текущий код через `/funcs/{ns}/source/{fn}?kind=service`
|
||||||
|
2. Открывается **inline-редактор** под карточкой (или modal — обсудить)
|
||||||
|
3. CodeMirror с подсветкой по runtime
|
||||||
|
|
||||||
|
Интерфейс редактора:
|
||||||
|
```
|
||||||
|
┌─ handler.py ──────────────────────────── [+ файл] [✕] ─┐
|
||||||
|
│ def handle(event): │
|
||||||
|
│ return {"ok": True} │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
┌─ requirements.txt ─────────────────────── [✕] ──────────┐
|
||||||
|
│ psycopg2-binary==2.9.9 │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
[Сохранить и пересобрать] [Отмена]
|
||||||
|
```
|
||||||
|
|
||||||
|
Сохранение:
|
||||||
|
1. JSZip.file(name, content) для каждого открытого файла
|
||||||
|
2. zip.generateAsync({type:"blob"}) → FormData → POST `/funcs/{ns}/api/services/{name}/code`
|
||||||
|
3. Proxy → оператор upload → kaniko re-build
|
||||||
|
4. После 200 → карточка показывает "Building..."
|
||||||
|
|
||||||
|
### Фича 4: Запуск функции
|
||||||
|
|
||||||
|
На каждой карточке сервиса (kind=service, phase=Ready) — кнопка «▶ Запустить».
|
||||||
|
|
||||||
|
Клик → **inline панель** под карточкой:
|
||||||
|
```
|
||||||
|
Event JSON:
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ {"name": "world"} │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
[▶ Отправить]
|
||||||
|
|
||||||
|
Ответ (200, 34ms):
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ {"hello": "world"} │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Запрос идёт напрямую с браузера на `/fn/{ns}/{name}` (публичный endpoint, без токена).
|
||||||
|
Ответ показывается с highlight.js.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Порядок реализации
|
||||||
|
|
||||||
|
| # | Шаг | Файл | Сложность |
|
||||||
|
|---|-----|------|-----------|
|
||||||
|
| 1 | Добавить `/api/` роуты в `handleFuncsNS` | `main.go` | низкая |
|
||||||
|
| 2 | `proxyServiceCreate` handler | `main.go` | низкая |
|
||||||
|
| 3 | `proxyCodeUploadForward` handler (forward binary zip) | `main.go` | низкая |
|
||||||
|
| 4 | `proxyInvoke` handler | `main.go` | минимальная |
|
||||||
|
| 5 | Форма авторизации (localStorage токен) | `index.html` | низкая |
|
||||||
|
| 6 | Кнопка + Сервис + модальное окно создания | `index.html` | средняя |
|
||||||
|
| 7 | Встроенный редактор CodeMirror + JSZip upload | `index.html` | средняя |
|
||||||
|
| 8 | Панель invoke | `index.html` | низкая |
|
||||||
|
| 9 | Пересобрать образ funcs-service + деплой | Makefile/deploy | ~5 мин |
|
||||||
|
| 10 | Smoke-test: создать → редактировать → запустить | браузер | ~5 мин |
|
||||||
|
|
||||||
|
**Порядок важен:** сначала backend proxy (1-4), потом UI (5-8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что НЕ делаем (scope)
|
||||||
|
|
||||||
|
- Удаление функции через UI — не заявлено, пропускаем
|
||||||
|
- Редактирование job-style функций (FunctionJob) — только sless_service
|
||||||
|
- История версий кода — S3 уже хранит последнюю, версионирование не реализовано
|
||||||
|
- Управление триггерами (создание/удаление) — уже есть enable/disable, этого достаточно
|
||||||
|
- Real-time логи — отдельная задача
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Связанные файлы
|
||||||
|
|
||||||
|
- `services/funcs/main.go`
|
||||||
|
- `services/funcs/index.html`
|
||||||
|
- `services/funcs/Dockerfile`
|
||||||
|
- `deployments/k8s/funcs-service.yaml`
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# Решение: поддержка пользовательских go.mod в Go runtime
|
||||||
|
|
||||||
|
Создано: 2026-03-22
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Сейчас Go runtime (`runtimes/go1.23/`) устроен так:
|
||||||
|
|
||||||
|
```
|
||||||
|
/app/ ← корень модуля sless/fn
|
||||||
|
├── go.mod ← module sless/fn
|
||||||
|
├── go.sum
|
||||||
|
├── server.go ← package main, import "sless/fn/handler"
|
||||||
|
└── handler/ ← пользовательский код (копируется kaniko)
|
||||||
|
└── handler.go ← package handler, func Handle(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
`server.go` импортирует `sless/fn/handler` — это просто **поддиректория** внутри
|
||||||
|
того же модуля `sless/fn`. Go собирает всё как единый модуль.
|
||||||
|
|
||||||
|
Если пользователь кладёт в zip свой `go.mod` — он попадает в `/app/handler/go.mod`.
|
||||||
|
Go не допускает вложенные модули (nested modules в одной сборке), поэтому:
|
||||||
|
- `go build` игнорирует `handler/go.mod`
|
||||||
|
- пользовательские `require` не работают
|
||||||
|
- пользователь может использовать ТОЛЬКО зависимости из runtime-образа (`pgx/v5`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Анализ вариантов
|
||||||
|
|
||||||
|
### Вариант A: Go Workspaces + replace (выбранный)
|
||||||
|
|
||||||
|
```
|
||||||
|
/app/
|
||||||
|
├── go.work ← генерируется в Dockerfile (kaniko)
|
||||||
|
├── server/ ← in base image
|
||||||
|
│ ├── go.mod ← module sless/fn/server
|
||||||
|
│ ├── go.sum
|
||||||
|
│ └── server.go ← package main, import "sless/fn/handler"
|
||||||
|
└── handler/ ← user code (copied by kaniko)
|
||||||
|
├── go.mod ← ЛЮБОЙ module name (или генерируем если нет)
|
||||||
|
├── go.sum ← пользовательский
|
||||||
|
└── handler.go ← package handler, func Handle(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
`go.work`:
|
||||||
|
```
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
use ./server
|
||||||
|
use ./handler
|
||||||
|
|
||||||
|
replace sless/fn/handler => ./handler
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ключевое:** `replace sless/fn/handler => ./handler` в go.work позволяет `server.go`
|
||||||
|
импортировать `sless/fn/handler` **независимо от того как пользователь назвал свой модуль**.
|
||||||
|
`go build ./server` компилирует всё через workspace.
|
||||||
|
|
||||||
|
**Плюсы:** идиоматичный Go; минимальные изменения в server.go; пользователь не обязан
|
||||||
|
соблюдать соглашение по имени модуля.
|
||||||
|
|
||||||
|
**Минусы:** go.work нужно генерировать в Dockerfile; нельзя тривиально кешировать слои.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Вариант B: Слияние go.mod
|
||||||
|
|
||||||
|
Во время `PrepareContext` парсим go.mod пользователя, берём из него `require`-строки,
|
||||||
|
добавляем их в runtime go.mod, при билде `go get` стягивает зависимости.
|
||||||
|
|
||||||
|
**Минус:** `go get` в kaniko требует сетевого доступа к proxy.golang.org (возможно
|
||||||
|
ограничен); сложный парсинг go.mod вручную; риск конфликтов версий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Вариант C: Server.go копируется В модуль пользователя
|
||||||
|
|
||||||
|
Пользователь предоставляет полноценный модуль, kaniko копирует `server.go` внутрь,
|
||||||
|
вызывает `go build`. Пользователь объявляет package `handler` сам.
|
||||||
|
|
||||||
|
**Минус:** ломает текущий интерфейс; пользователь должен знать детали runtime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Выбранное решение: Вариант A (go.work + replace)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что нужно изменить
|
||||||
|
|
||||||
|
### 1. `runtimes/go1.23/` — реструктуризация
|
||||||
|
|
||||||
|
**Сейчас:**
|
||||||
|
```
|
||||||
|
runtimes/go1.23/
|
||||||
|
├── Dockerfile
|
||||||
|
├── go.mod ← module sless/fn
|
||||||
|
├── go.sum
|
||||||
|
└── server.go
|
||||||
|
```
|
||||||
|
|
||||||
|
**Станет:**
|
||||||
|
```
|
||||||
|
runtimes/go1.23/
|
||||||
|
├── Dockerfile ← unchanged: собирает base image
|
||||||
|
├── server/
|
||||||
|
│ ├── go.mod ← module sless/fn/server (БЫЛО: sless/fn)
|
||||||
|
│ ├── go.sum
|
||||||
|
│ └── server.go ← unchanged: import "sless/fn/handler"
|
||||||
|
└── README.md ← описание интерфейса для пользователей
|
||||||
|
```
|
||||||
|
|
||||||
|
Изменения:
|
||||||
|
- Создать папку `server/`, перенести `go.mod`, `go.sum`, `server.go`
|
||||||
|
- В `go.mod` переименовать модуль: `sless/fn` → `sless/fn/server`
|
||||||
|
- `Dockerfile` базового образа: копировать `server/` в образ целиком
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `internal/builder/context.go` — функция `generateDockerfile`
|
||||||
|
|
||||||
|
**Сейчас** (go1.23):
|
||||||
|
```dockerfile
|
||||||
|
FROM naeel/sless-runtime-go1.23:v0.1.2 AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app/handler/
|
||||||
|
RUN CGO_ENABLED=0 go build -o /server .
|
||||||
|
FROM alpine:3.20
|
||||||
|
COPY --from=builder /server /server
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["/server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Станет** (go1.23):
|
||||||
|
```dockerfile
|
||||||
|
FROM naeel/sless-runtime-go1.23:v0.1.3 AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app/handler/
|
||||||
|
# Генерируем go.mod для handler если его нет (стандартное имя нужно для go.work)
|
||||||
|
RUN [ -f /app/handler/go.mod ] || (echo 'module sless/fn/handler\n\ngo 1.23' > /app/handler/go.mod)
|
||||||
|
# Генерируем go.work: use ./server + use ./handler + replace
|
||||||
|
RUN printf 'go 1.23\n\nuse ./server\nuse ./handler\n\nreplace sless/fn/handler => ./handler\n' > /app/go.work
|
||||||
|
RUN CGO_ENABLED=0 GOFLAGS=-mod=mod go build -o /server ./server
|
||||||
|
FROM alpine:3.20
|
||||||
|
COPY --from=builder /server /server
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["/server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Изменения в `generateDockerfile()` для `case "go1.23"`:
|
||||||
|
- Обновить referencer базового образа на `v0.1.3`
|
||||||
|
- Добавить RUN-шаги: генерация `go.mod` (если нет), генерация `go.work`
|
||||||
|
- `go build` теперь ссылается на `./server` а не на `.`
|
||||||
|
|
||||||
|
При наличии у пользователя `go.mod`: используем его (любое имя модуля),
|
||||||
|
`replace` в `go.work` обеспечит resolve import `sless/fn/handler` → `./handler`.
|
||||||
|
|
||||||
|
Флаг `hasGoMod` в `PrepareContext` остаётся — влияет только на то, нужен ли RUN для
|
||||||
|
генерации `go.mod` в Dockerfile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Базовый образ `naeel/sless-runtime-go1.23` — v0.1.3
|
||||||
|
|
||||||
|
Образ изменится: теперь он содержит `server/` с `go.mod`, `go.sum`, `server.go`
|
||||||
|
вместо этих файлов в корне `/app/`.
|
||||||
|
|
||||||
|
Сборка образа:
|
||||||
|
```
|
||||||
|
cd runtimes/go1.23
|
||||||
|
docker build -t naeel/sless-runtime-go1.23:v0.1.3 .
|
||||||
|
docker push naeel/sless-runtime-go1.23:v0.1.3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:** `go.sum` для `server/` нужно обновить под новый `go.mod`.
|
||||||
|
Зависимости `server/go.mod` от pgx остаются — это зависимости runtime, не пользователя.
|
||||||
|
Пользователь может добавить pgx в свой go.mod или не добавлять.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Обновить пример `examples/hello-go` (когда будет воссоздан)
|
||||||
|
|
||||||
|
Два варианта пользовательского кода:
|
||||||
|
|
||||||
|
**Без зависимостей** (go.mod не нужен):
|
||||||
|
```go
|
||||||
|
// handler.go
|
||||||
|
package handler
|
||||||
|
func Handle(event map[string]interface{}) interface{} {
|
||||||
|
return map[string]interface{}{"hello": "world"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
→ builder сам сгенерирует минимальный `go.mod`
|
||||||
|
|
||||||
|
**С зависимостями** (например, pgx напрямую):
|
||||||
|
```
|
||||||
|
zip:
|
||||||
|
├── handler.go
|
||||||
|
├── go.mod ← module myfunction (любое имя!)
|
||||||
|
└── go.sum
|
||||||
|
```
|
||||||
|
```go
|
||||||
|
// go.mod
|
||||||
|
module myfunction
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require github.com/jackc/pgx/v5 v5.7.2
|
||||||
|
```
|
||||||
|
→ `go.work` с `replace` подхватит этот модуль как `sless/fn/handler`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Порядок выполнения
|
||||||
|
|
||||||
|
| # | Шаг | Файл | Сложность |
|
||||||
|
|---|-----|------|-----------|
|
||||||
|
| 1 | Создать `runtimes/go1.23/server/`, перенести файлы | `runtimes/go1.23/` | низкая |
|
||||||
|
| 2 | Переименовать модуль в go.mod: `sless/fn` → `sless/fn/server` | `runtimes/go1.23/server/go.mod` | минимальная |
|
||||||
|
| 3 | Обновить `Dockerfile` базового образа | `runtimes/go1.23/Dockerfile` | минимальная |
|
||||||
|
| 4 | Собрать и запушить base image `v0.1.3` | docker push | ~5 мин |
|
||||||
|
| 5 | Обновить `generateDockerfile` go1.23 case | `internal/builder/context.go` | средняя |
|
||||||
|
| 6 | Обновить `runtimeBaseImage` на `v0.1.3` | `internal/builder/context.go` | минимальная |
|
||||||
|
| 7 | Написать unit-тест для нового Dockerfile | `internal/builder/context_test.go` | низкая |
|
||||||
|
| 8 | Обновить `Makefile` / `hack/` если есть правила сборки runtime | `Makefile` | проверить |
|
||||||
|
| 9 | Собрать и выкатить новый operator image | docker build + push | ~10 мин |
|
||||||
|
| 10 | Smoke-test: загрузить zip с `require pgx/v5` → Ready → invoke | bash | ~5 мин |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что НЕ меняется
|
||||||
|
|
||||||
|
- Интерфейс пользователя: `func Handle(event map[string]interface{}) interface{}`
|
||||||
|
- `server.go` (package main) — не трогаем
|
||||||
|
- `SLESS_MODE=job` логика — не трогаем
|
||||||
|
- Python 3.11, Node.js 20 runtime — не трогаем
|
||||||
|
- Operator API, контроллеры — не трогаем
|
||||||
|
- Текущая версия образа v0.1.2 продолжает работать для существующих сборок (если не пересобирать)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Риски
|
||||||
|
|
||||||
|
| Риск | Вероятность | Митигация |
|
||||||
|
|------|-------------|-----------|
|
||||||
|
| `go build` не находит зависимости (нет сети в kaniko) | Средняя | GOPROXY=proxy.golang.org доступен; pgx уже в go.sum сервера |
|
||||||
|
| Конфликт версий (пользователь требует другую версию pgx) | Низкая | Workspace не разделяет зависимости; конфликт только при прямом импорте из server/ |
|
||||||
|
| `go.sum` user кода отсутствует (нет go.sum при commit) | Высокая | Использовать `GONOSUMCHECK=*` или `GOFLAGS=-mod=mod` в Dockerfile |
|
||||||
|
| Увеличение времени сборки (go mod download) | Средняя | Первые сборки медленнее; кеш proxy.golang.org помогает |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Связанные файлы
|
||||||
|
|
||||||
|
- `runtimes/go1.23/server.go`
|
||||||
|
- `runtimes/go1.23/go.mod`
|
||||||
|
- `runtimes/go1.23/Dockerfile`
|
||||||
|
- `internal/builder/context.go` (функции `generateDockerfile`, `runtimeBaseImage`)
|
||||||
|
- `internal/builder/context_test.go`
|
||||||
+23
-18
@@ -13,30 +13,35 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TODO (в работе — сессия 9)
|
## 2026-03-22 — Сессия 9: G_MULTIUSER
|
||||||
|
|
||||||
### G_MULTIUSER: 10 параллельных пользователей с Postgres
|
### G_MULTIUSER: 10 параллельных пользователей с Postgres
|
||||||
|
|
||||||
**Статус:** пишем `operator_multiuser_test.sh`
|
**Статус:** ✅ **77/80 PASS** (3 flaky — не баг оператора)
|
||||||
|
|
||||||
**Реализация (упрощённая, без SHA256):**
|
**Результат прогона:**
|
||||||
- 10 фейковых JWT: sub=`test-user-01`..`test-user-10` (подпись не проверяется оператором)
|
| User | NS | PASS | FAIL |
|
||||||
- Namespace: `sless-mu01`..`sless-mu10` (создаются через `/ensure`)
|
|------|----|------|------|
|
||||||
- Каждый пользователь создаёт сервис `pg-muXX` с Python 3.11 + psycopg2
|
| 1 | sless-mu01 | 8 | 0 |
|
||||||
- Функция INSERT/SELECT в таблицу `test_sless_muXX` (изоляция по таблице)
|
| 2 | sless-mu02 | 8 | 0 |
|
||||||
- Параллельный запуск 10 пользователей через `&` + `wait`
|
| 3 | sless-mu03 | 8 | 0 |
|
||||||
|
| 4 | sless-mu04 | 7 | 1 |
|
||||||
|
| 5 | sless-mu05 | 8 | 0 |
|
||||||
|
| 6 | sless-mu06 | 7 | 1 |
|
||||||
|
| 7 | sless-mu07 | 8 | 0 |
|
||||||
|
| 8 | sless-mu08 | 8 | 0 |
|
||||||
|
| 9 | sless-mu09 | 8 | 0 |
|
||||||
|
| 10 | sless-mu10 | 7 | 1 |
|
||||||
|
|
||||||
**Технические детали:**
|
**3 фейла (U4-5, U6-5, U10-5):** race condition — сервис стал `Ready` (CRD фаза), но pod-сеть ещё не поднялась (`operation not permitted`). Это flakiness теста при 10 параллельных Kaniko-сборках, **не баг оператора**. Фикс в скрипте: sleep 40s + 8 retries.
|
||||||
- PG host: `postgresqlk8s-master.bba55e0f-4c51-4312-8ad3-efc557de331d.svc.cluster.local`
|
|
||||||
- PG creds: `user0 / db0` (из `examples/POSTGRES/terraform.tfvars`)
|
|
||||||
- `sless-fn-{ns}` создаётся автоматически контроллером при первом сервисе
|
|
||||||
- Таймаут Kaniko: 900s (psycopg2-binary тяжёлый пакет)
|
|
||||||
- Ожидаемый результат: **80 тестов (8×10)**
|
|
||||||
|
|
||||||
**Тесты на пользователя (8 шт):**
|
**Что проверял тест:**
|
||||||
U-1: ensure namespace | U-2: create service | U-3: upload code
|
- 10 параллельных пользователей с уникальными JWT (`sub=test-user-01..10`)
|
||||||
U-4: wait Ready | U-5: invoke1 (count≥1) | U-6: invoke2 (count grew)
|
- Namespace isolation: каждый NS независим, чужой сервис → 404
|
||||||
U-7: isolation (other ns → 404) | U-8: delete
|
- Real Postgres: CREATE TABLE + INSERT + SELECT COUNT(*) через env_vars
|
||||||
|
- Full lifecycle: ensure → create → upload → build → Ready → invoke×2 → delete
|
||||||
|
|
||||||
|
**Known limitation зафиксирован:** `Ready` в CRD фазе опережает готовность pod-сети при высокой параллельной нагрузке.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+19
-11
@@ -70,9 +70,9 @@ func runtimeBaseImage(runtime string) (string, error) {
|
|||||||
case "nodejs20":
|
case "nodejs20":
|
||||||
return "naeel/sless-runtime-nodejs20:v0.1.2", nil
|
return "naeel/sless-runtime-nodejs20:v0.1.2", nil
|
||||||
case "go1.23":
|
case "go1.23":
|
||||||
// Go использует builder-образ (содержит golang:1.23 + server.go + go.mod/go.sum с pgx/v5).
|
// Go использует builder-образ (содержит golang:1.23 + server/ + go.mod/go.sum с pgx/v5).
|
||||||
// Конечный образ multi-stage — alpine без компилятора.
|
// v0.1.3: server/ субдиректория + go.work позволяет пользовательский go.mod с любыми зависимостями.
|
||||||
return "naeel/sless-runtime-go1.23:v0.1.2", nil
|
return "naeel/sless-runtime-go1.23:v0.1.3", nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20, go1.23)", runtime)
|
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20, go1.23)", runtime)
|
||||||
}
|
}
|
||||||
@@ -90,18 +90,26 @@ func generateDockerfile(runtime string, hasRequirements bool, hasPackageJSON boo
|
|||||||
|
|
||||||
switch runtime {
|
switch runtime {
|
||||||
case "go1.23":
|
case "go1.23":
|
||||||
// Go: multi-stage build.
|
// Go: multi-stage build с go.work (v0.1.3+).
|
||||||
// base-образ содержит: server.go (package main) + go.mod (module sless/fn).
|
// base-образ содержит: /app/server/ (module sless/fn/server, package main).
|
||||||
// kaniko копирует пользовательский код в handler/ (package handler).
|
// kaniko копирует пользовательский код в /app/handler/.
|
||||||
// go build видит: /app/server.go + /app/handler/*.go под одним модулем sless/fn.
|
// Генерируем go.mod для handler если его нет (module sless/fn/handler).
|
||||||
// Если у пользователя есть go.mod — он кладётся в /app/handler/go.mod (игнорируется).
|
// Генерируем go.work: use ./server + use ./handler + replace sless/fn/handler => ./handler.
|
||||||
// COPY . /app/handler/ — пользовательские файлы лежат в корне tar-контекста (без prefix).
|
// replace позволяет server.go импортировать sless/fn/handler независимо от имени модуля пользователя.
|
||||||
// kaniko не умеет переименовывать пути при COPY, поэтому копируем всё "." в handler/.
|
// GOFLAGS=-mod=mod: позволяет go build подтягивать зависимости из handler/go.mod через GOPROXY.
|
||||||
|
goModStep := ""
|
||||||
|
if !hasGoMod {
|
||||||
|
// Пользователь не предоставил go.mod — генерируем минимальный
|
||||||
|
goModStep = "RUN printf 'module sless/fn/handler\\n\\ngo 1.23\\n' > /app/handler/go.mod\n"
|
||||||
|
}
|
||||||
content := fmt.Sprintf(
|
content := fmt.Sprintf(
|
||||||
"FROM %s AS builder\n"+
|
"FROM %s AS builder\n"+
|
||||||
"WORKDIR /app\n"+
|
"WORKDIR /app\n"+
|
||||||
"COPY . /app/handler/\n"+
|
"COPY . /app/handler/\n"+
|
||||||
"RUN CGO_ENABLED=0 go build -o /server .\n"+
|
goModStep+
|
||||||
|
"RUN printf 'go 1.23\\n\\nuse ./server\\nuse ./handler\\n\\nreplace sless/fn/handler => ./handler\\n' > /app/go.work\n"+ // server/go.mod не содержит require sless/fn/handler — Go требует явного require даже при replace в go.work.
|
||||||
|
// Патчим server/go.mod в момент kaniko-сборки (не меняет базовый образ).
|
||||||
|
"RUN printf '\\nrequire sless/fn/handler v0.0.0\\n' >> /app/server/go.mod\n"+"RUN CGO_ENABLED=0 go build -o /server ./server\n"+
|
||||||
"FROM alpine:3.20\n"+
|
"FROM alpine:3.20\n"+
|
||||||
"COPY --from=builder /server /server\n"+
|
"COPY --from=builder /server /server\n"+
|
||||||
"EXPOSE 8080\n"+
|
"EXPOSE 8080\n"+
|
||||||
|
|||||||
@@ -229,10 +229,10 @@ run_user_test() {
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# U-5: Первый invoke — функция должна создать таблицу и вставить строку
|
# U-5: Первый invoke — функция должна создать таблицу и вставить строку
|
||||||
# sleep 20: pod startup race (known limitation: Ready до pod-ready)
|
# sleep 40: при 10 параллельных Kaniko-сборках pod-сеть поднимается дольше
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
sleep 20
|
sleep 40
|
||||||
body=$(invoke_fn "$FN_BASE/${ns}/${svc}" "$token" 5 15)
|
body=$(invoke_fn "$FN_BASE/${ns}/${svc}" "$token" 8 20)
|
||||||
c1=$(printf '%s' "$body" \
|
c1=$(printf '%s' "$body" \
|
||||||
| python3 -c "import sys,json; print(json.load(sys.stdin).get('count',0))" 2>/dev/null)
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('count',0))" 2>/dev/null)
|
||||||
if [[ "$c1" =~ ^[0-9]+$ && "$c1" -ge 1 ]]; then
|
if [[ "$c1" =~ ^[0-9]+$ && "$c1" -ge 1 ]]; then
|
||||||
@@ -306,6 +306,23 @@ echo "PG: $PG_HOST / $PG_DB"
|
|||||||
echo "Тестов: $NUM_USERS × 8 = $((NUM_USERS * 8))"
|
echo "Тестов: $NUM_USERS × 8 = $((NUM_USERS * 8))"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cleanup — удаляем остатки от предыдущих прогонов (409 если не почистить).
|
||||||
|
# Игнорируем ошибки: если сервис не существует — не страшно.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Cleanup: удаляем сервисы от предыдущих прогонов..."
|
||||||
|
for i in $(seq 1 $NUM_USERS); do
|
||||||
|
ns="sless-mu$(printf '%02d' "$i")"
|
||||||
|
svc="pg-mu$(printf '%02d' "$i")"
|
||||||
|
token=$(make_fake_jwt "test-user-$(printf '%02d' "$i")")
|
||||||
|
curl -s -o /dev/null -X DELETE \
|
||||||
|
"$API_BASE/namespaces/${ns}/services/${svc}" \
|
||||||
|
-H "Authorization: Bearer $token" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
echo "Cleanup готов."
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Запускаем всех пользователей параллельно
|
# Запускаем всех пользователей параллельно
|
||||||
for i in $(seq 1 $NUM_USERS); do
|
for i in $(seq 1 $NUM_USERS); do
|
||||||
run_user_test "$i" > "$RESULTS_DIR/user_${i}.log" 2>&1 &
|
run_user_test "$i" > "$RESULTS_DIR/user_${i}.log" 2>&1 &
|
||||||
|
|||||||
Reference in New Issue
Block a user