fix: nodejs ESM wrapper via new Function, README for fission-console
- Replace two-file zip (_userfn.cjs + main.js) with single-file approach - User code embedded via new Function(module, exports, code) for safe CJS isolation - Auto-adds status:200 when function omits it - Add fission-console technical README section (v0.6.7)
This commit is contained in:
@@ -92,3 +92,196 @@ TF_CLI_CONFIG_FILE=/tmp/terraformrc-fission terraform apply -auto-approve
|
||||
Где:
|
||||
- `900` — число запросов на endpoint
|
||||
- `90` — параллелизм
|
||||
|
||||
---
|
||||
|
||||
# fission-console
|
||||
|
||||
Backend сервис для управления serverless-функциями поверх Fission.
|
||||
Предоставляет REST API + embedded Web UI. Каждый пользователь изолирован в отдельном Kubernetes namespace.
|
||||
|
||||
## Деплой
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Кластер | `kube5s.ru` |
|
||||
| Namespace | `fission` |
|
||||
| URL | `https://fission.kube5s.ru/console/` |
|
||||
| Образ | `naeel/fission-console:<version>` |
|
||||
| Порт | `8090` |
|
||||
| Текущая версия | `v0.6.7` |
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `PORT` | `8090` | Порт HTTP-сервера |
|
||||
| `FISSION_ROUTER_URL` | `http://router.fission.svc.cluster.local` | URL Fission Router для invoke |
|
||||
| `FISSION_INVOKE_TIMEOUT` | `20s` | Таймаут вызова функции |
|
||||
| `REAPER_INTERVAL` | `5m` | Интервал очистки истёкших TTL |
|
||||
| `FISSION_TEST_MODE` | `false` | Тестовый режим — отключает JWT, включает `X-Test-Sub` |
|
||||
| `FISSION_SYSTEM_NAMESPACE` | `fission` | Namespace самого Fission |
|
||||
| `KUBECONFIG` | — | Путь к kubeconfig (если не in-cluster) |
|
||||
|
||||
## Аутентификация
|
||||
|
||||
**Production:** `Authorization: Bearer <jwt>` через `/console/api/auth`.
|
||||
|
||||
**Test Mode** (`FISSION_TEST_MODE=true`):
|
||||
|
||||
```bash
|
||||
curl -H "X-Test-Sub: user@example.com" https://fission.kube5s.ru/console/api/functions
|
||||
```
|
||||
|
||||
## Изоляция по namespace
|
||||
|
||||
```
|
||||
sub → SHA256(sub)[:8] → namespace = "fission-{16 hex chars}"
|
||||
```
|
||||
|
||||
При первом обращении namespace + RoleBinding-и создаются автоматически. Namespace регистрируется в `FISSION_RESOURCE_NAMESPACES` всех Fission deployments.
|
||||
|
||||
## API
|
||||
|
||||
Базовый путь: `/console/api`
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| `GET` | `/console/api/functions` | Список функций |
|
||||
| `POST` | `/console/api/functions` | Создать функцию |
|
||||
| `GET` | `/console/api/functions/{name}` | Получить функцию |
|
||||
| `DELETE` | `/console/api/functions/{name}` | Удалить функцию |
|
||||
| `PUT` | `/console/api/functions/{name}/code` | Обновить код |
|
||||
| `POST` | `/console/api/functions/{name}/invoke` | Вызвать функцию |
|
||||
| `GET` | `/console/api/environments` | Список environments |
|
||||
| `GET` | `/console/api/packages` | Список packages |
|
||||
| `GET` | `/console/api/httptriggers` | Список HTTP triggers |
|
||||
|
||||
### Создать функцию
|
||||
|
||||
```bash
|
||||
curl -X POST https://fission.kube5s.ru/console/api/functions \
|
||||
-H "X-Test-Sub: user@example.com" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "hello",
|
||||
"language": "nodejs",
|
||||
"ttl": "1h",
|
||||
"code": "module.exports = async function(ctx) { return { body: \"Hello!\" }; };"
|
||||
}'
|
||||
```
|
||||
|
||||
**Поля запроса:**
|
||||
|
||||
| Поле | Обязательно | Описание |
|
||||
|---|---|---|
|
||||
| `name` | да | Имя функции |
|
||||
| `language` | да* | Язык: `nodejs`, `python`, `go`, `php`, `ruby`, `perl` |
|
||||
| `environment` | да* | Имя существующего Environment CRD |
|
||||
| `code` | да | Исходный код |
|
||||
| `ttl` | нет | Время жизни: `30m`, `1h`, `7d`, `2m` |
|
||||
| `entrypoint` | нет | Точка входа (auto-detect по языку) |
|
||||
|
||||
\* Одно из двух.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"name": "hello",
|
||||
"route": "/hello",
|
||||
"httptrigger": "hello-route",
|
||||
"package": "hello-pkg",
|
||||
"expires_at": "2026-04-19T15:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Вызвать функцию
|
||||
|
||||
```bash
|
||||
curl -X POST https://fission.kube5s.ru/console/api/functions/hello/invoke \
|
||||
-H "X-Test-Sub: user@example.com" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"invoke_url": "http://router.fission.svc.cluster.local/hello",
|
||||
"latency_ms": 268,
|
||||
"status": 200,
|
||||
"response_raw": "Hello!"
|
||||
}
|
||||
```
|
||||
|
||||
## Языки и формат кода
|
||||
|
||||
### NodeJS (рекомендуется)
|
||||
|
||||
```js
|
||||
module.exports = async function(ctx) {
|
||||
return { body: JSON.stringify({ ok: true }) };
|
||||
};
|
||||
```
|
||||
|
||||
- Node.js 22, ESM mode
|
||||
- `status: 200` добавляется автоматически, если не указан
|
||||
- Код изолируется через `new Function('module', 'exports', code)`
|
||||
- Поддерживается `module.exports.handler`, `.main`, `.default`
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
def main():
|
||||
return {"body": '{"ok": true}', "status": 200}
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Hello!"))
|
||||
}
|
||||
```
|
||||
|
||||
## TTL
|
||||
|
||||
| Пример | Значение |
|
||||
|---|---|
|
||||
| `30m` | 30 минут |
|
||||
| `1h` | 1 час |
|
||||
| `7d` | 7 дней |
|
||||
| `2m` | 2 минуты |
|
||||
|
||||
По истечении TTL функция + package + httptrigger удаляются reaperом. Пустой environment удаляется вместе с последней функцией.
|
||||
|
||||
## Health
|
||||
|
||||
```bash
|
||||
curl https://fission.kube5s.ru/health
|
||||
curl https://fission.kube5s.ru/console/health
|
||||
```
|
||||
|
||||
## Сборка образа
|
||||
|
||||
```bash
|
||||
cd console/
|
||||
go build . # проверка компиляции
|
||||
docker build -t naeel/fission-console:vX.Y.Z .
|
||||
docker push naeel/fission-console:vX.Y.Z
|
||||
kubectl set image deployment/fission-console console=naeel/fission-console:vX.Y.Z -n fission
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.6.7
|
||||
- NodeJS: однофайловый zip (`main.js` с `new Function` wrapper)
|
||||
- NodeJS: автоматический `status: 200` если функция не возвращает его
|
||||
|
||||
### v0.6.6
|
||||
- NodeJS: упаковка кода в zip (ESM mode требует `.js`)
|
||||
- NodeJS: исправлена точка входа (`main.main` → `main`)
|
||||
- Namespace isolation: SHA256(sub)[:8]
|
||||
- TTL + expiry reaper
|
||||
- TEST_MODE
|
||||
- Lazy Environment creation / cleanup
|
||||
|
||||
+76
-3
@@ -311,7 +311,7 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if req.Entrypoint == "" {
|
||||
req.Entrypoint = "main.main"
|
||||
req.Entrypoint = defaultEntrypoint(req.Language)
|
||||
}
|
||||
if req.Route == "" {
|
||||
req.Route = "/" + req.Name
|
||||
@@ -358,7 +358,18 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
"buildcommand": "build",
|
||||
}
|
||||
} else {
|
||||
literal := base64.StdEncoding.EncodeToString([]byte(req.Code))
|
||||
var deployBytes []byte
|
||||
if req.Language == "nodejs" {
|
||||
zipBytes, zipErr := buildJSDeployZip(req.Code)
|
||||
if zipErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build nodejs archive: %v", zipErr))
|
||||
return
|
||||
}
|
||||
deployBytes = zipBytes
|
||||
} else {
|
||||
deployBytes = []byte(req.Code)
|
||||
}
|
||||
literal := base64.StdEncoding.EncodeToString(deployBytes)
|
||||
pkgSpec = map[string]any{
|
||||
"deployment": map[string]any{
|
||||
"type": "literal",
|
||||
@@ -390,6 +401,7 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
// Парсим TTL — если указан, записываем аннотацию на Function CRD.
|
||||
// reaper периодически читает эту аннотацию и удаляет протухшие функции + чистит environment если он больше не используется.
|
||||
fnAnnotations := map[string]any{}
|
||||
fnAnnotations["fission-console/language"] = req.Language
|
||||
if req.TTL != "" {
|
||||
expiresAt, ttlErr := parseTTL(req.TTL)
|
||||
if ttlErr != nil {
|
||||
@@ -669,6 +681,55 @@ func (s *server) buildGoSourceZip(code string) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func buildJSDeployZip(code string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
|
||||
// main.js — единственный файл: ESM wrapper + код пользователя инлайн через new Function
|
||||
// new Function безопасно изолирует module/exports от глобального контекста
|
||||
codeJSON, err := json.Marshal(code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal user code: %w", err)
|
||||
}
|
||||
wrapper := fmt.Sprintf(`const __mod = { exports: {} };
|
||||
(new Function('module', 'exports', %s))(__mod, __mod.exports);
|
||||
const _fn = __mod.exports;
|
||||
|
||||
export default async function(ctx) {
|
||||
const fn = typeof _fn === 'function' ? _fn : (_fn.default || _fn.handler || _fn.main);
|
||||
if (!fn) throw new Error('no exported function found in user code');
|
||||
const result = await fn(ctx);
|
||||
if (!result) return { status: 200, body: '' };
|
||||
if (typeof result.status !== 'undefined') return result;
|
||||
return { status: 200, ...result };
|
||||
}
|
||||
`, string(codeJSON))
|
||||
|
||||
fw, err := zw.Create("main.js")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := fw.Write([]byte(wrapper)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := zw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func defaultEntrypoint(lang string) string {
|
||||
switch lang {
|
||||
case "nodejs", "php", "ruby", "perl":
|
||||
return "main"
|
||||
case "go":
|
||||
return "Handler"
|
||||
default:
|
||||
return "main.main"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) handleGetFunction(w http.ResponseWriter, r *http.Request, name string) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
@@ -764,7 +825,19 @@ func (s *server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
literal := base64.StdEncoding.EncodeToString([]byte(req.Code))
|
||||
lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language")
|
||||
var deployBytes []byte
|
||||
if lang == "nodejs" {
|
||||
zipBytes, zipErr := buildJSDeployZip(req.Code)
|
||||
if zipErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build nodejs archive: %v", zipErr))
|
||||
return
|
||||
}
|
||||
deployBytes = zipBytes
|
||||
} else {
|
||||
deployBytes = []byte(req.Code)
|
||||
}
|
||||
literal := base64.StdEncoding.EncodeToString(deployBytes)
|
||||
if err := unstructured.SetNestedField(pkg.Object, literal, "spec", "deployment", "literal"); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set package literal: %v", err))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user