Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df74a69fde | ||
|
|
d7352532dc | ||
|
|
aea4e93ca1 | ||
|
|
f956b5fb18 | ||
|
|
1ccf8ff33d | ||
|
|
4348d291bc | ||
|
|
6d02c532d2 | ||
|
|
d18958467c | ||
|
|
f527aa1e54 | ||
|
|
9a23a03fc8 | ||
|
|
d15eb2405e | ||
|
|
4006cec7c7 | ||
|
|
2bbe5cea3b | ||
|
|
df1eb701f0 | ||
|
|
f8c4b3ebca | ||
|
|
2bc04d5bfd | ||
|
|
1610011f7f | ||
|
|
3251df8c09 | ||
|
|
05c15cbf28 | ||
|
|
72f73c5f63 | ||
|
|
e4d09b14da | ||
|
|
ea02e02059 | ||
|
|
c37d91cbfc | ||
|
|
2b97a3dbc6 | ||
|
|
f1b0369737 | ||
|
|
9995df60d6 | ||
|
|
41e93f3965 | ||
|
|
6bd96cb7c1 | ||
|
|
aad13ea2b7 | ||
|
|
33a421e189 |
@@ -5,15 +5,18 @@
|
||||
- Ничего лишнего.
|
||||
- Код — только по запросу.
|
||||
|
||||
## ⛔⛔⛔ ВОПРОС = СТОП
|
||||
|
||||
**Если в сообщении есть вопрос в ЛЮБОЙ форме** ("так ?", "верно ?", "почему ?", "как ?", "так же ?" и т.д.):
|
||||
1. ТОЛЬКО ответить на вопрос
|
||||
2. ОСТАНОВИТЬСЯ
|
||||
3. ЖДАТЬ следующей команды
|
||||
**ЗАПРЕЩЕНО** начинать работу, писать код, запускать команды — без явного "делай".
|
||||
|
||||
1. Не трогать рабочий код без явного указания.
|
||||
<<<<<<< HEAD
|
||||
2. Файлы редактировать локально — `~/remote_dev/fission` = `~/terra/fission` на ВМ (sshfs), SCP не нужен.
|
||||
Перед любым запуском команд проверять, что локальные файлы и содержимое на ВМ синхронизированы; при сомнении ориентироваться на состояние файлов на ВМ.
|
||||
3. Все команды — **только через SSH**, никогда локально:
|
||||
```bash
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
|
||||
```
|
||||
не надо без необходимости выдавать цепочки команд, не надо открывать интерактивные сессии, не надо запускать команды локально.
|
||||
4. **ЗАПРЕЩЕНО** откатывать код, версии, образы и рабочее состояние без явной команды пользователя.
|
||||
5. После каждого исправления делать отдельный commit через SSH.
|
||||
=======
|
||||
|
||||
2. Файлы редактируются локально:
|
||||
~/fission
|
||||
@@ -59,3 +62,4 @@
|
||||
- **ЗАПРЕЩЕНО:** обновлять манифесты, заливать образы, применять на кластер, запускать тесты
|
||||
|
||||
**Исключение:** только если приказ явно включает цепочку ("собери И залей И тесты")
|
||||
>>>>>>> d21e1ac (feat: restore cron page, email in navbar, metrics pipeline v1.3.19)
|
||||
|
||||
+2
-45
@@ -1,16 +1,5 @@
|
||||
# Правила работы агента
|
||||
|
||||
## ⛔⛔⛔ DOCKER — ОБЯЗАТЕЛЬНЫЙ ПОРЯДОК ПЕРЕД КАЖДЫМ BUILD
|
||||
|
||||
1. УВЕЛИЧИТЬ ТЕГ в `console/deploy/console.yaml` (vX.Y.Z → vX.Y.Z+1)
|
||||
2. rsync на ВМ
|
||||
3. ПРОВЕРИТЬ что файлы на ВМ новые (grep ключевой строки)
|
||||
4. docker build с НОВЫМ тегом
|
||||
5. docker push с НОВЫМ тегом
|
||||
6. kubectl apply (не rollout restart — apply подтягивает новый тег)
|
||||
|
||||
**НИКОГДА не делать `docker build` со старым тегом — под не перетянет образ (imagePullPolicy: IfNotPresent)**
|
||||
|
||||
## Файловая система (актуально)
|
||||
|
||||
1. Все файлы редактируются локально: `~/fission`
|
||||
@@ -49,11 +38,7 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=
|
||||
|
||||
## Git
|
||||
|
||||
⛔⛔⛔ АБСОЛЮТНОЕ ПРАВИЛО:
|
||||
- Git — ТОЛЬКО ЛОКАЛЬНО в `~/fission`. НИКОГДА через SSH на VM.
|
||||
- Разрешены ТОЛЬКО две операции: `git commit` и `git push`.
|
||||
- ЗАПРЕЩЕНО: git pull, git fetch, git rebase, git merge, git reset, git stash, git checkout — что угодно кроме commit и push.
|
||||
- Если push отклонён — СТОП, доложить пользователю. Не лезть в pull/merge/rebase самостоятельно.
|
||||
Коммитить и пушить через SSH после каждого завершённого этапа.
|
||||
|
||||
Версионирование тегами: `vMAJOR.MINOR.PATCH`
|
||||
- Patch — любое изменение кода
|
||||
@@ -64,37 +49,9 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=
|
||||
git tag vX.Y.Z && git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
## ⛔ ТЕРМИНАЛЬНЫЙ БУФЕР — НИКОГДА НЕ ЧИТАТЬ СТАРЫЙ
|
||||
|
||||
**АБСОЛЮТНОЕ ПРАВИЛО:**
|
||||
- get_terminal_output из старых сессий — МУСОР. Там старые прогоны.
|
||||
- Всегда запускать новую команду через SSH и читать её вывод напрямую.
|
||||
- НИКОГДА не читать буфер терминала из предыдущей сессии как актуальные данные.
|
||||
- Актуальный результат — только из команды, которая была запущена СЕЙЧАС.
|
||||
|
||||
## ⛔ ДОКУМЕНТАЦИЯ ТЕСТ-ПРОГОНОВ — В РЕАЛЬНОМ ВРЕМЕНИ
|
||||
|
||||
**Правила:**
|
||||
1. Перед запуском `run_all.sh` — создать файл `test-results/YYYY-MM-DD_HH-MM.log` и записать в него метку времени и что запускается.
|
||||
2. Запускать `run_all.sh 2>&1 | tee ~/terra/fission/test-results/YYYY-MM-DD_HH-MM.log` — вывод пишется сразу в файл и отображается в терминале.
|
||||
3. После завершения — rsync лога локально. Лог остаётся как документация.
|
||||
4. Папка `test-results/` в репозитории — `.gitignore` не добавлять, логи коммитить.
|
||||
|
||||
**Формат запуска:**
|
||||
```bash
|
||||
LOG="test-results/$(date +%Y-%m-%d_%H-%M).log"
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 \
|
||||
"bash ~/terra/fission/scripts/run_all.sh 2>&1 | tee ~/terra/fission/${LOG}"
|
||||
rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no" \
|
||||
naeel@5.172.178.213:~/terra/fission/test-results/ ~/fission/test-results/
|
||||
```
|
||||
|
||||
**Никогда не разбираться с результатами по памяти / буферу / чату. Только лог.**
|
||||
|
||||
## Поведение агента
|
||||
|
||||
- Не трогать рабочий код без явного указания
|
||||
- Не делать НИЧЕГО сверх того, о чём явно приказали — ни git-команд, ни rebase, ни дополнительных шагов
|
||||
- Если для продолжения нужен выбор — СПРОСИТЬ разрешения, не делать самостоятельно
|
||||
- Не делать ничего сверх того, о чём явно попросили
|
||||
- Деструктивные операции (`kubectl delete`, `rm -rf`, `terraform destroy` и др.) — только после явного подтверждения с указанием конкретных объектов
|
||||
- Отвечать кратко, без вступлений, извинений, благодарностей и прочей воды
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Batch LLM Explain Archive — тесты
|
||||
|
||||
## Методика
|
||||
- Для каждого архива из examples/ создаём .zip с кодом.
|
||||
- Отправляем на endpoint /console/api/ai/explain-archive.
|
||||
- Сохраняем prompt, ответ LLM, замечания.
|
||||
- Итеративно улучшаем prompt по результатам.
|
||||
|
||||
## Пример: hello-python
|
||||
|
||||
**main.py:**
|
||||
```
|
||||
def main():
|
||||
return "hello from fission via terraform"
|
||||
```
|
||||
|
||||
**Ответ LLM:**
|
||||
> Эта серверless‑функция состоит из единственной функции `main`, которая при вызове сразу возвращает строку `"hello from fission via terraform"`. По сути, она только выводит приветственное сообщение.
|
||||
|
||||
**Комментарий:**
|
||||
- Ответ корректный, краткий, по делу.
|
||||
@@ -1,25 +0,0 @@
|
||||
# go-hello.zip
|
||||
|
||||
**handler.go:**
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Handler — точка входа для Fission go-env.
|
||||
// Go builder компилирует этот файл в .so плагин,
|
||||
// go-env загружает его через plugin.Open().
|
||||
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintf(w, "Hello from real Go in Fission (builder pipeline)")
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ LLM:**
|
||||
> Эта функция — HTTP‑обработчик для серверлесс‑платформы Fission, написанный на Go. При получении любого запроса она отвечает простым текстом «Hello from real Go in Fission (builder pipeline)», устанавливая заголовок `Content-Type: text/plain`. Файл компилируется в плагин *.so, который затем загружается и вызывается Fission.
|
||||
|
||||
**Комментарий:**
|
||||
- Ответ корректный, упомянута специфика Fission (плагин .so), описание поведения точное.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,18 +0,0 @@
|
||||
# php-hello.zip
|
||||
|
||||
**main.php:**
|
||||
```php
|
||||
<?php
|
||||
function handler($context)
|
||||
{
|
||||
/** @var \Psr\Http\Message\ResponseInterface $response */
|
||||
$response = $context["response"];
|
||||
$response->getBody()->write("Hello from PHP in Fission");
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ LLM:**
|
||||
> Эта PHP‑функция выступает в роли обработчика запросов в Fission. Она получает объект ответа из контекста и записывает в его тело строку `Hello from PHP in Fission`. Таким образом при вызове функции клиент получает именно это приветственное сообщение.
|
||||
|
||||
**Комментарий:**
|
||||
- Ответ корректный, кратко и по делу, специфика Fission отражена.
|
||||
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
# python-cart.zip
|
||||
|
||||
**main.py:**
|
||||
```python
|
||||
import json
|
||||
import time
|
||||
from flask import request
|
||||
|
||||
# In-memory корзина (живёт пока жив под poolmgr)
|
||||
_CARTS: dict = {}
|
||||
|
||||
... (см. полный код в архиве)
|
||||
```
|
||||
|
||||
**Ответ LLM:**
|
||||
> Эта сервер‑лес функция реализует простую корзину покупок, хранящуюся в оперативной памяти под ключом `user_id`. При `GET` возвращает текущее содержимое корзины, подсчитанный итог и количество товаров. `POST` принимает JSON с данными товара (id, название, цена, количество) и добавляет его в корзину, увеличивая количество, если такой товар уже есть. `DELETE` удаляет из корзины товар по `product_id`. Для остальных HTTP‑методов возвращается ошибка 405.
|
||||
|
||||
**Комментарий:**
|
||||
- Ответ LLM очень точный, описывает все основные ветки логики, даже не видя весь код (ограничение по длине prompt).
|
||||
- LLM корректно понял in-memory storage и работу с методами.
|
||||
Binary file not shown.
@@ -1,16 +0,0 @@
|
||||
# ruby-hello.zip
|
||||
|
||||
**main.rb:**
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
def handler
|
||||
"Hello from Ruby in Fission"
|
||||
end
|
||||
```
|
||||
|
||||
**Ответ LLM:**
|
||||
> Функция `handler` просто возвращает строку `"Hello from Ruby in Fission"`. Она не принимает входных параметров и не выполняет никакой логики, кроме возврата фиксированного сообщения. Таким образом, её цель – продемонстрировать работу простого Ruby‑обработчика в среде Fission.
|
||||
|
||||
**Комментарий:**
|
||||
- Ответ корректный, кратко и по делу, специфика Fission отражена.
|
||||
Binary file not shown.
@@ -37,8 +37,16 @@ func main() {
|
||||
log.Fatalf("create dynamic client: %v", err)
|
||||
}
|
||||
|
||||
testMode := os.Getenv("FISSION_TEST_MODE") == "true"
|
||||
|
||||
var jwtAuth auth.Authenticator
|
||||
if testMode {
|
||||
jwtAuth = &auth.TestAuthenticator{}
|
||||
} else {
|
||||
jwtAuth = auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil)
|
||||
}
|
||||
authenticator := &auth.MultiAuthenticator{
|
||||
JWT: auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil),
|
||||
JWT: jwtAuth,
|
||||
Demo: &auth.DemoAuthenticator{},
|
||||
}
|
||||
|
||||
@@ -51,7 +59,7 @@ func main() {
|
||||
SATokenPath: envDefault("SA_TOKEN_PATH", "/var/run/secrets/kubernetes.io/serviceaccount/token"),
|
||||
AuthUser: envDefault("FISSION_AUTH_USERNAME", ""),
|
||||
AuthPass: envDefault("FISSION_AUTH_PASSWORD", ""),
|
||||
StoragesvcURL: envDefault("FISSION_STORAGESVC_URL", "http://storagesvc.fission.svc.cluster.local"),
|
||||
TestMode: testMode,
|
||||
Authenticator: authenticator,
|
||||
// --- ai/ask feature ---
|
||||
LLMUrl: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"),
|
||||
@@ -62,7 +70,7 @@ func main() {
|
||||
// Запускаем фоновые горутины: reaper истёкших функций
|
||||
nsm := srv.NSManager()
|
||||
nsm.StartExpiryReaper(envDurationDefault("REAPER_INTERVAL", 5*time.Minute))
|
||||
nsm.StartNSReconciler(envDurationDefault("NS_RECONCILE_INTERVAL", 30*time.Second))
|
||||
// StartNSReconciler удалён — за FISSION_RESOURCE_NAMESPACES теперь отвечает Layer 1 NSWatcher
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: ":" + port,
|
||||
|
||||
@@ -52,7 +52,7 @@ spec:
|
||||
serviceAccountName: fission-console
|
||||
containers:
|
||||
- name: console
|
||||
image: naeel/fission-console:v1.3.45
|
||||
image: naeel/fission-console:v1.3.27
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8090
|
||||
@@ -77,8 +77,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: router
|
||||
key: password
|
||||
- name: FISSION_STORAGESVC_URL
|
||||
value: "http://storagesvc.fission.svc.cluster.local"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
|
||||
@@ -50,13 +50,28 @@ func normalizeEnv(env string) string {
|
||||
}
|
||||
|
||||
// authMiddleware оборачивает handler аутентификацией.
|
||||
// Токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом.
|
||||
//
|
||||
// В testMode: заголовок X-Test-Sub позволяет задать sub напрямую (без токена).
|
||||
// Иначе: токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом.
|
||||
func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var identity auth.UserIdentity
|
||||
|
||||
env := normalizeEnv(r.Header.Get("X-Auth-Env"))
|
||||
|
||||
if s.testMode {
|
||||
if sub := strings.TrimSpace(r.Header.Get("X-Test-Sub")); sub != "" {
|
||||
identity = auth.UserIdentity{Sub: sub}
|
||||
ctx := s.contextWithIdentity(r.Context(), identity)
|
||||
if err := s.nsManager.EnsureUserNS(ctx, auth.NamespaceForSub(sub)); err != nil {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure namespace: %v", err))
|
||||
return
|
||||
}
|
||||
h(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
token := authTokenFromRequest(r)
|
||||
var err error
|
||||
identity, err = s.authenticator.Authenticate(r.Context(), token, env)
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxExplainCodeBytes — суммарный лимит кода из архива для отправки в LLM.
|
||||
// LLM бесплатный и слабый — не перегружаем контекст.
|
||||
maxExplainCodeBytes = 20 * 1024
|
||||
)
|
||||
|
||||
// handleExplainArchive — POST /console/api/ai/explain-archive
|
||||
// Принимает zip-архив, извлекает код, отправляет в LLM с вопросом "что делает?".
|
||||
func (s *Server) handleExplainArchive(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
if s.llmKey == "" {
|
||||
writeJSONError(w, http.StatusServiceUnavailable, "AI-ассистент не настроен (FISSION_LLM_KEY не задан)")
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 10*1024*1024)
|
||||
if err := r.ParseMultipartForm(maxArchiveBytes); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "ошибка разбора multipart: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
f, _, err := r.FormFile("archive")
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "поле 'archive' обязательно")
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.ReadFrom(f); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "ошибка чтения архива: "+err.Error())
|
||||
return
|
||||
}
|
||||
if buf.Len() > maxArchiveBytes {
|
||||
writeJSONError(w, http.StatusRequestEntityTooLarge,
|
||||
fmt.Sprintf("архив слишком большой: максимум %d KB", maxArchiveBytes/1024))
|
||||
return
|
||||
}
|
||||
|
||||
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "не удалось открыть zip: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Поддерживаемые расширения
|
||||
supported := map[string]bool{".py": true, ".js": true, ".rb": true, ".php": true, ".go": true}
|
||||
|
||||
type fileChunk struct {
|
||||
name string
|
||||
code string
|
||||
}
|
||||
var chunks []fileChunk
|
||||
var totalBytes int
|
||||
|
||||
for _, zf := range zr.File {
|
||||
if zf.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := strings.ToLower(archiveFileExt(zf.Name))
|
||||
if !supported[ext] {
|
||||
continue
|
||||
}
|
||||
if totalBytes >= maxExplainCodeBytes {
|
||||
break
|
||||
}
|
||||
rc, err := zf.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
limit := maxExplainCodeBytes - totalBytes
|
||||
content, err := io.ReadAll(io.LimitReader(rc, int64(limit)))
|
||||
rc.Close()
|
||||
if err != nil || len(content) == 0 {
|
||||
continue
|
||||
}
|
||||
chunks = append(chunks, fileChunk{name: zf.Name, code: string(content)})
|
||||
totalBytes += len(content)
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
writeJSONError(w, http.StatusBadRequest, "нет поддерживаемых файлов (.py, .js, .rb, .php, .go)")
|
||||
return
|
||||
}
|
||||
|
||||
// Собираем текст для LLM
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Ниже приведены файлы из архива serverless-функции. Опиши КРАТКО (2-5 предложений) что делает эта функция.\n\n")
|
||||
for _, c := range chunks {
|
||||
sb.WriteString(fmt.Sprintf("=== %s ===\n%s\n\n", c.name, c.code))
|
||||
}
|
||||
prompt := sb.String()
|
||||
|
||||
llmReqBody := llmChatRequest{
|
||||
Model: "gpt-oss-120b",
|
||||
Messages: []llmMessage{
|
||||
{Role: "system", Content: "Ты ассистент для анализа serverless-функций. Отвечай кратко, на русском языке."},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
MaxTokens: 512,
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(llmReqBody)
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
llmReq, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
strings.TrimRight(s.llmURL, "/")+"/chat/completions",
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
llmReq.Header.Set("Content-Type", "application/json")
|
||||
llmReq.Header.Set("Authorization", "Bearer "+s.llmKey)
|
||||
|
||||
resp, err := s.http.Do(llmReq)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadGateway, "LLM недоступен: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var llmResp struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 64*1024)).Decode(&llmResp); err != nil {
|
||||
writeJSONError(w, http.StatusBadGateway, "parse error: "+err.Error())
|
||||
return
|
||||
}
|
||||
if llmResp.Error != nil {
|
||||
writeJSONError(w, http.StatusBadGateway, llmResp.Error.Message)
|
||||
return
|
||||
}
|
||||
if len(llmResp.Choices) == 0 {
|
||||
writeJSONError(w, http.StatusBadGateway, "пустой ответ от LLM")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"answer": strings.TrimSpace(llmResp.Choices[0].Message.Content),
|
||||
})
|
||||
}
|
||||
|
||||
// archiveFileExt возвращает расширение файла из пути внутри zip (включая точку).
|
||||
func archiveFileExt(name string) string {
|
||||
for i := len(name) - 1; i >= 0 && name[i] != '/'; i-- {
|
||||
if name[i] == '.' {
|
||||
return name[i:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -36,12 +35,6 @@ var validFuncName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
|
||||
// Выше — не имеет смысла для inline функции; лучше использовать Package с URL.
|
||||
const maxCodeSize = 1 << 20
|
||||
|
||||
// maxArchiveUploadSize — максимальный размер zip-архива при загрузке функции (32 MB).
|
||||
const maxArchiveUploadSize = 32 << 20
|
||||
|
||||
// fissionSourceTypeAnnotation — аннотация на Function, хранит тип источника: "code" или "archive".
|
||||
const fissionSourceTypeAnnotation = "fission-console/source-type"
|
||||
|
||||
// defaultFunctionInvokeTimeout совпадает с дефолтом Fission для spec.functionTimeout.
|
||||
const defaultFunctionInvokeTimeout = 60 * time.Second
|
||||
|
||||
@@ -58,102 +51,6 @@ func buildDeployArchive(lang, code string) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// deleteFromStoragesvc удаляет архив из S3 через storagesvc по URL из Package spec.
|
||||
// URL имеет формат: http://storagesvc.../v1/archive?id=fission/UUID
|
||||
// Best-effort: ошибка логируется, но не прерывает операцию удаления.
|
||||
func (s *Server) deleteFromStoragesvc(ctx context.Context, archiveURL string) {
|
||||
if s.storagesvcURL == "" || archiveURL == "" {
|
||||
return
|
||||
}
|
||||
// archiveURL = "http://storagesvc.../v1/archive?id=fission/UUID"
|
||||
// Строим DELETE URL к storagesvc, сохраняя query-параметр id
|
||||
parsed, err := url.Parse(archiveURL)
|
||||
if err != nil {
|
||||
log.Printf("deleteFromStoragesvc: parse url %q: %v", archiveURL, err)
|
||||
return
|
||||
}
|
||||
deleteURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?" + parsed.RawQuery
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteURL, nil)
|
||||
if err != nil {
|
||||
log.Printf("deleteFromStoragesvc: build request: %v", err)
|
||||
return
|
||||
}
|
||||
resp, err := s.http.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("deleteFromStoragesvc: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("deleteFromStoragesvc: status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
return
|
||||
}
|
||||
log.Printf("deleteFromStoragesvc: deleted %s", parsed.Query().Get("id"))
|
||||
}
|
||||
|
||||
// uploadToStoragesvc загружает байты в Fission storagesvc и возвращает URL для package archive.
|
||||
// Если storagesvcURL не задан — возвращает пустую строку (fallback на literal).
|
||||
func (s *Server) uploadToStoragesvc(ctx context.Context, data []byte) (string, error) {
|
||||
if s.storagesvcURL == "" {
|
||||
log.Printf("uploadToStoragesvc: storagesvcURL is empty, skip upload")
|
||||
return "", nil
|
||||
}
|
||||
log.Printf("uploadToStoragesvc: uploading %d bytes to %s", len(data), s.storagesvcURL)
|
||||
uploadURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive"
|
||||
body := &bytes.Reader{}
|
||||
// multipart/form-data с полем uploadfile
|
||||
var buf bytes.Buffer
|
||||
boundary := fmt.Sprintf("fission%d", time.Now().UnixNano())
|
||||
buf.WriteString("--" + boundary + "\r\n")
|
||||
buf.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"uploadfile\"; filename=\"archive.zip\"\r\n"))
|
||||
buf.WriteString("Content-Type: application/octet-stream\r\n\r\n")
|
||||
buf.Write(data)
|
||||
buf.WriteString("\r\n--" + boundary + "--\r\n")
|
||||
body = bytes.NewReader(buf.Bytes())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("build storagesvc upload request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
||||
req.Header.Set("X-File-Size", fmt.Sprintf("%d", len(data)))
|
||||
|
||||
resp, err := s.http.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("storagesvc upload: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return "", fmt.Errorf("storagesvc upload status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
||||
}
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil || result.ID == "" {
|
||||
return "", fmt.Errorf("storagesvc upload: bad response: %s", string(respBody))
|
||||
}
|
||||
archiveURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?id=" + result.ID
|
||||
return archiveURL, nil
|
||||
}
|
||||
|
||||
// buildDeploySpec строит spec.deployment для Fission Package.
|
||||
// Если storagesvcURL задан — загружает архив в S3 через storagesvc и возвращает type: url.
|
||||
// Иначе — возвращает type: literal с base64-кодом.
|
||||
func (s *Server) buildDeploySpec(ctx context.Context, data []byte) (map[string]any, error) {
|
||||
archiveURL, err := s.uploadToStoragesvc(ctx, data)
|
||||
if err != nil {
|
||||
log.Printf("storagesvc upload failed, falling back to literal: %v", err)
|
||||
// fallback — сохраняем как literal
|
||||
return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil
|
||||
}
|
||||
if archiveURL == "" {
|
||||
return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil
|
||||
}
|
||||
return map[string]any{"type": "url", "url": archiveURL}, nil
|
||||
}
|
||||
|
||||
func (s *Server) resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration {
|
||||
if fn != nil {
|
||||
seconds, found, err := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout")
|
||||
@@ -310,12 +207,6 @@ func (s *Server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// /functions/:name/archive — обновление через zip-архив
|
||||
if len(parts) == 2 && parts[1] == "archive" && r.Method == http.MethodPut {
|
||||
s.handleUpdateFunctionArchive(w, r, name)
|
||||
return
|
||||
}
|
||||
|
||||
// /functions/:name/invoke — вызов функции
|
||||
if len(parts) == 2 && parts[1] == "invoke" && r.Method == http.MethodPost {
|
||||
s.handleInvokeFunction(w, r, name)
|
||||
@@ -333,13 +224,6 @@ func (s *Server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
ns := s.userNS(r)
|
||||
|
||||
// Поддерживаем два формата: JSON (код) и multipart/form-data (архив).
|
||||
isArchiveUpload := strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data")
|
||||
if isArchiveUpload {
|
||||
s.handleCreateFunctionFromArchive(w, r, ns)
|
||||
return
|
||||
}
|
||||
|
||||
var req model.CreateFunctionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("decode request: %v", err))
|
||||
@@ -429,7 +313,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
// Строим Package spec в зависимости от языка:
|
||||
// - Go: source package → builder job компилирует в .so плагин
|
||||
// - Node.js: deployment zip с ESM wrapper (package.json + main.js)
|
||||
// - Остальные: deployment archive с кодом (S3 или literal fallback)
|
||||
// - Остальные: deployment literal с кодом напрямую
|
||||
var pkgSpec map[string]any
|
||||
if req.Language == "go" {
|
||||
srcZip, err := runtime.BuildGoSourceZip(req.Code)
|
||||
@@ -437,13 +321,12 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("build go source archive: %v", err))
|
||||
return
|
||||
}
|
||||
srcSpec, srcErr := s.buildDeploySpec(ctx, srcZip)
|
||||
if srcErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload go source: %v", srcErr))
|
||||
return
|
||||
}
|
||||
literal := base64.StdEncoding.EncodeToString(srcZip)
|
||||
pkgSpec = map[string]any{
|
||||
"source": srcSpec,
|
||||
"source": map[string]any{
|
||||
"type": "literal",
|
||||
"literal": literal,
|
||||
},
|
||||
"deployment": map[string]any{},
|
||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||
"buildcommand": "build",
|
||||
@@ -454,13 +337,8 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr))
|
||||
return
|
||||
}
|
||||
deploySpec, uploadErr := s.buildDeploySpec(ctx, deployBytes)
|
||||
if uploadErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload %s archive: %v", req.Language, uploadErr))
|
||||
return
|
||||
}
|
||||
pkgSpec = map[string]any{
|
||||
"deployment": deploySpec,
|
||||
"deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
|
||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||
"source": map[string]any{},
|
||||
}
|
||||
@@ -475,8 +353,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Парсим TTL ДО создания K8s ресурсов — невалидный TTL не оставляет мусор
|
||||
fnAnnotations := map[string]any{
|
||||
"fission-console/language": req.Language,
|
||||
fissionSourceTypeAnnotation: "code",
|
||||
"fission-console/language": req.Language,
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
fnAnnotations[functionCreatedAtAnnotation] = now.Format(time.RFC3339)
|
||||
@@ -610,14 +487,6 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
|
||||
}
|
||||
}
|
||||
|
||||
// Читаем source-type аннотацию (code / archive)
|
||||
sourceType := "code"
|
||||
if ann := fn.GetAnnotations(); ann != nil {
|
||||
if v := ann[fissionSourceTypeAnnotation]; v != "" {
|
||||
sourceType = v
|
||||
}
|
||||
}
|
||||
|
||||
writeAnyJSON(w, http.StatusOK, map[string]any{
|
||||
"name": name,
|
||||
"namespace": ns,
|
||||
@@ -628,7 +497,6 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
|
||||
"created_at": functionTimestampResponse(fn)["created_at"],
|
||||
"updated_at": functionTimestampResponse(fn)["updated_at"],
|
||||
"code": code,
|
||||
"source_type": sourceType,
|
||||
"route": route,
|
||||
"methods": methods,
|
||||
"raw": fn.Object,
|
||||
@@ -695,17 +563,12 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
|
||||
}()
|
||||
now := time.Now().UTC()
|
||||
newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36)
|
||||
deploySpec, uploadErr := s.buildDeploySpec(ctx, deployBytes)
|
||||
if uploadErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload %s archive: %v", lang, uploadErr))
|
||||
return
|
||||
}
|
||||
newPkg := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "Package",
|
||||
"metadata": map[string]any{"name": newPkgName, "namespace": ns},
|
||||
"spec": map[string]any{
|
||||
"deployment": deploySpec,
|
||||
"deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
|
||||
"environment": map[string]any{"name": envName, "namespace": ns},
|
||||
"source": map[string]any{},
|
||||
},
|
||||
@@ -1137,22 +1000,10 @@ func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, na
|
||||
}
|
||||
|
||||
if pkgName != "" {
|
||||
// Получаем URL архива из Package spec.deployment перед удалением, чтобы потом очистить S3
|
||||
var archiveURL string
|
||||
if pkg, pkgErr := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Get(ctx, pkgName, metav1.GetOptions{}); pkgErr == nil {
|
||||
deployType, _, _ := unstructured.NestedString(pkg.Object, "spec", "deployment", "type")
|
||||
if deployType == "url" {
|
||||
archiveURL, _, _ = unstructured.NestedString(pkg.Object, "spec", "deployment", "url")
|
||||
}
|
||||
}
|
||||
if err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete package %q: %v", pkgName, err))
|
||||
return
|
||||
}
|
||||
// Удаляем архив из S3 после успешного удаления Package (best-effort)
|
||||
if archiveURL != "" {
|
||||
go s.deleteFromStoragesvc(context.Background(), archiveURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Убираем environment pool pods если язык больше не используется (best-effort)
|
||||
@@ -1246,287 +1097,3 @@ func normalizeMethods(in []string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// handleCreateFunctionFromArchive создаёт функцию из загруженного zip-архива (multipart/form-data).
|
||||
// Поля формы: name, language (или environment), entrypoint, route, methods, timeout, ttl.
|
||||
// Файловое поле: archive (.zip).
|
||||
func (s *Server) handleCreateFunctionFromArchive(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
if err := r.ParseMultipartForm(maxArchiveUploadSize); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("parse multipart form: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
if name == "" || (!validFuncName.MatchString(name) || len(name) > 57) {
|
||||
writeJSONError(w, http.StatusBadRequest, "invalid function name: must match ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ and be <= 57 chars")
|
||||
return
|
||||
}
|
||||
|
||||
lang := strings.TrimSpace(r.FormValue("language"))
|
||||
envName := strings.TrimSpace(r.FormValue("environment"))
|
||||
|
||||
f, _, err := r.FormFile("archive")
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("archive file required: %v", err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
archiveBytes, err := io.ReadAll(io.LimitReader(f, maxArchiveUploadSize))
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("read archive: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
nsCtx, nsCancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer nsCancel()
|
||||
if err := s.nsManager.EnsureUserNS(nsCtx, ns); err != nil {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure namespace: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Определяем environment: по языку или явно
|
||||
if lang != "" {
|
||||
envCtx, envCancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer envCancel()
|
||||
resolved, envErr := fission.EnsureEnvironment(envCtx, s.dyn, ns, lang)
|
||||
if envErr != nil {
|
||||
if strings.Contains(envErr.Error(), "unsupported language") {
|
||||
writeJSONError(w, http.StatusBadRequest, envErr.Error())
|
||||
} else {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure environment: %v", envErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
envName = resolved
|
||||
}
|
||||
if envName == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "language or environment is required")
|
||||
return
|
||||
}
|
||||
if _, err := s.dyn.Resource(fission.EnvironmentGVR).Namespace(ns).Get(ctx, envName, metav1.GetOptions{}); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("environment %q not found: %v", envName, err))
|
||||
return
|
||||
}
|
||||
|
||||
entrypoint := strings.TrimSpace(r.FormValue("entrypoint"))
|
||||
if entrypoint == "" {
|
||||
entrypoint = runtime.DefaultEntrypoint(lang)
|
||||
}
|
||||
route := strings.TrimSpace(r.FormValue("route"))
|
||||
if route == "" {
|
||||
nsShort := ns
|
||||
if len(nsShort) > 12 {
|
||||
nsShort = nsShort[len(nsShort)-12:]
|
||||
}
|
||||
route = "/" + nsShort + "/" + name
|
||||
}
|
||||
if !strings.HasPrefix(route, "/") {
|
||||
route = "/" + route
|
||||
}
|
||||
methods := normalizeMethods(strings.Split(r.FormValue("methods"), ","))
|
||||
timeout := normalizeFunctionTimeout(0)
|
||||
if tv := r.FormValue("timeout"); tv != "" {
|
||||
if n, err := strconv.ParseInt(tv, 10, 64); err == nil {
|
||||
timeout = normalizeFunctionTimeout(n)
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем архив в storagesvc
|
||||
deploySpec, uploadErr := s.buildDeploySpec(ctx, archiveBytes)
|
||||
if uploadErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload archive: %v", uploadErr))
|
||||
return
|
||||
}
|
||||
|
||||
pkgName := name + "-pkg"
|
||||
triggerName := name + "-route"
|
||||
|
||||
pkg := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "Package",
|
||||
"metadata": map[string]any{"name": pkgName, "namespace": ns},
|
||||
"spec": map[string]any{
|
||||
"deployment": deploySpec,
|
||||
"environment": map[string]any{"name": envName, "namespace": ns},
|
||||
"source": map[string]any{},
|
||||
},
|
||||
}}
|
||||
|
||||
now := time.Now().UTC()
|
||||
fnAnnotations := map[string]any{
|
||||
"fission-console/language": lang,
|
||||
fissionSourceTypeAnnotation: "archive",
|
||||
functionCreatedAtAnnotation: now.Format(time.RFC3339),
|
||||
functionUpdatedAtAnnotation: now.Format(time.RFC3339),
|
||||
}
|
||||
if ttl := r.FormValue("ttl"); ttl != "" {
|
||||
if expiresAt, ttlErr := parseTTL(ttl); ttlErr == nil {
|
||||
fnAnnotations["fission-console/expires-at"] = expiresAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
|
||||
methodValues := make([]any, 0, len(methods))
|
||||
for _, m := range methods {
|
||||
methodValues = append(methodValues, m)
|
||||
}
|
||||
|
||||
fn := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "Function",
|
||||
"metadata": map[string]any{"name": name, "namespace": ns, "annotations": fnAnnotations},
|
||||
"spec": map[string]any{
|
||||
"environment": map[string]any{"name": envName, "namespace": ns},
|
||||
"package": map[string]any{"packageref": map[string]any{"name": pkgName, "namespace": ns}},
|
||||
"functionTimeout": timeout,
|
||||
},
|
||||
}}
|
||||
if entrypoint != "" {
|
||||
_ = unstructured.SetNestedField(fn.Object, entrypoint, "spec", "package", "functionName")
|
||||
}
|
||||
|
||||
trigger := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "HTTPTrigger",
|
||||
"metadata": map[string]any{"name": triggerName, "namespace": ns},
|
||||
"spec": map[string]any{
|
||||
"functionref": map[string]any{"name": name, "type": "name"},
|
||||
"relativeurl": route,
|
||||
"methods": methodValues,
|
||||
},
|
||||
}}
|
||||
|
||||
if _, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Create(ctx, pkg, metav1.CreateOptions{}); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
writeJSONError(w, http.StatusConflict, fmt.Sprintf("function %q already exists", name))
|
||||
return
|
||||
}
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create package: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Create(ctx, fn, metav1.CreateOptions{}); err != nil {
|
||||
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{})
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create function: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err := s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).Create(ctx, trigger, metav1.CreateOptions{}); err != nil {
|
||||
_ = s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{})
|
||||
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{})
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create trigger: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
writeAnyJSON(w, http.StatusCreated, map[string]any{
|
||||
"name": name,
|
||||
"namespace": ns,
|
||||
"environment": envName,
|
||||
"route": route,
|
||||
"source_type": "archive",
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdateFunctionArchive обновляет функцию из загруженного zip-архива (multipart/form-data).
|
||||
// Поля формы: timeout (optional). Файловое поле: archive (.zip).
|
||||
func (s *Server) handleUpdateFunctionArchive(w http.ResponseWriter, r *http.Request, name string) {
|
||||
if err := r.ParseMultipartForm(maxArchiveUploadSize); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("parse multipart form: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
f, _, err := r.FormFile("archive")
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("archive file required: %v", err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
archiveBytes, err := io.ReadAll(io.LimitReader(f, maxArchiveUploadSize))
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("read archive: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||
defer cancel()
|
||||
ns := s.userNS(r)
|
||||
|
||||
fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
status := http.StatusBadGateway
|
||||
if apierrors.IsNotFound(err) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeJSONError(w, status, fmt.Sprintf("get function %q: %v", name, err))
|
||||
return
|
||||
}
|
||||
|
||||
oldPkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
|
||||
envName, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name")
|
||||
|
||||
newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36)
|
||||
deploySpec, uploadErr := s.buildDeploySpec(ctx, archiveBytes)
|
||||
if uploadErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload archive: %v", uploadErr))
|
||||
return
|
||||
}
|
||||
|
||||
newPkg := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "Package",
|
||||
"metadata": map[string]any{"name": newPkgName, "namespace": ns},
|
||||
"spec": map[string]any{
|
||||
"deployment": deploySpec,
|
||||
"environment": map[string]any{"name": envName, "namespace": ns},
|
||||
"source": map[string]any{},
|
||||
},
|
||||
}}
|
||||
|
||||
createdPkg, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Create(ctx, newPkg, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create new package: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем timeout если задан
|
||||
timeout := normalizeFunctionTimeout(0)
|
||||
if tv := r.FormValue("timeout"); tv != "" {
|
||||
if n, err := strconv.ParseInt(tv, 10, 64); err == nil {
|
||||
timeout = normalizeFunctionTimeout(n)
|
||||
}
|
||||
}
|
||||
_ = unstructured.SetNestedField(fn.Object, timeout, "spec", "functionTimeout")
|
||||
|
||||
fnAnnotations := fn.GetAnnotations()
|
||||
if fnAnnotations == nil {
|
||||
fnAnnotations = map[string]string{}
|
||||
}
|
||||
fnAnnotations[fissionSourceTypeAnnotation] = "archive"
|
||||
fnAnnotations[functionUpdatedAtAnnotation] = time.Now().UTC().Format(time.RFC3339)
|
||||
fn.SetAnnotations(fnAnnotations)
|
||||
|
||||
if err := unstructured.SetNestedField(fn.Object, map[string]any{
|
||||
"name": newPkgName,
|
||||
"namespace": ns,
|
||||
"resourceversion": createdPkg.GetResourceVersion(),
|
||||
}, "spec", "package", "packageref"); err != nil {
|
||||
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set packageref: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Update(ctx, fn, metav1.UpdateOptions{}); err != nil {
|
||||
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{})
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("update function: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if oldPkgName != "" && oldPkgName != newPkgName {
|
||||
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, oldPkgName, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
writeAnyJSON(w, http.StatusOK, map[string]any{
|
||||
"updated": true,
|
||||
"package": newPkgName,
|
||||
"source_type": "archive",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxArchiveBytes — максимальный размер zip-архива (100 KB).
|
||||
// Serverless-функции не должны быть большими — только скрипты и небольшие зависимости.
|
||||
maxArchiveBytes = 100 * 1024
|
||||
|
||||
// maxUnzippedBytes — ограничение на суммарный распакованный размер (защита от zip bomb).
|
||||
// 100 KB реального кода — достаточно для любой serverless-функции.
|
||||
maxUnzippedBytes = 100 * 1024
|
||||
)
|
||||
|
||||
// lintResult — результат проверки одного файла.
|
||||
type lintResult struct {
|
||||
File string `json:"file"`
|
||||
OK bool `json:"ok"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
// handleLintArchive — POST /console/api/ai/lint-archive
|
||||
// Принимает zip-архив (multipart/form-data, поле "archive"),
|
||||
// прогоняет линтер по всем файлам с известным расширением,
|
||||
// возвращает список результатов по файлам.
|
||||
func (s *Server) handleLintArchive(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
// Hard-limit 10 MB чтобы соединение не обрывалось (MaxBytesReader с маленьким лимитом
|
||||
// закрывает соединение до отправки ответа — клиент получает 000 вместо 413).
|
||||
// Реальный лимит размера архива проверяется после чтения файла.
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 10*1024*1024)
|
||||
|
||||
if err := r.ParseMultipartForm(maxArchiveBytes); err != nil {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
writeJSONError(w, http.StatusRequestEntityTooLarge,
|
||||
fmt.Sprintf("архив слишком большой: максимум %d KB", maxArchiveBytes/1024))
|
||||
} else {
|
||||
writeJSONError(w, http.StatusBadRequest, "ошибка разбора multipart: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
f, _, err := r.FormFile("archive")
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "поле 'archive' обязательно")
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Читаем архив в память
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.ReadFrom(f); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "ошибка чтения архива: "+err.Error())
|
||||
return
|
||||
}
|
||||
if buf.Len() > maxArchiveBytes {
|
||||
writeJSONError(w, http.StatusRequestEntityTooLarge,
|
||||
fmt.Sprintf("архив слишком большой: %d байт, максимум %d KB", buf.Len(), maxArchiveBytes/1024))
|
||||
return
|
||||
}
|
||||
|
||||
// Открываем zip из памяти
|
||||
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "не удалось открыть zip: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Считаем суммарный распакованный размер (защита от zip bomb)
|
||||
var totalUnzipped uint64
|
||||
for _, zf := range zr.File {
|
||||
totalUnzipped += zf.UncompressedSize64
|
||||
}
|
||||
if totalUnzipped > maxUnzippedBytes {
|
||||
writeJSONError(w, http.StatusRequestEntityTooLarge,
|
||||
fmt.Sprintf("суммарный размер распакованных файлов превышает %d KB", maxUnzippedBytes/1024))
|
||||
return
|
||||
}
|
||||
|
||||
// Карта линтеров по расширению
|
||||
type linterCfg struct {
|
||||
cmd []string
|
||||
stdinOk bool // поддерживает stdin
|
||||
stdinArg string // аргумент для stdin-режима (имя "файла")
|
||||
}
|
||||
linters := map[string]linterCfg{
|
||||
".py": {cmd: []string{"python3", "-m", "py_compile"}, stdinOk: false},
|
||||
".js": {cmd: []string{"node", "--check"}, stdinOk: false},
|
||||
".rb": {cmd: []string{"ruby", "-c"}, stdinOk: true},
|
||||
".php": {cmd: []string{"php", "-l"}, stdinOk: false},
|
||||
}
|
||||
|
||||
var results []lintResult
|
||||
var hasError bool
|
||||
|
||||
for _, zf := range zr.File {
|
||||
if zf.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(zf.Name))
|
||||
cfg, supported := linters[ext]
|
||||
if !supported {
|
||||
continue
|
||||
}
|
||||
|
||||
// Читаем содержимое файла из zip в память
|
||||
rc, err := zf.Open()
|
||||
if err != nil {
|
||||
results = append(results, lintResult{File: zf.Name, OK: false, Output: "ошибка чтения: " + err.Error()})
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
var content bytes.Buffer
|
||||
_, readErr := content.ReadFrom(rc)
|
||||
rc.Close()
|
||||
if readErr != nil {
|
||||
results = append(results, lintResult{File: zf.Name, OK: false, Output: "ошибка чтения: " + readErr.Error()})
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
var res lintResult
|
||||
res.File = zf.Name
|
||||
|
||||
// Записываем во временный файл в /dev/shm (RAM) если доступен, иначе os.TempDir()
|
||||
tmpDir := "/dev/shm"
|
||||
if _, statErr := os.Stat(tmpDir); statErr != nil {
|
||||
tmpDir = ""
|
||||
}
|
||||
tmpf, tmpErr := os.CreateTemp(tmpDir, "fission-lint-*"+ext)
|
||||
if tmpErr != nil {
|
||||
res.OK = false
|
||||
res.Output = "не удалось создать tmp: " + tmpErr.Error()
|
||||
results = append(results, res)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
tmpName := tmpf.Name()
|
||||
_, writeErr := tmpf.Write(content.Bytes())
|
||||
tmpf.Close()
|
||||
|
||||
if writeErr != nil {
|
||||
os.Remove(tmpName)
|
||||
res.OK = false
|
||||
res.Output = "ошибка записи tmp: " + writeErr.Error()
|
||||
results = append(results, res)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
args := append(cfg.cmd, tmpName) //nolint:gocritic — намеренно создаём новый слайс
|
||||
//nolint:gosec — cmd содержит только захардкоженные команды из linters
|
||||
out, lintErr := exec.CommandContext(ctx, args[0], args[1:]...).CombinedOutput()
|
||||
cancel()
|
||||
os.Remove(tmpName)
|
||||
|
||||
outStr := strings.TrimSpace(string(out))
|
||||
// Скрываем путь к tmp-файлу от пользователя
|
||||
outStr = strings.ReplaceAll(outStr, tmpName, zf.Name)
|
||||
|
||||
if lintErr == nil {
|
||||
res.OK = true
|
||||
} else {
|
||||
res.OK = false
|
||||
hasError = true
|
||||
if outStr != "" {
|
||||
res.Output = outStr
|
||||
} else {
|
||||
res.Output = fmt.Sprintf("ошибка синтаксиса (%v)", lintErr)
|
||||
}
|
||||
}
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
writeJSONError(w, http.StatusBadRequest, "архив не содержит файлов поддерживаемых языков (.py, .js, .rb, .php)")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": !hasError,
|
||||
"results": results,
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -37,10 +36,10 @@ type Server struct {
|
||||
|
||||
saTokenPath string
|
||||
invokeTimeout time.Duration
|
||||
testMode bool // FISSION_TEST_MODE=true — разрешает X-Test-Sub shortcut
|
||||
|
||||
authUser string
|
||||
authPass string
|
||||
storagesvcURL string // URL Fission storagesvc для загрузки архивов кода в S3
|
||||
authUser string
|
||||
authPass string
|
||||
|
||||
// --- ai/ask feature (удалить блок целиком чтобы выкосить) ---
|
||||
llmURL string // FISSION_LLM_URL
|
||||
@@ -69,7 +68,7 @@ type Config struct {
|
||||
SATokenPath string
|
||||
AuthUser string
|
||||
AuthPass string
|
||||
StoragesvcURL string
|
||||
TestMode bool
|
||||
Authenticator auth.Authenticator // слой аутентификации
|
||||
LLMUrl string
|
||||
LLMKey string
|
||||
@@ -77,7 +76,6 @@ type Config struct {
|
||||
|
||||
// NewServer создаёт и настраивает HTTP Server со всеми зависимостями.
|
||||
func NewServer(cfg Config) *Server {
|
||||
log.Printf("NewServer: storagesvcURL=%q", cfg.StoragesvcURL)
|
||||
return &Server{
|
||||
dyn: cfg.Dyn,
|
||||
ns: cfg.Namespace,
|
||||
@@ -87,7 +85,7 @@ func NewServer(cfg Config) *Server {
|
||||
invokeTimeout: cfg.InvokeTimeout,
|
||||
authUser: cfg.AuthUser,
|
||||
authPass: cfg.AuthPass,
|
||||
storagesvcURL: cfg.StoragesvcURL,
|
||||
testMode: cfg.TestMode,
|
||||
authenticator: cfg.Authenticator,
|
||||
llmURL: cfg.LLMUrl,
|
||||
llmKey: cfg.LLMKey,
|
||||
@@ -156,8 +154,6 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/console/api/ns/status", auth(s.handleNSStatus))
|
||||
mux.HandleFunc("/console/api/ns/debug", auth(s.handleNSDebug))
|
||||
mux.HandleFunc("/console/api/ai/check", auth(s.handleAICheck))
|
||||
mux.HandleFunc("/console/api/ai/lint-archive", auth(s.handleLintArchive))
|
||||
mux.HandleFunc("/console/api/ai/explain-archive", auth(s.handleExplainArchive))
|
||||
// --- ai/ask feature (удалить строку чтобы выкосить роут) ---
|
||||
mux.HandleFunc("/console/api/ai/ask", auth(s.handleAIAsk))
|
||||
// --- end ai/ask feature ---
|
||||
|
||||
@@ -25,14 +25,10 @@ type UserIdentity struct {
|
||||
Email string // email — опционально, для отображения в UI
|
||||
}
|
||||
|
||||
// NamespaceForSub вычисляет детерминированный K8s namespace из Sub.
|
||||
// Если sub начинается с "test-" → "fission-test-" + hex(SHA256(sub)[:8])
|
||||
// Иначе → "fission-" + hex(SHA256(sub)[:8])
|
||||
// NamespaceForSub вычисляет детерминированный K8s namespace из Sub:
|
||||
// "fission-" + hex(SHA256(sub)[:8])
|
||||
func NamespaceForSub(sub string) string {
|
||||
h := sha256.Sum256([]byte(sub))
|
||||
if strings.HasPrefix(sub, "test-") {
|
||||
return "fission-test-" + hex.EncodeToString(h[:8])
|
||||
}
|
||||
return "fission-" + hex.EncodeToString(h[:8])
|
||||
}
|
||||
|
||||
@@ -223,13 +219,7 @@ func (d *DemoAuthenticator) Authenticate(_ context.Context, login, _ string) (Us
|
||||
if len([]rune(login)) < 6 {
|
||||
return UserIdentity{}, fmt.Errorf("login must be at least 6 characters")
|
||||
}
|
||||
sub := uuidV5(login)
|
||||
// Если логин начинается с "test-" — помечаем Sub так же, чтобы
|
||||
// NamespaceForSub мог создать fission-test-* namespace.
|
||||
if strings.HasPrefix(login, "test-") {
|
||||
sub = "test-" + sub
|
||||
}
|
||||
return UserIdentity{Sub: sub, Email: demoEmail(login)}, nil
|
||||
return UserIdentity{Sub: uuidV5(login), Email: demoEmail(login)}, nil
|
||||
}
|
||||
|
||||
// --- MultiAuthenticator ---
|
||||
|
||||
@@ -3,8 +3,6 @@ package cloud
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,7 +10,6 @@ import (
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"fission-console/internal/fission"
|
||||
@@ -143,171 +140,6 @@ func (m *NSManager) provisionNamespace(ctx context.Context, ns string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var deployGVR = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
|
||||
|
||||
// fissionSystemNS возвращает namespace где живут компоненты Fission (router, executor, ...).
|
||||
func fissionSystemNS() string {
|
||||
if ns := os.Getenv("FISSION_SYSTEM_NAMESPACE"); ns != "" {
|
||||
return ns
|
||||
}
|
||||
return "fission"
|
||||
}
|
||||
|
||||
// StartNSReconciler запускает фоновый goroutine который синхронизирует FISSION_RESOURCE_NAMESPACES
|
||||
// в router и executor со всеми реально существующими пользовательскими namespace-ами.
|
||||
//
|
||||
// Почему reconciler, а не прямой патч в EnsureUserNS:
|
||||
// - Прямой патч → rolling restart при каждом новом логине → cold start для всех
|
||||
// - Reconciler батчит изменения, не делает лишних патчей если список не изменился
|
||||
// - Автоматически убирает ghost namespace-ы (удалённые вручную через kubectl)
|
||||
func (m *NSManager) StartNSReconciler(interval time.Duration) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
log.Printf("cloud.NSReconciler: started, interval=%v", interval)
|
||||
for range ticker.C {
|
||||
m.reconcileNSList()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// reconcileNSList синхронизирует FISSION_RESOURCE_NAMESPACES в router и executor.
|
||||
// Читает все Active namespace-ы с меткой managed-by=fission-console,
|
||||
// сравнивает с текущим значением в router, патчит только если есть разница.
|
||||
func (m *NSManager) reconcileNSList() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sysNS := fissionSystemNS()
|
||||
|
||||
// Шаг 1: реальные namespace-ы с нашей меткой (только Active)
|
||||
nsList, err := m.dyn.Resource(fission.NamespaceGVR).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: "managed-by=fission-console",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("cloud.NSReconciler: list namespaces: %v", err)
|
||||
return
|
||||
}
|
||||
desired := map[string]struct{}{"default": {}}
|
||||
for _, ns := range nsList.Items {
|
||||
phase, _, _ := unstructured.NestedString(ns.Object, "status", "phase")
|
||||
if phase == "Active" {
|
||||
desired[ns.GetName()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Шаг 2: текущее значение из router (источник истины)
|
||||
routerDep, err := m.dyn.Resource(deployGVR).Namespace(sysNS).Get(ctx, "router", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Printf("cloud.NSReconciler: get router deployment: %v", err)
|
||||
return
|
||||
}
|
||||
currentVal := currentFissionResourceNS(routerDep)
|
||||
|
||||
// Шаг 3: сравниваем
|
||||
currentSet := map[string]struct{}{}
|
||||
for _, p := range strings.Split(currentVal, ",") {
|
||||
if t := strings.TrimSpace(p); t != "" {
|
||||
currentSet[t] = struct{}{}
|
||||
}
|
||||
}
|
||||
same := len(currentSet) == len(desired)
|
||||
if same {
|
||||
for k := range desired {
|
||||
if _, ok := currentSet[k]; !ok {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if same {
|
||||
return // ничего не изменилось — нет патча, нет rolling restart
|
||||
}
|
||||
|
||||
// Шаг 4: строим новое значение
|
||||
parts := make([]string, 0, len(desired))
|
||||
for ns := range desired {
|
||||
parts = append(parts, ns)
|
||||
}
|
||||
sort.Strings(parts)
|
||||
newVal := strings.Join(parts, ",")
|
||||
|
||||
// Шаг 5: патчим router и executor
|
||||
for _, depName := range []string{"router", "executor"} {
|
||||
if patchErr := m.patchDeployFissionNS(ctx, sysNS, depName, newVal); patchErr != nil {
|
||||
log.Printf("cloud.NSReconciler: patch %s: %v", depName, patchErr)
|
||||
}
|
||||
}
|
||||
log.Printf("cloud.NSReconciler: synced FISSION_RESOURCE_NAMESPACES: %q → %q", currentVal, newVal)
|
||||
}
|
||||
|
||||
// currentFissionResourceNS читает текущий FISSION_RESOURCE_NAMESPACES из deployment.
|
||||
func currentFissionResourceNS(dep *unstructured.Unstructured) string {
|
||||
containers, _, _ := unstructured.NestedSlice(dep.Object, "spec", "template", "spec", "containers")
|
||||
for _, c := range containers {
|
||||
cont, ok := c.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
envs, _, _ := unstructured.NestedSlice(cont, "env")
|
||||
for _, e := range envs {
|
||||
env, ok := e.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if env["name"] == "FISSION_RESOURCE_NAMESPACES" {
|
||||
if v, ok := env["value"].(string); ok && v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
// patchDeployFissionNS обновляет FISSION_RESOURCE_NAMESPACES в указанном deployment.
|
||||
func (m *NSManager) patchDeployFissionNS(ctx context.Context, sysNS, depName, newVal string) error {
|
||||
dep, err := m.dyn.Resource(deployGVR).Namespace(sysNS).Get(ctx, depName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containers, _, _ := unstructured.NestedSlice(dep.Object, "spec", "template", "spec", "containers")
|
||||
if len(containers) == 0 {
|
||||
return nil
|
||||
}
|
||||
cont, ok := containers[0].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
envs, _, _ := unstructured.NestedSlice(cont, "env")
|
||||
updated := false
|
||||
for i, e := range envs {
|
||||
env, ok := e.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if env["name"] == "FISSION_RESOURCE_NAMESPACES" {
|
||||
if env["value"] != newVal {
|
||||
env["value"] = newVal
|
||||
envs[i] = env
|
||||
updated = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !updated {
|
||||
envs = append(envs, map[string]any{"name": "FISSION_RESOURCE_NAMESPACES", "value": newVal})
|
||||
}
|
||||
cont["env"] = envs
|
||||
containers[0] = cont
|
||||
if err := unstructured.SetNestedSlice(dep.Object, containers, "spec", "template", "spec", "containers"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = m.dyn.Resource(deployGVR).Namespace(sysNS).Update(ctx, dep, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
// StartExpiryReaper запускает фоновый goroutine для удаления функций с истёкшим TTL.
|
||||
// Interval — как часто проверять. Рекомендуемое значение: 5 минут.
|
||||
func (m *NSManager) StartExpiryReaper(interval time.Duration) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -38,20 +37,16 @@ import (
|
||||
// (не ClusterRoleBinding) безопасен — даёт полный доступ только внутри NS.
|
||||
func SetupFissionNamespace(ctx context.Context, dyn dynamic.Interface, ns string) error {
|
||||
// 1. Namespace
|
||||
labels := map[string]any{
|
||||
"managed-by": "fission-console",
|
||||
"fission.io/managed": "true", // Layer 1: executor NSWatcher auto-discovers this NS
|
||||
}
|
||||
if strings.HasPrefix(ns, "fission-test-") {
|
||||
labels["fission-console/env"] = "test"
|
||||
}
|
||||
nsObj := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Namespace",
|
||||
"metadata": map[string]any{
|
||||
"name": ns,
|
||||
"labels": labels,
|
||||
"name": ns,
|
||||
"labels": map[string]any{
|
||||
"managed-by": "fission-console",
|
||||
"fission.io/managed": "true", // Layer 1: executor NSWatcher auto-discovers this NS
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+19
-42
@@ -100,7 +100,7 @@
|
||||
<div class="nubes">NUBES</div>
|
||||
<div class="product">FISSION CONSOLE</div>
|
||||
</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.45</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.27</div>
|
||||
</div>
|
||||
<div class="row" style="margin:0;">
|
||||
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
||||
@@ -209,28 +209,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex; gap:4px; margin-bottom:6px;">
|
||||
<button class="btn ghost" id="c-mode-code" onclick="setCodeMode('c','code')" style="font-size:.8rem; padding:3px 10px;">✏️ Код</button>
|
||||
<button class="btn ghost" id="c-mode-archive" onclick="setCodeMode('c','archive')" style="font-size:.8rem; padding:3px 10px;">📦 Архив (.zip)</button>
|
||||
</div>
|
||||
<div id="c-code-area">
|
||||
<textarea id="c-code">def main():
|
||||
<label>Код</label>
|
||||
<textarea id="c-code">def main():
|
||||
return {"ok": True, "msg": "hello from fission console"}
|
||||
</textarea>
|
||||
<div style="margin-top:6px; display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<button class="btn ghost" id="c-ai-btn" onclick="aiCheck('c-code','c-lang','c-ai-result')">🔍 Проверить
|
||||
синтаксис линтером</button>
|
||||
<button class="btn ghost" id="c-gen-btn" onclick="showGenPrompt()">✨ Сгенерировать код LLM</button>
|
||||
<button class="btn ghost" id="c-exp-btn" onclick="aiExplain('c-code','c-lang','c-ai-result')">📖 LLM:
|
||||
Что делает?</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="c-archive-area" style="display:none;">
|
||||
<input type="file" id="c-archive-file" accept=".zip" style="display:block; margin-bottom:8px; color:var(--text-primary);">
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<button id="c-lint-btn" class="btn ghost" onclick="lintArchiveFile('c')">🔍 Проверка архива линтером</button>
|
||||
<button id="c-explain-archive-btn" class="btn ghost" onclick="explainArchiveFile('c')">📖 LLM: Что делает?</button>
|
||||
</div>
|
||||
<div style="margin-top:6px; display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<button class="btn ghost" id="c-ai-btn" onclick="aiCheck('c-code','c-lang','c-ai-result')">🔍 Проверить
|
||||
синтаксис линтером</button>
|
||||
<button class="btn ghost" id="c-gen-btn" onclick="showGenPrompt()">✨ Сгенерировать код LLM</button>
|
||||
<button class="btn ghost" id="c-exp-btn" onclick="aiExplain('c-code','c-lang','c-ai-result')">📖 LLM:
|
||||
Что
|
||||
делает?</button>
|
||||
</div>
|
||||
<div id="c-gen-prompt" style="display:none; margin-top:8px; display:none; gap:6px; align-items:center;">
|
||||
<input id="c-gen-desc" type="text"
|
||||
@@ -290,26 +279,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex; gap:4px; margin-bottom:6px;">
|
||||
<button class="btn ghost" id="e-mode-code" onclick="setCodeMode('e','code')" style="font-size:.8rem; padding:3px 10px;">✏️ Код</button>
|
||||
<button class="btn ghost" id="e-mode-archive" onclick="setCodeMode('e','archive')" style="font-size:.8rem; padding:3px 10px;">📦 Архив (.zip)</button>
|
||||
</div>
|
||||
<div id="e-code-area">
|
||||
<label>Код</label>
|
||||
<textarea id="e-code"></textarea>
|
||||
<div style="margin-top:6px; display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<button class="btn ghost" id="e-ai-btn" onclick="aiCheck('e-code','e-lang-hidden','e-ai-result')">🔍
|
||||
Проверить синтаксис линтером</button>
|
||||
<button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">📖
|
||||
LLM: Что делает?</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="e-archive-area" style="display:none;">
|
||||
<input type="file" id="e-archive-file" accept=".zip" style="display:block; margin-bottom:8px; color:var(--text-primary);">
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<button id="e-lint-btn" class="btn ghost" onclick="lintArchiveFile('e')">🔍 Проверка архива линтером</button>
|
||||
<button id="e-explain-archive-btn" class="btn ghost" onclick="explainArchiveFile('e')">📖 LLM: Что делает?</button>
|
||||
</div>
|
||||
<label>Код</label>
|
||||
<textarea id="e-code"></textarea>
|
||||
<div style="margin-top:6px; display:flex; gap:6px; flex-wrap:wrap;">
|
||||
<button class="btn ghost" id="e-ai-btn" onclick="aiCheck('e-code','e-lang-hidden','e-ai-result')">🔍
|
||||
Проверить синтаксис линтером</button>
|
||||
<button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">📖
|
||||
LLM:
|
||||
Что делает?</button>
|
||||
</div>
|
||||
<div id="e-ai-result"
|
||||
style="display:none;margin-top:8px;padding:10px 12px;border-radius:6px;font-size:13px;line-height:1.5;white-space:pre-wrap;font-family:monospace;">
|
||||
@@ -400,7 +377,7 @@
|
||||
</div>
|
||||
|
||||
<div class="actions" style="justify-content:space-between; align-items:center;">
|
||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.45</span>
|
||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.27</span>
|
||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-30
@@ -1,12 +1,6 @@
|
||||
/* app.js — основная логика: загрузка данных, таблица функций */
|
||||
|
||||
async function reloadAll() {
|
||||
const progress = startTimedStatus('Загрузка...', 'Загрузка данных', null);
|
||||
|
||||
// Показываем таблицу сразу с placeholder'ом
|
||||
document.getElementById('fn-rows').innerHTML =
|
||||
'<tr><td colspan="9" style="color:var(--text-secondary); font-style:italic;">⏳ Загружаем функции...</td></tr>';
|
||||
|
||||
try {
|
||||
const [envs, pkgs, fns, http, times] = await Promise.all([
|
||||
getJSON(API_BASE + '/environments'),
|
||||
@@ -29,7 +23,7 @@ async function reloadAll() {
|
||||
return t && t.spec && t.spec.functionref && t.spec.functionref.name;
|
||||
}).filter(Boolean)).size);
|
||||
|
||||
function makeFnRow(f) {
|
||||
const rows = (fns || []).map(function (f) {
|
||||
const spec = f.spec || {};
|
||||
const meta = f.metadata || {};
|
||||
const ann = meta.annotations || {};
|
||||
@@ -63,33 +57,14 @@ async function reloadAll() {
|
||||
'<td>' + cronCell + '</td>' +
|
||||
'<td class="nowrap">' + actions + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
}).join('');
|
||||
|
||||
var fnList = fns || [];
|
||||
if (fnList.length === 0) {
|
||||
progress.stop('Загружено: нет функций', '');
|
||||
document.getElementById('fn-rows').innerHTML = rows || '<tr><td colspan="9">Нет функций</td></tr>';
|
||||
if (!rows) {
|
||||
document.getElementById('fn-rows').innerHTML = '<tr><td colspan="9">Нет функций</td></tr>';
|
||||
} else {
|
||||
// Рендерим первую строку сразу — убираем placeholder
|
||||
var tbody = document.getElementById('fn-rows');
|
||||
tbody.innerHTML = makeFnRow(fnList[0]);
|
||||
// Остальные добавляем по одной с небольшой задержкой
|
||||
var i = 1;
|
||||
function appendNext() {
|
||||
if (i >= fnList.length) {
|
||||
progress.stop('Загружено ' + fnList.length + ' функций', 'ok');
|
||||
return;
|
||||
}
|
||||
var tr = document.createElement('tbody');
|
||||
tr.innerHTML = makeFnRow(fnList[i]);
|
||||
tbody.appendChild(tr.firstChild);
|
||||
i++;
|
||||
setTimeout(appendNext, 40);
|
||||
}
|
||||
setTimeout(appendNext, 40);
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('fn-rows').innerHTML = '<tr><td colspan="9">Load error: ' + e.message + '</td></tr>';
|
||||
progress.stop('Ошибка загрузки: ' + e.message, 'err');
|
||||
showStatus('Ошибка загрузки: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
+13
-180
@@ -1,127 +1,5 @@
|
||||
/* functions.js — CRUD операции с функциями */
|
||||
|
||||
// setCodeMode переключает режим формы между вводом кода и загрузкой архива.
|
||||
// prefix: 'c' (create) или 'e' (edit)
|
||||
// mode: 'code' | 'archive'
|
||||
function setCodeMode(prefix, mode) {
|
||||
var codeArea = document.getElementById(prefix + '-code-area');
|
||||
var archiveArea = document.getElementById(prefix + '-archive-area');
|
||||
var btnCode = document.getElementById(prefix + '-mode-code');
|
||||
var btnArchive = document.getElementById(prefix + '-mode-archive');
|
||||
if (!codeArea || !archiveArea) return;
|
||||
|
||||
var isCode = mode === 'code';
|
||||
codeArea.style.display = isCode ? '' : 'none';
|
||||
archiveArea.style.display = isCode ? 'none' : '';
|
||||
if (btnCode) btnCode.style.opacity = isCode ? '1' : '0.5';
|
||||
if (btnArchive) btnArchive.style.opacity = isCode ? '0.5' : '1';
|
||||
}
|
||||
|
||||
// lintArchiveFile — отправляет zip-архив на /console/api/ai/lint-archive,
|
||||
// показывает результат пофайлово в блоке {prefix}-ai-result рядом с кнопкой.
|
||||
function lintArchiveFile(prefix) {
|
||||
var input = document.getElementById(prefix + '-archive-file');
|
||||
var resEl = document.getElementById(prefix + '-ai-result');
|
||||
var btn = document.getElementById(prefix + '-lint-btn');
|
||||
function showRes(text, bg, color) {
|
||||
if (resEl) {
|
||||
resEl.style.display = 'block';
|
||||
resEl.style.background = bg;
|
||||
resEl.style.color = color;
|
||||
resEl.textContent = text;
|
||||
}
|
||||
}
|
||||
if (!input || !input.files || !input.files[0]) {
|
||||
showRes('Выберите .zip файл', '#3a1a1a', '#f88');
|
||||
return;
|
||||
}
|
||||
var file = input.files[0];
|
||||
if (file.size > 100 * 1024) {
|
||||
showRes('Архив слишком большой: максимум 100 KB', '#3a1a1a', '#f88');
|
||||
return;
|
||||
}
|
||||
if (btn) { btn.disabled = true; btn.textContent = '⏳ Проверяю…'; }
|
||||
showRes('Запускаю линтер…', 'var(--bg-alt)', 'var(--fg)');
|
||||
var fd = new FormData();
|
||||
fd.append('archive', file);
|
||||
fetch(API_BASE + '/ai/lint-archive', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔍 Проверка архива линтером'; }
|
||||
if (data.error) {
|
||||
showRes('Ошибка: ' + data.error, '#3a1a1a', '#f88');
|
||||
return;
|
||||
}
|
||||
var results = data.results || [];
|
||||
if (results.length === 0) {
|
||||
showRes('Нет поддерживаемых файлов в архиве (.py, .js, .rb, .php)', '#3a2a00', '#ffa');
|
||||
return;
|
||||
}
|
||||
var ok = results.filter(function(r) { return r.ok; }).length;
|
||||
var fail = results.filter(function(r) { return !r.ok; }).length;
|
||||
var lines = results.map(function(r) {
|
||||
return (r.ok ? '✅ ' : '❌ ') + r.file + (r.output ? '\n ' + r.output : '');
|
||||
});
|
||||
var summary = ok + ' OK, ' + fail + ' ошибок\n\n' + lines.join('\n');
|
||||
showRes(summary, fail > 0 ? '#3a1a1a' : '#1a3a1a', fail > 0 ? '#f88' : '#8f8');
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔍 Проверка архива линтером'; }
|
||||
showRes('Ошибка сети: ' + e.message, '#3a2a00', '#ffa');
|
||||
});
|
||||
}
|
||||
|
||||
// explainArchiveFile — отправляет zip-архив на /console/api/ai/explain-archive,
|
||||
// показывает ответ LLM в блоке {prefix}-ai-result.
|
||||
function explainArchiveFile(prefix) {
|
||||
var input = document.getElementById(prefix + '-archive-file');
|
||||
var resEl = document.getElementById(prefix + '-ai-result');
|
||||
var btn = document.getElementById(prefix + '-explain-archive-btn');
|
||||
function showRes(text, bg, color) {
|
||||
if (resEl) {
|
||||
resEl.style.display = 'block';
|
||||
resEl.style.background = bg;
|
||||
resEl.style.color = color;
|
||||
resEl.textContent = text;
|
||||
}
|
||||
}
|
||||
if (!input || !input.files || !input.files[0]) {
|
||||
showRes('Выберите .zip файл', '#3a1a1a', '#f88');
|
||||
return;
|
||||
}
|
||||
var file = input.files[0];
|
||||
if (file.size > 100 * 1024) {
|
||||
showRes('Архив слишком большой: максимум 100 KB', '#3a1a1a', '#f88');
|
||||
return;
|
||||
}
|
||||
if (btn) { btn.disabled = true; btn.textContent = '⏳ Спрашиваю LLM…'; }
|
||||
showRes('Запрашиваю у LLM…', 'var(--bg-alt)', 'var(--fg)');
|
||||
var fd = new FormData();
|
||||
fd.append('archive', file);
|
||||
fetch(API_BASE + '/ai/explain-archive', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '📖 LLM: Что делает?'; }
|
||||
if (data.error) {
|
||||
showRes('Ошибка: ' + data.error, '#3a2a00', '#ffa');
|
||||
return;
|
||||
}
|
||||
showRes(data.answer || '(пустой ответ)', '#1a2a3a', '#8cf');
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '📖 LLM: Что делает?'; }
|
||||
showRes('Ошибка сети: ' + e.message, '#3a2a00', '#ffa');
|
||||
});
|
||||
}
|
||||
|
||||
function parseMethods(v) {
|
||||
const items = String(v || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
|
||||
return items.length ? Array.from(new Set(items)) : ['GET'];
|
||||
@@ -167,34 +45,15 @@ async function submitCreate() {
|
||||
const lang = document.getElementById('c-lang').value.trim();
|
||||
if (!lang) throw new Error('language is required');
|
||||
|
||||
// Определяем режим: archiveArea видна → archive mode
|
||||
var archiveArea = document.getElementById('c-archive-area');
|
||||
var isArchiveMode = archiveArea && archiveArea.style.display !== 'none';
|
||||
var archiveFile = isArchiveMode ? document.getElementById('c-archive-file').files[0] : null;
|
||||
|
||||
if (isArchiveMode && archiveFile) {
|
||||
// Отправляем multipart/form-data с архивом
|
||||
var fd = new FormData();
|
||||
fd.append('name', name);
|
||||
fd.append('language', lang);
|
||||
fd.append('entrypoint', document.getElementById('c-entry').value.trim());
|
||||
fd.append('route', document.getElementById('c-route').value.trim());
|
||||
fd.append('methods', document.getElementById('c-methods').value.trim());
|
||||
fd.append('timeout', String(parseTimeout(document.getElementById('c-timeout').value)));
|
||||
fd.append('archive', archiveFile);
|
||||
var resp = await fetch(API_BASE + '/functions', { method: 'POST', headers: authHeaders(), body: fd });
|
||||
if (!resp.ok) { var e = await resp.json().catch(() => ({})); throw new Error(e.error || resp.statusText); }
|
||||
} else {
|
||||
await requestJSON(API_BASE + '/functions', 'POST', {
|
||||
name: name,
|
||||
language: lang,
|
||||
entrypoint: document.getElementById('c-entry').value.trim(),
|
||||
route: document.getElementById('c-route').value.trim(),
|
||||
methods: parseMethods(document.getElementById('c-methods').value),
|
||||
timeout: parseTimeout(document.getElementById('c-timeout').value),
|
||||
code: document.getElementById('c-code').value
|
||||
});
|
||||
}
|
||||
await requestJSON(API_BASE + '/functions', 'POST', {
|
||||
name: name,
|
||||
language: lang,
|
||||
entrypoint: document.getElementById('c-entry').value.trim(),
|
||||
route: document.getElementById('c-route').value.trim(),
|
||||
methods: parseMethods(document.getElementById('c-methods').value),
|
||||
timeout: parseTimeout(document.getElementById('c-timeout').value),
|
||||
code: document.getElementById('c-code').value
|
||||
});
|
||||
|
||||
try {
|
||||
await syncScheduleForFunction(name, 'c');
|
||||
@@ -227,16 +86,6 @@ async function openEdit(name) {
|
||||
document.getElementById('e-entry').value = fn.entrypoint || '';
|
||||
document.getElementById('e-timeout').value = String(fn.timeout || 60);
|
||||
document.getElementById('e-code').value = fn.code || '';
|
||||
|
||||
// Сбрасываем режим: source_type из API (code / archive)
|
||||
if (fn.source_type === 'archive') {
|
||||
setCodeMode('e', 'archive');
|
||||
} else {
|
||||
setCodeMode('e', 'code');
|
||||
}
|
||||
var archiveInfo = document.getElementById('e-archive-info');
|
||||
if (archiveInfo) archiveInfo.textContent = '';
|
||||
|
||||
var schedule = timeTriggerByFn(name);
|
||||
document.getElementById('e-schedule-enabled').checked = !!schedule;
|
||||
document.getElementById('e-cron').value = (schedule && schedule.spec && schedule.spec.cron) || '';
|
||||
@@ -273,26 +122,10 @@ async function submitEdit() {
|
||||
const progress = startTimedStatus('Сохраняем код...', 'Сохранение кода...', explainDelay);
|
||||
try {
|
||||
const name = S.currentEdit.name;
|
||||
|
||||
// Определяем режим
|
||||
var archiveArea = document.getElementById('e-archive-area');
|
||||
var isArchiveMode = archiveArea && archiveArea.style.display !== 'none';
|
||||
var archiveFile = isArchiveMode ? document.getElementById('e-archive-file').files[0] : null;
|
||||
|
||||
if (isArchiveMode && archiveFile) {
|
||||
// Обновляем через архив
|
||||
var fd = new FormData();
|
||||
fd.append('timeout', String(parseTimeout(document.getElementById('e-timeout').value)));
|
||||
fd.append('archive', archiveFile);
|
||||
var resp = await fetch(API_BASE + '/functions/' + encodeURIComponent(name) + '/archive',
|
||||
{ method: 'PUT', headers: authHeaders(), body: fd });
|
||||
if (!resp.ok) { var e = await resp.json().catch(() => ({})); throw new Error(e.error || resp.statusText); }
|
||||
} else {
|
||||
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', {
|
||||
code: document.getElementById('e-code').value,
|
||||
timeout: parseTimeout(document.getElementById('e-timeout').value)
|
||||
});
|
||||
}
|
||||
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', {
|
||||
code: document.getElementById('e-code').value,
|
||||
timeout: parseTimeout(document.getElementById('e-timeout').value)
|
||||
});
|
||||
|
||||
await syncScheduleForFunction(name, 'e');
|
||||
closeEdit();
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
import requests
|
||||
|
||||
def main(event, context):
|
||||
try:
|
||||
r = requests.get('https://httpbin.org/get', timeout=3)
|
||||
return {
|
||||
"statusCode": 200,
|
||||
"body": f"Demo OK: {r.status_code}, {r.json().get('url')}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"statusCode": 500,
|
||||
"body": f"Error: {str(e)}"
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
# Archive Upload и Edit Modal — описание логики (v1.3.45, 2026-05-04)
|
||||
|
||||
## Контекст
|
||||
|
||||
До v1.3.45 функции создавались только через ввод кода (JSON поле `code`).
|
||||
Edit modal при открытии не сбрасывал режим (code/archive) — показывал то что было
|
||||
выбрано ранее. Создание и обновление через zip-архив не поддерживалось вовсе.
|
||||
|
||||
---
|
||||
|
||||
## 1. Аннотация `fission-console/source-type`
|
||||
|
||||
Добавлена новая аннотация на Kubernetes-объекте `Function`:
|
||||
|
||||
```
|
||||
fission-console/source-type: "code" | "archive"
|
||||
```
|
||||
|
||||
Ставится в момент создания функции и обновляется при каждом сохранении.
|
||||
Хранится в `metadata.annotations` Function объекта в K8s — персистентно.
|
||||
|
||||
**Зачем:** позволяет при открытии edit modal знать каким способом была создана
|
||||
функция, без анализа содержимого Package (которое не даёт надёжного ответа).
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend — создание функции
|
||||
|
||||
### 2a. Определение режима (handlers.go `handleCreateFunction`)
|
||||
|
||||
```go
|
||||
isArchiveUpload := strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data")
|
||||
if isArchiveUpload {
|
||||
s.handleCreateFunctionFromArchive(w, r, ns)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
Если Content-Type начинается с `multipart/form-data` — передаём в новый хендлер.
|
||||
Иначе — старый JSON-путь без изменений.
|
||||
|
||||
### 2b. Создание через JSON-код (без изменений, кроме аннотации)
|
||||
|
||||
В `fnAnnotations` добавлено:
|
||||
```go
|
||||
fissionSourceTypeAnnotation: "code",
|
||||
```
|
||||
Все старые функции (созданные до v1.3.45) не имеют этой аннотации — API вернёт
|
||||
`source_type: "code"` по умолчанию.
|
||||
|
||||
### 2c. Создание через архив (`handleCreateFunctionFromArchive`)
|
||||
|
||||
**Вход:** `multipart/form-data` с полями:
|
||||
- `name`, `language`, `environment`, `entrypoint`, `route`, `methods`, `timeout`, `ttl`
|
||||
- `archive` — файл `.zip`
|
||||
|
||||
**Алгоритм:**
|
||||
1. `r.ParseMultipartForm(32MB)` — парсим форму
|
||||
2. Валидация `name` по regexp `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`, max 57 символов
|
||||
3. Если задан `language` — через `fission.EnsureEnvironment` создаём/получаем Environment
|
||||
4. `r.FormFile("archive")` → `io.ReadAll(io.LimitReader(f, 32MB))` — читаем архив
|
||||
5. `s.buildDeploySpec(ctx, archiveBytes)` — загружаем в Fission storagesvc (S3), получаем
|
||||
`{type: "url", url: "http://storagesvc/.../archive?id=..."}` или fallback на literal
|
||||
6. Создаём Package с `spec.deployment = deploySpec`
|
||||
7. Создаём Function с аннотацией `fission-console/source-type: archive`
|
||||
8. Создаём HTTPTrigger
|
||||
9. При ошибке на шаге 7 или 8 — откат (удаляем уже созданные объекты)
|
||||
|
||||
**Ответ:** `201 Created` с JSON `{name, namespace, environment, route, source_type: "archive"}`
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend — обновление функции через архив
|
||||
|
||||
### Новый endpoint: `PUT /console/api/functions/:name/archive`
|
||||
|
||||
Зарегистрирован в `handleFunctionsAction`:
|
||||
```go
|
||||
if len(parts) == 2 && parts[1] == "archive" && r.Method == http.MethodPut {
|
||||
s.handleUpdateFunctionArchive(w, r, name)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### `handleUpdateFunctionArchive` — алгоритм:
|
||||
|
||||
1. Парсим multipart form (32MB лимит)
|
||||
2. `r.FormFile("archive")` → читаем байты (лимит 32MB)
|
||||
3. Получаем текущую Function из K8s
|
||||
4. Запоминаем `oldPkgName` (для удаления после замены)
|
||||
5. Генерируем `newPkgName = name + "-pkg-" + base36(unixMs)` — уникальное имя
|
||||
6. Загружаем архив в storagesvc → создаём новый Package
|
||||
7. Обновляем Function: `spec.package.packageref.name = newPkgName`
|
||||
8. Обновляем аннотации: `source-type: archive`, `updated-at: now`
|
||||
9. Обновляем `spec.functionTimeout` если передан `timeout`
|
||||
10. Update Function в K8s
|
||||
11. Удаляем старый Package (best-effort, ошибки игнорируем)
|
||||
|
||||
**Почему создаём новый Package вместо обновления старого:**
|
||||
Executor кэширует function service по `functionUid`. Изменение содержимого
|
||||
существующего Package не сбрасывает кэш. Новое имя пакета гарантирует cache miss.
|
||||
(Та же логика что в `handleUpdateFunctionCode`.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Backend — GET /functions/:name
|
||||
|
||||
Добавлено поле `source_type` в ответ:
|
||||
|
||||
```go
|
||||
sourceType := "code" // default для старых функций без аннотации
|
||||
if ann := fn.GetAnnotations(); ann != nil {
|
||||
if v := ann[fissionSourceTypeAnnotation]; v != "" {
|
||||
sourceType = v
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поле возвращается в JSON ответе:
|
||||
```json
|
||||
{
|
||||
"name": "my-func",
|
||||
"code": "...",
|
||||
"source_type": "archive",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Frontend — submitCreate
|
||||
|
||||
До v1.3.45 всегда отправлял JSON с полем `code`.
|
||||
|
||||
Теперь определяет режим по видимости `c-archive-area`:
|
||||
|
||||
```javascript
|
||||
var archiveArea = document.getElementById('c-archive-area');
|
||||
var isArchiveMode = archiveArea && archiveArea.style.display !== 'none';
|
||||
var archiveFile = isArchiveMode ? document.getElementById('c-archive-file').files[0] : null;
|
||||
```
|
||||
|
||||
**Если archive mode и файл выбран** → FormData:
|
||||
```javascript
|
||||
var fd = new FormData();
|
||||
fd.append('name', name);
|
||||
fd.append('language', lang);
|
||||
// ... остальные поля ...
|
||||
fd.append('archive', archiveFile);
|
||||
fetch(API_BASE + '/functions', { method: 'POST', headers: authHeaders(), body: fd });
|
||||
```
|
||||
|
||||
**Иначе** → старый JSON путь без изменений.
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend — openEdit
|
||||
|
||||
До v1.3.45 сбрасывал режим на основе `fn.code !== ''`, что было неправильно:
|
||||
`extractPackageSourceCode` умеет доставать код даже из архивных функций через URL,
|
||||
поэтому `fn.code` всегда был непустым.
|
||||
|
||||
Теперь использует `fn.source_type`:
|
||||
```javascript
|
||||
if (fn.source_type === 'archive') {
|
||||
setCodeMode('e', 'archive');
|
||||
} else {
|
||||
setCodeMode('e', 'code');
|
||||
}
|
||||
```
|
||||
|
||||
**Поведение:**
|
||||
- Функция создана через код → открывается редактор кода с текущим кодом
|
||||
- Функция создана из архива → открывается archive area с file input (пустым)
|
||||
- Старые функции без аннотации → `source_type: "code"` по умолчанию → code mode
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend — submitEdit
|
||||
|
||||
До v1.3.45 всегда отправлял `PUT /functions/:name/code` с JSON.
|
||||
|
||||
Теперь определяет режим аналогично submitCreate:
|
||||
|
||||
```javascript
|
||||
var archiveArea = document.getElementById('e-archive-area');
|
||||
var isArchiveMode = archiveArea && archiveArea.style.display !== 'none';
|
||||
var archiveFile = isArchiveMode ? document.getElementById('e-archive-file').files[0] : null;
|
||||
```
|
||||
|
||||
**Если archive mode и файл выбран** → `PUT /functions/:name/archive` с FormData:
|
||||
```javascript
|
||||
fd.append('timeout', String(parseTimeout(...)));
|
||||
fd.append('archive', archiveFile);
|
||||
fetch(...'/archive', { method: 'PUT', headers: authHeaders(), body: fd });
|
||||
```
|
||||
|
||||
**Иначе** → старый `PUT /functions/:name/code` с JSON.
|
||||
|
||||
**Важно:** если пользователь открыл функцию в archive mode но не выбрал новый файл —
|
||||
`archiveFile` будет `null` и код уйдёт по JSON-пути (отправит пустой код).
|
||||
Это expected behaviour: пользователь должен явно выбрать файл для обновления архива.
|
||||
|
||||
---
|
||||
|
||||
## 8. Исправление версии в UI
|
||||
|
||||
В `index.html` номер версии присутствует в **двух** местах:
|
||||
- Строка 103: верхний правый угол (`<div>` в header)
|
||||
- Строка 403: блок "Помощь" (`<span>` в help modal)
|
||||
|
||||
До v1.3.45 оба места обновлялись несинхронно — строка 103 отставала.
|
||||
Начиная с v1.3.45 оба места обновляются одновременно через `multi_replace_string_in_file`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Проблема с imagePullPolicy
|
||||
|
||||
При первом деплое v1.3.43 (после исправления версии) UI показывал `v1.3.40`.
|
||||
Причина: `imagePullPolicy: IfNotPresent` — K8s имел образ `v1.3.43` в кэше
|
||||
и не тянул обновлённый образ с тем же тегом.
|
||||
|
||||
**Решение:** менять тег при каждой сборке (v1.3.43 → v1.3.44 → v1.3.45).
|
||||
Это принципиальное правило: **никогда не пересобирать образ с тем же тегом**.
|
||||
|
||||
---
|
||||
|
||||
## 10. Константы и аннотации (handlers.go)
|
||||
|
||||
```go
|
||||
const maxArchiveUploadSize = 32 << 20 // 32 MB
|
||||
const fissionSourceTypeAnnotation = "fission-console/source-type"
|
||||
```
|
||||
|
||||
Существующие константы не изменялись:
|
||||
```go
|
||||
const maxCodeSize = 1 << 20 // 1 MB — только для inline кода
|
||||
const defaultFunctionInvokeTimeout = 60 * time.Second
|
||||
```
|
||||
@@ -1,271 +0,0 @@
|
||||
# Логика авторизации — Fission Console
|
||||
|
||||
> ⚠️ ЛЕГАСИ (актуально до ~апреля 2026): описание ниже относится к старой схеме с Deck API + JWT.
|
||||
> Актуальное описание — **выше этого блока**, в разделе "АКТУАЛЬНО (май 2026+)".
|
||||
|
||||
---
|
||||
|
||||
# АКТУАЛЬНО (май 2026+)
|
||||
|
||||
## Что принимает сервер
|
||||
|
||||
Токен передаётся одним из двух способов:
|
||||
- Заголовок `X-Auth-Token: <токен>`
|
||||
- Заголовок `Authorization: Bearer <токен>`
|
||||
|
||||
`X-Auth-Env` — необязателен, по умолчанию `test`.
|
||||
|
||||
---
|
||||
|
||||
## Типы токенов и маршрутизация (MultiAuthenticator)
|
||||
|
||||
Сервер использует `MultiAuthenticator`, который определяет тип по форме токена:
|
||||
|
||||
| Форма токена | Authenticator | Описание |
|
||||
|---|---|---|
|
||||
| JWT (три части через `.`) | `DeckAuthenticator` или `TestAuthenticator` | Реальный облачный токен или тест-JWT |
|
||||
| Любая строка ≥6 символов | `DemoAuthenticator` | Demo-логин без внешних запросов |
|
||||
| Строка <6 символов | — | Ошибка 401 |
|
||||
|
||||
---
|
||||
|
||||
## DemoAuthenticator (текущий основной режим)
|
||||
|
||||
Принимает **любую строку ≥6 символов** как логин. Не ходит во внешние сервисы.
|
||||
|
||||
```
|
||||
login (≥6 символов)
|
||||
│
|
||||
▼
|
||||
Sub = UUID v5(login) ← детерминированный, фиксированный UUID namespace
|
||||
Email = "demo-lo***in" ← маскированный для отображения в UI
|
||||
│
|
||||
▼
|
||||
Namespace = "fission-" + hex(SHA256(Sub)[:8])
|
||||
```
|
||||
|
||||
Пример:
|
||||
- login = `livetest@test.local`
|
||||
- Sub = `uuidV5("livetest@test.local")` = фиксированный UUID
|
||||
- Namespace = `fission-c3fce59430e41b0f`
|
||||
|
||||
Каждый логин → один и тот же namespace (детерминированно).
|
||||
|
||||
---
|
||||
|
||||
## DeckAuthenticator (production JWT)
|
||||
|
||||
Для JWT-токенов из облака NUBES:
|
||||
|
||||
```
|
||||
JWT токен
|
||||
│
|
||||
├─► validateToken → GET {deckAPI}/index.cfm/instances
|
||||
│ Authorization: Bearer <token>
|
||||
│ 401 → ошибка | другой → ОК
|
||||
│ Кэш на 5 минут (sync.Map, ключ "env:token")
|
||||
│
|
||||
└─► identityFromJWT → decode payload (base64url, без верификации подписи)
|
||||
→ claim "sub" → Sub
|
||||
→ claim "email" → Email (опционально)
|
||||
```
|
||||
|
||||
Deck API URL по стендам:
|
||||
- `prod` → `https://deck-api.ngcloud.ru/api/v1`
|
||||
- `dev` → `https://deck-api-dev.ngcloud.ru/api/v1`
|
||||
- `test` → `https://deck-api-test.ngcloud.ru/api/v1`
|
||||
|
||||
---
|
||||
|
||||
## TestAuthenticator (тест-режим, устаревший)
|
||||
|
||||
Принимает JWT — декодирует sub из payload. Или строку с `@` — использует как sub напрямую.
|
||||
Не ходит в Deck API. Включался через `FISSION_TEST_MODE=true`.
|
||||
|
||||
> ⚠️ Сейчас не используется — заменён на DemoAuthenticator.
|
||||
|
||||
---
|
||||
|
||||
## Вычисление namespace: `NamespaceForSub`
|
||||
|
||||
```go
|
||||
func NamespaceForSub(sub string) string {
|
||||
h := sha256.Sum256([]byte(sub))
|
||||
return "fission-" + hex.EncodeToString(h[:8]) // 16 hex символов
|
||||
}
|
||||
```
|
||||
|
||||
Namespace **детерминирован**: одинаковый логин → всегда один namespace.
|
||||
|
||||
---
|
||||
|
||||
## authMiddleware — что происходит на каждый запрос
|
||||
|
||||
```
|
||||
Входящий HTTP запрос
|
||||
│
|
||||
├─► authTokenFromRequest → X-Auth-Token || Authorization: Bearer
|
||||
│
|
||||
├─► authenticator.Authenticate(token, env) → UserIdentity{Sub, Email}
|
||||
│ ошибка → 401
|
||||
│
|
||||
├─► NamespaceForSub(Sub) → "fission-XXXXXXXXXXXXXXXX"
|
||||
│
|
||||
├─► nsManager.EnsureUserNS(namespace)
|
||||
│ → создаёт K8s namespace + RBAC если нет
|
||||
│ ошибка → 502
|
||||
│
|
||||
└─► контекст запроса: ctxKeyNS=namespace, ctxKeyIdentity=identity
|
||||
→ handler работает в namespace пользователя
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /auth — явный вход
|
||||
|
||||
```
|
||||
POST /console/api/auth
|
||||
Body: { "token": "...", "env": "test" }
|
||||
│
|
||||
└─► то же что authMiddleware, возвращает:
|
||||
{ ok: true, namespace: "fission-...", email: "demo-lo***in" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Итого — полная схема (актуальная)
|
||||
|
||||
```
|
||||
Пользователь передаёт токен/логин
|
||||
│
|
||||
▼
|
||||
MultiAuthenticator
|
||||
├── JWT? → DeckAuthenticator (Deck API + JWT decode)
|
||||
└── нет → DemoAuthenticator (≥6 символов → UUID v5)
|
||||
│
|
||||
▼
|
||||
UserIdentity { Sub, Email }
|
||||
│
|
||||
▼
|
||||
NamespaceForSub(Sub) = "fission-" + hex(SHA256(Sub)[:8])
|
||||
│
|
||||
▼
|
||||
EnsureUserNS → K8s namespace существует
|
||||
│
|
||||
▼
|
||||
Все хендлеры работают с namespace из context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# ЛЕГАСИ (до апреля 2026) — не удалять, для анализа
|
||||
|
||||
|
||||
|
||||
## Что вводит пользователь
|
||||
|
||||
В форме логина (`index.html`, overlay `#login-overlay`):
|
||||
- **Стенд** (`#l-env`): `dev` / `test` / `prod`
|
||||
- **Токен** (`#l-token`): JWT-токен из личного кабинета NUBES (Профиль → Токены)
|
||||
|
||||
---
|
||||
|
||||
## Фронтенд: `doLogin()` → `js/auth.js`
|
||||
|
||||
1. Берёт токен и стенд из формы
|
||||
2. Делает `POST /console/api/auth` с `{ token, env }` в теле
|
||||
3. Если ответ OK:
|
||||
- Сохраняет `auth_token` и `auth_env` в **localStorage** браузера
|
||||
- Вызывает `setUserAvatar(token)` — декодирует email из JWT payload и показывает в navbar
|
||||
- Скрывает overlay логина
|
||||
4. Проверяет `GET /console/api/ns/status` — если namespace не готов, показывает overlay инициализации
|
||||
|
||||
---
|
||||
|
||||
## Бэкенд: `handleAuth` → `handlers.go:1022`
|
||||
|
||||
1. Принимает `POST /console/api/auth`
|
||||
2. Валидирует токен через `resolveNamespaceForToken(token, env, testMode)`
|
||||
3. Вызывает `EnsureUserNS` — гарантирует что K8s namespace + RBAC + quota существуют
|
||||
4. Возвращает JSON: `{ ok: true, env: "test", namespace: "fission-..." }`
|
||||
|
||||
---
|
||||
|
||||
## Валидация токена: `validateDeckToken` → `server.go:279`
|
||||
|
||||
**Deck API** — внешний сервис облачной платформы NUBES:
|
||||
```
|
||||
dev → https://deck-api-dev.ngcloud.ru/api/v1
|
||||
test → https://deck-api-test.ngcloud.ru/api/v1
|
||||
prod → https://deck-api.ngcloud.ru/api/v1
|
||||
```
|
||||
|
||||
Проверка: `GET {deckAPI}/index.cfm/instances` с заголовком `Authorization: Bearer {token}`
|
||||
- `401` → токен невалиден
|
||||
- Любой другой ответ → токен принят
|
||||
- Результат кэшируется на **5 минут** (`sync.Map`)
|
||||
|
||||
---
|
||||
|
||||
## Вычисление namespace: `namespaceFromJWT` → `auth.go:131`
|
||||
|
||||
1. JWT токен **не верифицируется по подписи** — Deck API уже это сделал
|
||||
2. Декодируется payload (base64url)
|
||||
3. Извлекается claim `"sub"` (идентификатор пользователя в облаке)
|
||||
4. Namespace = `"fission-" + hex(SHA256(sub)[:8])`
|
||||
|
||||
Пример: sub = `c3fce59430e41b0f...` → namespace `fission-c3fce59430e41b0f`
|
||||
|
||||
---
|
||||
|
||||
## Email в navbar: `setUserAvatar` / `jwtEmail` → `auth.js`
|
||||
|
||||
- `jwtEmail(token)` — декодирует JWT payload на **фронтенде** (без обращения к серверу)
|
||||
- Берёт `payload.email` или `payload.sub`
|
||||
- Отображает в `#user-avatar` в левом верхнем углу (вместо синего кружка)
|
||||
|
||||
---
|
||||
|
||||
## Последующие запросы
|
||||
|
||||
Каждый API-запрос от фронтенда включает заголовки:
|
||||
```
|
||||
X-Auth-Token: <токен из localStorage>
|
||||
X-Auth-Env: <стенд из localStorage>
|
||||
```
|
||||
|
||||
`authMiddleware` (`auth.go`) снова валидирует токен (но из кэша — без нового HTTP-запроса к Deck API),
|
||||
вычисляет namespace и кладёт его в `context.Context` → все хендлеры работают в нужном namespace.
|
||||
|
||||
---
|
||||
|
||||
## Test-режим (FISSION_TEST_MODE=true)
|
||||
|
||||
Вместо реального Deck API — проверяет заголовок `X-Test-Sub`.
|
||||
Если токен содержит `@` — трактуется как email и используется как sub для вычисления namespace.
|
||||
Используется в тестах и для live-тестирования без реальных токенов.
|
||||
|
||||
---
|
||||
|
||||
## Итого — схема
|
||||
|
||||
```
|
||||
Пользователь вводит токен + стенд
|
||||
│
|
||||
▼
|
||||
POST /console/api/auth
|
||||
│
|
||||
├─► validateDeckToken → Deck API ({env}.ngcloud.ru) → 200 = ОК
|
||||
│
|
||||
├─► namespaceFromJWT → decode JWT payload → claim "sub" → SHA256[:8] → "fission-XXXXXXXX"
|
||||
│
|
||||
├─► EnsureUserNS → K8s namespace + RBAC создаются если нет
|
||||
│
|
||||
└─► { ok: true, namespace: "fission-..." }
|
||||
|
||||
Фронтенд:
|
||||
├─► localStorage.setItem('auth_token', token)
|
||||
├─► localStorage.setItem('auth_env', env)
|
||||
├─► jwtEmail(token) → email из JWT payload → показать в navbar
|
||||
└─► reloadAll() — загрузить функции
|
||||
```
|
||||
@@ -1,180 +0,0 @@
|
||||
# Создание функции — текущий flow (май 2026)
|
||||
|
||||
## UI (браузер)
|
||||
|
||||
### Форма создания (`create-modal`)
|
||||
|
||||
Поля в `index.html`:
|
||||
- `c-lang` — select (python / nodejs / go / ruby / php)
|
||||
- `c-name` — имя функции
|
||||
- `c-entry` — entrypoint (заполняется автоматически по языку из `LANG_TEMPLATES`)
|
||||
- `c-route` — HTTP-путь
|
||||
- `c-methods` — методы, через запятую
|
||||
- `c-timeout` — таймаут в секундах
|
||||
- `c-schedule-enabled` + `c-cron` — cron-расписание (опционально)
|
||||
- `c-code` — **textarea** с исходным кодом функции (единственный способ передать код)
|
||||
|
||||
Кнопки рядом с `c-code`:
|
||||
- `aiCheck(...)` — линтер (POST `/console/api/ai/check`)
|
||||
- `aiExplain(...)` — объяснение через LLM (POST `/console/api/ai/ask`)
|
||||
|
||||
При нажатии «Создать» вызывается `submitCreate()` (`js/functions.js`):
|
||||
|
||||
```js
|
||||
await requestJSON(API_BASE + '/functions', 'POST', {
|
||||
name, language, entrypoint, route, methods, timeout,
|
||||
code: document.getElementById('c-code').value // <-- всегда текст
|
||||
});
|
||||
```
|
||||
|
||||
### Форма редактирования (`edit-modal`)
|
||||
|
||||
Аналогична. `submitEdit()` шлёт:
|
||||
|
||||
```js
|
||||
await requestJSON(API_BASE + '/functions/' + name + '/code', 'PUT', {
|
||||
code: document.getElementById('e-code').value,
|
||||
timeout: ...
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend
|
||||
|
||||
### POST /console/api/functions → `handleCreateFunction`
|
||||
|
||||
Структура запроса (`model.CreateFunctionRequest`):
|
||||
```json
|
||||
{ "name": "fn", "language": "python", "code": "def main(): ...",
|
||||
"entrypoint": "main", "route": "/ns/fn", "methods": ["GET"],
|
||||
"timeout": 60, "ttl": "" }
|
||||
```
|
||||
|
||||
Обязательные: `name`, `language` (или `environment`), `code`.
|
||||
|
||||
**Путь для интерпретируемых языков** (python / nodejs / ruby / php):
|
||||
```
|
||||
code (строка)
|
||||
→ buildDeployArchive(lang, code)
|
||||
python/ruby/php: []byte(code) (raw, не zip)
|
||||
nodejs: BuildJSDeployZip(code) → zip с package.json + main.js
|
||||
→ buildDeploySpec(ctx, bytes)
|
||||
→ uploadToStoragesvc() → multipart POST → storagesvc → archiveURL
|
||||
→ fallback: base64 literal (если storagesvc недоступен)
|
||||
→ Package.spec.deployment = { type: "url", url: "..." }
|
||||
или = { type: "literal", literal: "base64..." }
|
||||
```
|
||||
|
||||
**Путь для Go**:
|
||||
```
|
||||
code
|
||||
→ BuildGoSourceZip(code) → zip с исходником
|
||||
→ uploadToStoragesvc → archiveURL
|
||||
→ Package.spec.source = { ... } (source package, компилируется builder job)
|
||||
Package.spec.buildcommand = "build"
|
||||
```
|
||||
|
||||
После создания Package → создаётся Function → HTTPTrigger.
|
||||
При ошибке на любом шаге — откат (best-effort DELETE предыдущих объектов).
|
||||
|
||||
### PUT /console/api/functions/{name}/code → `handleUpdateFunctionCode`
|
||||
|
||||
```json
|
||||
{ "code": "...", "timeout": 60 }
|
||||
```
|
||||
|
||||
Обязателен: `code`.
|
||||
|
||||
Алгоритм:
|
||||
1. Читает текущую Function → берёт `fission-console/language` из аннотации
|
||||
2. `buildDeployArchive(lang, code)` → байты
|
||||
3. Создаёт **новый Package** с именем `{name}-pkg-{timestamp_base36}`
|
||||
4. Обновляет Function.spec.package.packageref на новый пакет
|
||||
5. Удаляет старый Package (best-effort)
|
||||
|
||||
> Почему новый Package, а не обновление старого:
|
||||
> executor кэширует function-service по `functionUid` — не видит изменений в том же Package.
|
||||
> Новое имя гарантирует cache miss.
|
||||
|
||||
---
|
||||
|
||||
## Линтер (ai/check) и LLM (ai/ask)
|
||||
|
||||
### POST /console/api/ai/check
|
||||
|
||||
```json
|
||||
{ "language": "python", "code": "def main(): ..." }
|
||||
```
|
||||
|
||||
Работает **только со строкой кода**. Запускает:
|
||||
- python: `python3 -m py_compile <tmpfile.py>`
|
||||
- nodejs: `node --check <tmpfile.js>`
|
||||
- ruby: `ruby -c <tmpfile.rb>`
|
||||
- php: `php -l <tmpfile.php>`
|
||||
- go: `go/parser.ParseFile(...)` прямо в процессе (без tmpfile)
|
||||
|
||||
Временный файл создаётся в `os.TempDir()`, удаляется через `defer`.
|
||||
|
||||
### POST /console/api/ai/ask
|
||||
|
||||
Отправляет код в LLM (объяснение, анализ). Детали в `ai_prompts.go`.
|
||||
|
||||
---
|
||||
|
||||
## Что сейчас НЕ поддерживается
|
||||
|
||||
- **Загрузка архива (.zip) из файла** — ни в UI, ни в API
|
||||
- Архив можно только скачать (через `extractPackageSourceCode` → `fetchPackageArchive`)
|
||||
|
||||
---
|
||||
|
||||
## Planned: загрузка архива
|
||||
|
||||
### API
|
||||
|
||||
Добавить в `CreateFunctionRequest` и `UpdateCodeRequest` поле `archive_b64 string`.
|
||||
Логика в handler: если `archive_b64 != ""` — пропустить `buildDeployArchive`,
|
||||
сразу `uploadToStoragesvc(base64.Decode(archive_b64))`.
|
||||
|
||||
Валидация:
|
||||
- magic bytes `PK\x03\x04` (zip)
|
||||
- максимум 10MB (против 1MB для кода)
|
||||
|
||||
### UI
|
||||
|
||||
Раздельные режимы в форме — кнопки-переключатели «✏️ Код» / «📦 Архив»:
|
||||
- режим «Код» — textarea (как сейчас)
|
||||
- режим «Архив» — `<input type="file" accept=".zip">`, textarea скрыта
|
||||
|
||||
В `submitCreate` / `submitEdit` — `FileReader.readAsDataURL(file)` → base64 → поле `archive_b64`.
|
||||
|
||||
### Линтер и LLM для архива
|
||||
|
||||
Проблема: линтер (`ai/check`) и LLM (`ai/ask`) принимают `code` — строку.
|
||||
Архив — zip из нескольких файлов. Варианты:
|
||||
|
||||
**Вариант A — на бэке, при загрузке**
|
||||
После распаковки архива (уже есть `decodeArchiveBytesToSource` в `package.go`):
|
||||
- Извлечь главный файл (entrypoint)
|
||||
- Прогнать через линтер
|
||||
- Результат вернуть в том же ответе на create/update (поле `lint_result`)
|
||||
|
||||
Плюсы: не меняет контракт линтера, нет доп. round-trip.
|
||||
Минусы: линтится только один файл из архива.
|
||||
|
||||
**Вариант B — на фронте, до отправки**
|
||||
`FileReader.readAsText(file)` — не работает для zip напрямую.
|
||||
Можно использовать `JSZip` (npm/CDN) — распаковать в браузере → взять главный файл → передать в `ai/check`.
|
||||
|
||||
Плюсы: линтинг до загрузки, быстрая обратная связь.
|
||||
Минусы: добавляется зависимость (JSZip ~100KB).
|
||||
|
||||
**Вариант C — гибрид (рекомендован)**
|
||||
1. Фронт: при выборе архива — использовать JSZip, найти entrypoint-файл по языку,
|
||||
показать его в readonly-textarea, кнопки линтера и LLM работают с этим текстом.
|
||||
2. Отправка: `archive_b64` (весь zip) идёт на бэк как есть.
|
||||
3. Бэк (опционально): после сохранения прогнать `decodeArchiveBytesToSource` → линтер,
|
||||
вернуть предупреждение если нашёл ошибки.
|
||||
|
||||
Так пользователь видит и код и результат линтера до деплоя, и полный архив уходит без изменений.
|
||||
@@ -1,79 +0,0 @@
|
||||
# Линтинг архива функции — flow (май 2026)
|
||||
|
||||
## Назначение
|
||||
|
||||
Пользователь может загрузить zip-архив с кодом функции.
|
||||
Перед сохранением или после загрузки — архив можно прогнать через линтер,
|
||||
чтобы убедиться что код синтаксически корректен.
|
||||
|
||||
Линтинг выполняется **на сервере** (в Kubernetes), а не на машине пользователя.
|
||||
|
||||
---
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
POST /console/api/ai/lint-archive
|
||||
Authorization: X-Auth-Token: <sub>
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
Поле: `archive` — zip-файл.
|
||||
|
||||
### Ограничения
|
||||
- Максимальный размер zip-архива: **100 KB**
|
||||
- Максимальный суммарный размер распакованных файлов: **100 KB** (защита от zip bomb)
|
||||
- При превышении — HTTP 413
|
||||
|
||||
### Response (200 OK)
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"results": [
|
||||
{"file": "hello.py", "ok": true},
|
||||
{"file": "bad.js", "ok": false, "output": "SyntaxError: Unexpected token (2:0)"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Response (ошибка)
|
||||
```json
|
||||
{"error": "архив слишком большой: максимум 100 KB"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Поддерживаемые языки
|
||||
|
||||
| Расширение | Язык | Линтер |
|
||||
|-----------|---------|---------------------------|
|
||||
| `.py` | Python | `python3 -m py_compile` |
|
||||
| `.js` | Node.js | `node --check` |
|
||||
| `.rb` | Ruby | `ruby -c` |
|
||||
| `.php` | PHP | `php -l` |
|
||||
|
||||
Файлы с другими расширениями (`.json`, `.txt`, `.md`, `.yaml` и т.д.) — пропускаются.
|
||||
Если в архиве нет ни одного поддерживаемого файла — HTTP 400.
|
||||
|
||||
---
|
||||
|
||||
## Реализация (backend)
|
||||
|
||||
Файл: `console/internal/api/lint_archive.go`
|
||||
|
||||
1. Архив читается в память (`bytes.Buffer`), без записи на диск.
|
||||
2. zip открывается через `archive/zip.Reader` из буфера в памяти.
|
||||
3. Проверяется суммарный распакованный размер — защита от zip bomb.
|
||||
4. Каждый поддерживаемый файл читается из zip в память.
|
||||
5. Записывается во временный файл в **`/dev/shm`** (RAM-backed tmpfs), fallback — `/tmp`.
|
||||
6. Линтер запускается с таймаутом 10 секунд на файл.
|
||||
7. Временный файл удаляется сразу после линтинга.
|
||||
8. Путь к tmp-файлу скрыт от пользователя в выводе линтера.
|
||||
|
||||
---
|
||||
|
||||
## Что ещё нужно сделать
|
||||
|
||||
- [ ] UI: кнопка "Проверить архив" рядом с полем загрузки архива в форме создания функции
|
||||
- [ ] Показывать результаты в UI (список файлов, статус, ошибки)
|
||||
- [ ] Тесты: `scripts/test_fn_archives.sh` — добавить вызов lint-archive endpoint
|
||||
@@ -1,66 +0,0 @@
|
||||
# Загрузка страницы — flow (май 2026)
|
||||
|
||||
## Последовательность при открытии/перезагрузке
|
||||
|
||||
```
|
||||
1. HTML загружен, JS инициализирован
|
||||
│
|
||||
▼
|
||||
2. checkAuth() (auth.js)
|
||||
├── localStorage["auth_token"] отсутствует → showLoginOverlay() → СТОП
|
||||
└── токен есть → setUserAvatar() → hideLoginOverlay() → reloadAll()
|
||||
│
|
||||
▼
|
||||
3. reloadAll() (app.js)
|
||||
├── показывает в статусе "Загрузка... Nс" (startTimedStatus, тикает каждую секунду)
|
||||
├── показывает в таблице "⏳ Загружаем функции..."
|
||||
├── запускает 5 параллельных запросов (Promise.all):
|
||||
│ GET /console/api/environments
|
||||
│ GET /console/api/packages
|
||||
│ GET /console/api/functions ← обычно самый долгий
|
||||
│ GET /console/api/httptriggers
|
||||
│ GET /console/api/timetriggers
|
||||
└── (ждём ответа... может занять 3–10с при cold start)
|
||||
│
|
||||
▼
|
||||
4. Данные получены
|
||||
├── обновляем счётчики (envs, pkgs, fns, http, cron)
|
||||
├── рендерим ПЕРВУЮ строку функций немедленно
|
||||
├── остальные строки добавляем по одной каждые 40мс (setTimeout)
|
||||
└── по завершении статус: "Загружено N функций за Xс"
|
||||
```
|
||||
|
||||
## Путь через doLogin() (при ручном входе)
|
||||
|
||||
После успешного POST /console/api/auth:
|
||||
1. Сохраняем токен в localStorage
|
||||
2. Проверяем GET /console/api/ns/status
|
||||
- `ready: false` → `pollNSStatus()` → показывается init-overlay с прогрессом по стадиям
|
||||
- `ready: true` → сразу `reloadAll()`
|
||||
|
||||
## Почему долго
|
||||
|
||||
При первой загрузке после простоя:
|
||||
- Fission `poolmgr` может не иметь прогретых пулов → холодный старт на first request к каждому language env
|
||||
- Kubernetes API (~k8s list всех функций, пакетов) — несколько сотен мс при большом числе объектов
|
||||
- `Promise.all` ждёт самый долгий запрос
|
||||
|
||||
## Что показывается пользователю (v1.3.34+)
|
||||
|
||||
| Момент | Что видит |
|
||||
|---|---|
|
||||
| Сразу при запуске | Таблица: "⏳ Загружаем функции..." |
|
||||
| Каждую секунду | Статус: "Загрузка данных Nс" |
|
||||
| Первая функция пришла | Первая строка таблицы появляется |
|
||||
| +40мс × N | Строки добавляются одна за другой |
|
||||
| Конец | Статус: "Загружено N функций за Xс" |
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Роль |
|
||||
|---|---|
|
||||
| `ui/js/auth.js` → `checkAuth()` | Точка входа при загрузке страницы |
|
||||
| `ui/js/auth.js` → `doLogin()` | Ручной вход, проверка NS status |
|
||||
| `ui/js/app.js` → `reloadAll()` | Загрузка данных, построчный рендер |
|
||||
| `ui/js/init.js` → `pollNSStatus()` | Ожидание готовности namespace (init overlay) |
|
||||
| `ui/js/init.js` → `startTimedStatus()` | Таймер в статус-баре |
|
||||
@@ -1,133 +0,0 @@
|
||||
# Рефакторинг UI: монолит → модули
|
||||
|
||||
Дата: 2026-05-02
|
||||
Ветка: второмая
|
||||
Файл-источник: console/ui/index.html (1651 строк, 73 функции)
|
||||
|
||||
---
|
||||
|
||||
## Целевая структура
|
||||
|
||||
```
|
||||
console/ui/
|
||||
├── index.html # только HTML-скелет + <script src="..."> теги
|
||||
├── cron.html # без изменений
|
||||
├── css/
|
||||
│ └── styles.css # все стили (сейчас в <style> блоке index.html)
|
||||
└── js/
|
||||
├── config.js # константы (DEFAULT_CODE, язык-шаблоны)
|
||||
├── api.js # authHeaders, getJSON, requestJSON
|
||||
├── auth.js # showLoginOverlay, hideLoginOverlay, doLogin, jwtEmail, setUserAvatar, doLogout, checkAuth
|
||||
├── ui.js # setText, showStatus, clearStatus, h, formatTimestamp, timestampCell
|
||||
├── functions.js # openCreate, closeCreate, submitCreate, openEdit, closeEdit, submitEdit, removeFn, parseMethods, parseTimeout, onLangChange
|
||||
├── triggers.js # httpTriggerByFn, timeTriggerByFn, toggleScheduleFields, schedulePayload, syncScheduleForFunction
|
||||
├── modals.js # openHelp, closeHelp, openInvoke, closeInvoke, submitInvoke
|
||||
├── ai.js # aiCheck, aiGenerate, aiExplain, askAssistant, toggleAssistant, showGenPrompt, llmGeneratedWarning, addLLMWarning
|
||||
├── init.js # initLog, pollNSStatus, formatSeconds, setInitElapsed, setInitCurrentStage, setBox, missingKeys, firstPendingStage, describeInitReason, summarizeInit, explainDelay, startTimedStatus, renderInitStages
|
||||
└── app.js # reloadAll, главный контроллер, window.S = {}
|
||||
```
|
||||
|
||||
embed.go: `//go:embed index.html cron.html css js`
|
||||
|
||||
---
|
||||
|
||||
## Порядок загрузки в index.html (критично!)
|
||||
|
||||
```html
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/auth.js"></script>
|
||||
<script src="js/functions.js"></script>
|
||||
<script src="js/triggers.js"></script>
|
||||
<script src="js/modals.js"></script>
|
||||
<script src="js/ai.js"></script>
|
||||
<script src="js/init.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаги (пошагово, с билдом после каждого)
|
||||
|
||||
### Шаг 1 — CSS
|
||||
- Вынести `<style>` блок из index.html в `css/styles.css`
|
||||
- Заменить на `<link rel="stylesheet" href="css/styles.css">`
|
||||
- Обновить embed.go
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 2 — config.js
|
||||
- Вынести: объект `DEFAULT_CODE` (шаблоны кода по языкам)
|
||||
- Самый изолированный, без зависимостей
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 3 — api.js
|
||||
- Вынести: `authHeaders`, `getJSON`, `requestJSON`
|
||||
- Зависит от: ничего (только localStorage)
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 4 — ui.js
|
||||
- Вынести: `setText`, `showStatus`, `clearStatus`, `h`, `formatTimestamp`, `timestampCell`
|
||||
- Зависит от: ничего
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 5 — auth.js
|
||||
- Вынести: `showLoginOverlay`, `hideLoginOverlay`, `doLogin`, `jwtEmail`, `setUserAvatar`, `doLogout`, `checkAuth`
|
||||
- Зависит от: api.js, ui.js
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 6 — triggers.js
|
||||
- Вынести: `httpTriggerByFn`, `timeTriggerByFn`, `toggleScheduleFields`, `schedulePayload`, `syncScheduleForFunction`
|
||||
- Зависит от: api.js, ui.js
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 7 — modals.js
|
||||
- Вынести: `openHelp`, `closeHelp`, `openInvoke`, `closeInvoke`, `submitInvoke`
|
||||
- Зависит от: api.js, ui.js
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 8 — functions.js
|
||||
- Вынести: `openCreate`, `closeCreate`, `submitCreate`, `openEdit`, `closeEdit`, `submitEdit`, `removeFn`, `parseMethods`, `parseTimeout`, `onLangChange`
|
||||
- Зависит от: api.js, ui.js, triggers.js, config.js
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 9 — ai.js
|
||||
- Вынести: `aiCheck`, `aiGenerate`, `aiExplain`, `askAssistant`, `toggleAssistant`, `showGenPrompt`, `llmGeneratedWarning`, `addLLMWarning`
|
||||
- Зависит от: api.js, ui.js
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 10 — init.js
|
||||
- Вынести: `initLog`, `pollNSStatus`, `formatSeconds`, `setInitElapsed`, `setInitCurrentStage`, `setBox`, `missingKeys`, `firstPendingStage`, `describeInitReason`, `summarizeInit`, `explainDelay`, `startTimedStatus`, `renderInitStages`
|
||||
- Зависит от: api.js, ui.js, auth.js
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
### Шаг 11 — app.js
|
||||
- Вынести: `reloadAll`, `window.S = {}`, DOMContentLoaded инициализация
|
||||
- Зависит от: всё выше
|
||||
- **BUILD + DEPLOY + ПРОВЕРКА**
|
||||
|
||||
---
|
||||
|
||||
## Правила
|
||||
|
||||
1. После каждого шага index.html должен уменьшиться ровно на то что вынесли
|
||||
2. Никаких дублирований — функция живёт только в одном файле
|
||||
3. Глобальные переменные (window.S) остаются в app.js
|
||||
4. Порядок `<script>` тегов строго по зависимостям
|
||||
5. Старый код из index.html удалять только после проверки билда
|
||||
|
||||
---
|
||||
|
||||
## Статус
|
||||
|
||||
- [ ] Шаг 1: CSS
|
||||
- [ ] Шаг 2: config.js
|
||||
- [ ] Шаг 3: api.js
|
||||
- [ ] Шаг 4: ui.js
|
||||
- [ ] Шаг 5: auth.js
|
||||
- [ ] Шаг 6: triggers.js
|
||||
- [ ] Шаг 7: modals.js
|
||||
- [ ] Шаг 8: functions.js
|
||||
- [ ] Шаг 9: ai.js
|
||||
- [ ] Шаг 10: init.js
|
||||
- [ ] Шаг 11: app.js
|
||||
@@ -24,6 +24,7 @@ func main() {
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
mux.HandleFunc("/metrics", srv.HandleMetrics)
|
||||
mux.HandleFunc("/cron/metrics", srv.HandleMetrics)
|
||||
mux.Handle("/", ui.Handler())
|
||||
|
||||
httpSrv := &http.Server{
|
||||
|
||||
@@ -17,7 +17,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: metrics-collector
|
||||
image: naeel/metrics-collector:v0.1.0
|
||||
image: naeel/metrics-collector:v0.1.1
|
||||
ports:
|
||||
- containerPort: 8091
|
||||
env:
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/metrics';
|
||||
const API = '/cron/metrics';
|
||||
const summaryEl = document.getElementById('summary');
|
||||
const historyBody = document.getElementById('history-body');
|
||||
const lastUpdatedEl = document.getElementById('last-updated');
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
requests==2.31.0
|
||||
@@ -1,7 +0,0 @@
|
||||
[default]
|
||||
access_key = 0GLQRD38H4I6RBDB0EWJ
|
||||
secret_key = eTFibiHmBd96IApj9PYsboTR6OBoD7osxoarHykw
|
||||
host_base = s3.msk-1.ngcloud.ru
|
||||
host_bucket = %(bucket)s.s3.msk-1.ngcloud.ru
|
||||
use_https = True
|
||||
signature_v2 = False
|
||||
+2
-303
@@ -634,307 +634,6 @@ PERL_CODE = r"""sub {
|
||||
}
|
||||
"""
|
||||
|
||||
# ── Go ────────────────────────────────────────────────────────────────────────
|
||||
GO_CODE = r"""package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sieveGo(limit int) []int {
|
||||
s := make([]bool, limit+1)
|
||||
for i := range s { s[i] = true }
|
||||
s[0], s[1] = false, false
|
||||
for i := 2; i*i <= limit; i++ {
|
||||
if s[i] { for j := i * i; j <= limit; j += i { s[j] = false } }
|
||||
}
|
||||
primes := []int{}
|
||||
for i := 2; i <= limit; i++ { if s[i] { primes = append(primes, i) } }
|
||||
return primes
|
||||
}
|
||||
|
||||
func fibGo(n int) int64 {
|
||||
if n <= 1 { return int64(n) }
|
||||
a, b := int64(0), int64(1)
|
||||
for i := 2; i <= n; i++ { a, b = b, a+b }
|
||||
return b
|
||||
}
|
||||
|
||||
func matMulGo(a, b [20][20]float64) [20][20]float64 {
|
||||
var c [20][20]float64
|
||||
for i := 0; i < 20; i++ {
|
||||
for k := 0; k < 20; k++ {
|
||||
for j := 0; j < 20; j++ { c[i][j] += a[i][k] * b[k][j] }
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func makeMatGo(seed float64) [20][20]float64 {
|
||||
var m [20][20]float64
|
||||
for i := 0; i < 20; i++ {
|
||||
for j := 0; j < 20; j++ {
|
||||
m[i][j] = math.Mod(float64(i*20+j)+seed, 17)*0.1 + 1.0
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func shellSortGo(arr []int) []int {
|
||||
a := make([]int, len(arr)); copy(a, arr)
|
||||
for gap := len(a) / 2; gap > 0; gap /= 2 {
|
||||
for i := gap; i < len(a); i++ {
|
||||
tmp := a[i]; j := i
|
||||
for j >= gap && a[j-gap] > tmp { a[j] = a[j-gap]; j -= gap }
|
||||
a[j] = tmp
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func quickSortGo(arr []int) []int {
|
||||
a := make([]int, len(arr)); copy(a, arr)
|
||||
sort.Ints(a)
|
||||
return a
|
||||
}
|
||||
|
||||
func polyEvalGo(coeffs []float64, x float64) float64 {
|
||||
result, power := 0.0, 1.0
|
||||
for _, c := range coeffs { result += c * power; power *= x }
|
||||
return result
|
||||
}
|
||||
|
||||
func djb2Go(s string) uint32 {
|
||||
h := uint32(5381)
|
||||
for _, c := range s { h = ((h << 5) + h + uint32(c)) & 0x7fffffff }
|
||||
return h
|
||||
}
|
||||
|
||||
func fnv1aGo(s string) uint32 {
|
||||
h := uint32(2166136261)
|
||||
for _, c := range []byte(s) { h ^= uint32(c); h *= 16777619 }
|
||||
return h
|
||||
}
|
||||
|
||||
func collatzGo(n int) int {
|
||||
steps := 0
|
||||
for n != 1 {
|
||||
if n%2 == 0 { n /= 2 } else { n = 3*n + 1 }
|
||||
steps++
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
func isPrimeGo(n int) bool {
|
||||
if n < 2 { return false }
|
||||
if n == 2 { return true }
|
||||
if n%2 == 0 { return false }
|
||||
for d := 3; d*d <= n; d += 2 { if n%d == 0 { return false } }
|
||||
return true
|
||||
}
|
||||
|
||||
func gcdGo(a, b int) int {
|
||||
for b != 0 { a, b = b, a%b }
|
||||
return a
|
||||
}
|
||||
|
||||
func lcmGo(a, b int) int { return a / gcdGo(a, b) * b }
|
||||
|
||||
// Горнер для вычисления полинома
|
||||
func hornerEval(coeffs []float64, x float64) float64 {
|
||||
result := 0.0
|
||||
for i := len(coeffs) - 1; i >= 0; i-- { result = result*x + coeffs[i] }
|
||||
return result
|
||||
}
|
||||
|
||||
// Решето Сундарама
|
||||
func sundaramSieve(n int) []int {
|
||||
limit := (n - 2) / 2
|
||||
s := make([]bool, limit+1)
|
||||
for i := 1; i <= limit; i++ {
|
||||
for j := i; i+j+2*i*j <= limit; j++ { s[i+j+2*i*j] = true }
|
||||
}
|
||||
primes := []int{2}
|
||||
for i := 1; i <= limit; i++ { if !s[i] { primes = append(primes, 2*i+1) } }
|
||||
return primes
|
||||
}
|
||||
|
||||
// Простые числа через Miller-Rabin (детерминированный для малых n)
|
||||
func millerRabinGo(n int) bool {
|
||||
if n < 2 { return false }
|
||||
if n == 2 || n == 3 || n == 5 || n == 7 { return true }
|
||||
if n%2 == 0 { return false }
|
||||
d, r := n-1, 0
|
||||
for d%2 == 0 { d /= 2; r++ }
|
||||
witnesses := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}
|
||||
for _, a := range witnesses {
|
||||
if a >= n { continue }
|
||||
x := modPowGo(a, d, n)
|
||||
if x == 1 || x == n-1 { continue }
|
||||
cont := false
|
||||
for _ = range make([]struct{}, r-1) {
|
||||
x = modPowGo(x, 2, n)
|
||||
if x == n-1 { cont = true; break }
|
||||
}
|
||||
if !cont { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func modPowGo(base, exp, mod int) int {
|
||||
result := 1; base %= mod
|
||||
for exp > 0 {
|
||||
if exp%2 == 1 { result = result * base % mod }
|
||||
exp /= 2; base = base * base % mod
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Числа Люка
|
||||
func lucasGo(n int) int64 {
|
||||
if n == 0 { return 2 }
|
||||
if n == 1 { return 1 }
|
||||
a, b := int64(2), int64(1)
|
||||
for i := 2; i <= n; i++ { a, b = b, a+b }
|
||||
return b
|
||||
}
|
||||
|
||||
// Цифровая сумма
|
||||
func digitSumGo(n int) int {
|
||||
s := 0
|
||||
for n > 0 { s += n % 10; n /= 10 }
|
||||
return s
|
||||
}
|
||||
|
||||
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// Random delay 300ms-1800ms
|
||||
delayMs := 300 + rng.Intn(1500)
|
||||
time.Sleep(time.Duration(delayMs) * time.Millisecond)
|
||||
|
||||
// Phase 1: Sieve of Eratosthenes up to 3000
|
||||
primes := sieveGo(3000)
|
||||
primeSum := 0
|
||||
for _, p := range primes { primeSum += p }
|
||||
time.Sleep(time.Duration(50+rng.Intn(200)) * time.Millisecond)
|
||||
|
||||
// Phase 2: Sundaram sieve cross-check
|
||||
primes2 := sundaramSieve(3000)
|
||||
_ = primes2
|
||||
time.Sleep(time.Duration(30+rng.Intn(150)) * time.Millisecond)
|
||||
|
||||
// Phase 3: Miller-Rabin for 1000..1200
|
||||
mrCount := 0
|
||||
for n := 1000; n <= 1200; n++ { if millerRabinGo(n) { mrCount++ } }
|
||||
time.Sleep(time.Duration(20+rng.Intn(100)) * time.Millisecond)
|
||||
|
||||
// Phase 4: Fibonacci + Lucas + digit sums
|
||||
type entry struct{ N int; Fib, Lucas int64; DigSum int }
|
||||
entries := []entry{}
|
||||
for n := 20; n <= 60; n += 5 {
|
||||
f := fibGo(n); l := lucasGo(n)
|
||||
entries = append(entries, entry{n, f, l, digitSumGo(int(f % 1000000))})
|
||||
}
|
||||
time.Sleep(time.Duration(30+rng.Intn(150)) * time.Millisecond)
|
||||
|
||||
// Phase 5: Matrix multiply 20x20
|
||||
A := makeMatGo(3.0); B := makeMatGo(7.0)
|
||||
C := matMulGo(A, B)
|
||||
matSum := 0.0
|
||||
for _, row := range C { for _, v := range row { matSum += v } }
|
||||
time.Sleep(time.Duration(20+rng.Intn(100)) * time.Millisecond)
|
||||
|
||||
// Phase 6: Shell sort + stdlib sort 800 elements, compare
|
||||
arr := make([]int, 800)
|
||||
for i := range arr { arr[i] = rng.Intn(10000) }
|
||||
shellSorted := shellSortGo(arr)
|
||||
quickSorted := quickSortGo(arr)
|
||||
sortMatch := true
|
||||
for i := range shellSorted { if shellSorted[i] != quickSorted[i] { sortMatch = false; break } }
|
||||
time.Sleep(time.Duration(10+rng.Intn(80)) * time.Millisecond)
|
||||
|
||||
// Phase 7: Polynomial evaluation (Horner vs direct)
|
||||
coeffs := []float64{1, -3, 2, 5, -1, 4, -2, 1, 3, -1, 2, 1, -3, 2, 5, -1, 3, -2, 1, 4}
|
||||
polyMax, hornerMax := math.Inf(-1), math.Inf(-1)
|
||||
for i := -80; i <= 80; i++ {
|
||||
x := float64(i) * 0.1
|
||||
if v := polyEvalGo(coeffs, x); v > polyMax { polyMax = v }
|
||||
if v := hornerEval(coeffs, x); v > hornerMax { hornerMax = v }
|
||||
}
|
||||
time.Sleep(time.Duration(10+rng.Intn(60)) * time.Millisecond)
|
||||
|
||||
// Phase 8: DJB2 + FNV1a hash chains 500 iterations
|
||||
hDJB := djb2Go("bench-go-start")
|
||||
hFNV := fnv1aGo("bench-go-start")
|
||||
for i := 0; i < 500; i++ {
|
||||
hDJB = djb2Go(fmt.Sprintf("%d", hDJB))
|
||||
hFNV = fnv1aGo(fmt.Sprintf("%d", hFNV))
|
||||
}
|
||||
time.Sleep(time.Duration(10+rng.Intn(50)) * time.Millisecond)
|
||||
|
||||
// Phase 9: Collatz max steps for 1..500
|
||||
collatzMax, collatzN := 0, 0
|
||||
for n := 1; n <= 500; n++ {
|
||||
if s := collatzGo(n); s > collatzMax { collatzMax = s; collatzN = n }
|
||||
}
|
||||
|
||||
// Phase 10: GCD/LCM table for first 20 primes
|
||||
gcdSum, lcmMod := 0, 1
|
||||
for i := 0; i < 20 && i < len(primes)-1; i++ {
|
||||
gcdSum += gcdGo(primes[i], primes[i+1])
|
||||
lcmMod = lcmGo(lcmMod, primes[i]) % 1000000007
|
||||
}
|
||||
time.Sleep(time.Duration(10+rng.Intn(40)) * time.Millisecond)
|
||||
|
||||
// Phase 11: Twin primes and cousin primes
|
||||
twins, cousins := 0, 0
|
||||
for i := 0; i < len(primes)-1; i++ {
|
||||
diff := primes[i+1] - primes[i]
|
||||
if diff == 2 { twins++ }
|
||||
if diff == 4 { cousins++ }
|
||||
}
|
||||
|
||||
// Phase 12: String operations — build prime list string and count digits
|
||||
var sb strings.Builder
|
||||
for i, p := range primes { if i >= 100 { break }; fmt.Fprintf(&sb, "%d,", p) }
|
||||
primeStr := sb.String()
|
||||
digitCount := 0
|
||||
for _, c := range primeStr { if c >= '0' && c <= '9' { digitCount++ } }
|
||||
|
||||
result := map[string]interface{}{
|
||||
"language": "go",
|
||||
"delay_ms": delayMs,
|
||||
"prime_count": len(primes),
|
||||
"prime_sum": primeSum,
|
||||
"mr_count": mrCount,
|
||||
"fib55": entries[7].Fib,
|
||||
"lucas55": entries[7].Lucas,
|
||||
"mat_sum": math.Round(matSum*10000) / 10000,
|
||||
"sort_match": sortMatch,
|
||||
"poly_max": math.Round(polyMax*10000) / 10000,
|
||||
"horner_max": math.Round(hornerMax*10000) / 10000,
|
||||
"djb2_final": hDJB,
|
||||
"fnv1a_final": hFNV,
|
||||
"collatz_max": collatzMax,
|
||||
"collatz_n": collatzN,
|
||||
"twin_primes": twins,
|
||||
"cousin_primes": cousins,
|
||||
"gcd_sum": gcdSum,
|
||||
"lcm_mod": lcmMod,
|
||||
"digit_count": digitCount,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
"""
|
||||
|
||||
# ── Python ────────────────────────────────────────────────────────────────────
|
||||
PYTHON_CODE = r"""import time
|
||||
@@ -1083,7 +782,7 @@ print("=== Удаляем старые bench-функции ===")
|
||||
for name, _, _ in functions:
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-X", "DELETE", f"{BASE}/functions/{name}",
|
||||
"-H", f"X-Auth-Token: {SUB}"],
|
||||
"-H", f"X-Test-Sub: {SUB}"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
print(f" DELETE {name}: {result.stdout.strip()[:80]}")
|
||||
@@ -1094,7 +793,7 @@ for name, lang, code in functions:
|
||||
size_kb = len(code) / 1024
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-X", "POST", f"{BASE}/functions",
|
||||
"-H", f"X-Auth-Token: {SUB}", "-H", "Content-Type: application/json",
|
||||
"-H", f"X-Test-Sub: {SUB}", "-H", "Content-Type: application/json",
|
||||
"-d", payload],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
|
||||
@@ -1,82 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#!/bin/bash
|
||||
# cleanup_test_namespaces.sh — удаляет ТОЛЬКО конкретные тестовые namespace
|
||||
# Использование: cleanup_test_namespaces.sh user1@test.local [user2@test.local ...]
|
||||
#
|
||||
# ВАЖНО: удаляет ТОЛЬКО NS, соответствующие переданным пользователям.
|
||||
# НЕ удаляет NS "по маске test-*" — только по конкретным хэшам.
|
||||
# Это предотвращает случайное удаление тестовых NS от других тестов.
|
||||
#
|
||||
# Переменная окружения: KUBECTL_DIRECT=1 (если использовать kubectl напрямую, иначе через SSH)
|
||||
|
||||
SSH_ARGS=(
|
||||
-i ~/.ssh/naeel_vm_id_ed25519
|
||||
-o StrictHostKeyChecking=no
|
||||
-o ConnectTimeout=10
|
||||
naeel@5.172.178.213
|
||||
)
|
||||
set -uo pipefail
|
||||
|
||||
kube_exec() {
|
||||
if [ "${KUBECTL_DIRECT:-0}" = "1" ]; then
|
||||
kubectl "$@"
|
||||
else
|
||||
ssh "${SSH_ARGS[@]}" "kubectl $*"
|
||||
fi
|
||||
}
|
||||
|
||||
# Зеркало логики Go NamespaceForSub:
|
||||
# sub начинается с "test-" → "fission-test-" + sha256[:8*2]
|
||||
# иначе → "fission-" + sha256[:8*2]
|
||||
namespace_for_sub() {
|
||||
local sub="$1"
|
||||
local hash
|
||||
hash=$(printf '%s' "$sub" | sha256sum | awk '{print substr($1, 1, 16)}')
|
||||
if [[ "$sub" == test-* ]]; then
|
||||
echo "fission-test-${hash}"
|
||||
else
|
||||
echo "fission-${hash}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ⛔ АБСОЛЮТНЫЙ GUARD: удалять ТОЛЬКО namespace-ы вида fission-test-*
|
||||
# Всё остальное — немедленный выход с ошибкой
|
||||
delete_ns() {
|
||||
local ns="$1"
|
||||
[ -n "$ns" ] || return 0
|
||||
|
||||
# ЖЁСТКАЯ ЗАЩИТА: только fission-test-* разрешено удалять
|
||||
if [[ "$ns" != fission-test-* ]]; then
|
||||
echo "⛔ ОТКАЗ: '$ns' не является тестовым namespace (должно начинаться с 'fission-test-')" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${KUBECTL_DIRECT:-0}" = "1" ]; then
|
||||
kubectl delete namespace "$ns" --wait=false >/dev/null 2>&1 || true
|
||||
else
|
||||
ssh "${SSH_ARGS[@]}" "kubectl delete namespace ${ns} --wait=false >/dev/null 2>&1 || true"
|
||||
fi
|
||||
echo "cleanup namespace: ${ns}"
|
||||
}
|
||||
|
||||
# --all-test: удалить все namespace-ы с лейблом fission-console/env=test
|
||||
# (только fission-test-* по определению)
|
||||
delete_all_test() {
|
||||
local ns_list
|
||||
if [ "${KUBECTL_DIRECT:-0}" = "1" ]; then
|
||||
ns_list=$(kubectl get ns -l 'fission-console/env=test' --no-headers \
|
||||
-o custom-columns=":metadata.name" 2>/dev/null || true)
|
||||
else
|
||||
ns_list=$(ssh "${SSH_ARGS[@]}" \
|
||||
'kubectl get ns -l "fission-console/env=test" --no-headers -o custom-columns=":metadata.name" 2>/dev/null || true')
|
||||
fi
|
||||
printf '%s\n' "$ns_list" | while IFS= read -r ns; do
|
||||
[ -n "$ns" ] || continue
|
||||
delete_ns "$ns"
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "usage: $0 SUB [SUB ...] | --all-test" >&2
|
||||
exit 1
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 user1@test.local [user2@test.local ...]"
|
||||
echo "Deletes only namespaces for specified test users"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" = "--all-test" ]; then
|
||||
delete_all_test
|
||||
exit 0
|
||||
if [ "${KUBECTL_DIRECT:-0}" = "1" ]; then
|
||||
KUBECTL_CMD="kubectl"
|
||||
else
|
||||
SSH="ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213"
|
||||
KUBECTL_CMD="$SSH kubectl"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$@" | awk 'NF && !seen[$0]++' | while IFS= read -r sub; do
|
||||
delete_ns "$(namespace_for_sub "$sub")"
|
||||
done
|
||||
declare -a NS_TO_DELETE=()
|
||||
|
||||
# Вычисляем хэши ТОЛЬКО для переданных пользователей
|
||||
# Fission console создаёт NS как fission-<hash(email)>
|
||||
for user in "$@"; do
|
||||
# Вычисляем хэш email (console использует sha256sum и берёт первые 16 символов)
|
||||
ns_hash=$(printf '%s' "$user" | sha256sum | head -c 16)
|
||||
ns_name="fission-${ns_hash}"
|
||||
NS_TO_DELETE+=("$ns_name")
|
||||
echo " [DEBUG] user='$user' → ns='$ns_name'"
|
||||
done
|
||||
|
||||
# Удаляем ТОЛЬКО те NS, которые явно переданы в параметрах
|
||||
# (не удаляем NS "по маске" — только по конкретным хэшам)
|
||||
if [ ${#NS_TO_DELETE[@]} -gt 0 ]; then
|
||||
for ns in "${NS_TO_DELETE[@]}"; do
|
||||
if $KUBECTL_CMD get ns "$ns" &>/dev/null 2>&1; then
|
||||
echo " Deleting namespace: $ns"
|
||||
$KUBECTL_CMD delete ns "$ns" --wait=false 2>/dev/null || true
|
||||
else
|
||||
echo " Namespace not found or already deleted: $ns"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " No namespaces to delete"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib.sh — общие утилиты для тест-скриптов fission-console.
|
||||
# Подключается через: source "$(dirname "$0")/lib.sh"
|
||||
|
||||
BASE="${BASE:-https://fission.kube5s.ru/console/api}"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { PASS=$((PASS+1)); echo " PASS: $1"; }
|
||||
fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; }
|
||||
|
||||
check_http() {
|
||||
local got="$1" want="$2" label="$3"
|
||||
[ "$got" = "$want" ] && pass "$label (HTTP $got)" || fail "$label — got $got, want $want"
|
||||
}
|
||||
|
||||
# json_field PAYLOAD KEY — извлекает строковое значение ключа из JSON
|
||||
json_field() {
|
||||
local payload="$1" key="$2"
|
||||
printf '%s' "$payload" | python3 -c "
|
||||
import json, sys
|
||||
key = sys.argv[1]
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
v = d.get(key)
|
||||
if v is None:
|
||||
print('')
|
||||
elif isinstance(v, (dict, list)):
|
||||
print(json.dumps(v))
|
||||
else:
|
||||
print(v)
|
||||
except Exception:
|
||||
print('')
|
||||
" "$key" 2>/dev/null
|
||||
}
|
||||
|
||||
# json_escape — читает stdin, возвращает JSON-строку
|
||||
json_escape() {
|
||||
python3 -c 'import json, sys; print(json.dumps(sys.stdin.read()))'
|
||||
}
|
||||
|
||||
# json_len PAYLOAD — длина JSON-массива
|
||||
json_len() {
|
||||
printf '%s' "$1" | python3 -c "import json,sys
|
||||
try:
|
||||
print(len(json.load(sys.stdin)))
|
||||
except Exception:
|
||||
print(0)" 2>/dev/null
|
||||
}
|
||||
|
||||
# wait_invoke SUB FN EXPECTED [TRIES=12] [DELAY=5]
|
||||
# Ждёт пока invoke вернёт status=200 и тело содержит EXPECTED.
|
||||
# Возвращает 0 при успехе, 1 при таймауте.
|
||||
wait_invoke() {
|
||||
local sub="$1" fn="$2" expected="$3"
|
||||
local tries="${4:-12}" delay="${5:-5}"
|
||||
local try resp status raw err
|
||||
for try in $(seq 1 "$tries"); do
|
||||
resp=$(curl -s --max-time 50 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${sub}" -H "Content-Type: application/json" -d '{}')
|
||||
status=$(json_field "$resp" status)
|
||||
raw=$(json_field "$resp" response_raw)
|
||||
err=$(json_field "$resp" error)
|
||||
echo " TRY=$try fn=$fn status=$status raw=$(printf '%s' "$raw" | head -c 80) err=$(printf '%s' "$err" | head -c 80)"
|
||||
if [ "$status" = "200" ] && printf '%s' "$raw" | grep -qF "$expected"; then
|
||||
return 0
|
||||
fi
|
||||
[ "$try" -lt "$tries" ] && sleep "$delay"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# mk_run_id — возвращает уникальный 8-символьный hex ID
|
||||
mk_run_id() {
|
||||
(date +%s%N 2>/dev/null; od -An -N4 -tu4 /dev/urandom 2>/dev/null) \
|
||||
| sha256sum | head -c 8
|
||||
}
|
||||
|
||||
# setup_cleanup CLEANUP_FN — регистрирует trap EXIT
|
||||
setup_cleanup() {
|
||||
trap "$1" EXIT
|
||||
}
|
||||
|
||||
# print_summary — итог тестов
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "========================================"
|
||||
printf " ИТОГ: PASS=%d FAIL=%d\n" "$PASS" "$FAIL"
|
||||
echo "========================================"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# pre_cleanup.sh — удаляет ВСЕ тест-namespace-ы fission от прошлых прогонов.
|
||||
# Паттерны: fission-<hex16> для namespace-ов созданных тест-субами вида
|
||||
# crud-*/ttl-*/invoke-*/update-*/isolation-*/stress-*/alice-*/bob-*/
|
||||
# linter-*@test.local
|
||||
# Вызывается из run_all.sh с KUBECTL_DIRECT=1.
|
||||
set -uo pipefail
|
||||
|
||||
if [ "${KUBECTL_DIRECT:-0}" != "1" ]; then
|
||||
echo "pre_cleanup.sh: нужен KUBECTL_DIRECT=1 (должен запускаться на VM)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CLEANUP="${BASH_SOURCE%/*}/cleanup_test_namespaces.sh"
|
||||
if [ ! -x "$CLEANUP" ]; then
|
||||
echo "pre_cleanup.sh: cleanup_test_namespaces.sh не найден" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Список тест-субов создаёт namespace с именем fission-<hex16>.
|
||||
# Получить все fission-namespace-ы кроме системного fission:
|
||||
NS_LIST=$(kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null | \
|
||||
grep -E '^fission-[0-9a-f]{16}$' || true)
|
||||
|
||||
if [ -z "$NS_LIST" ]; then
|
||||
echo "pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
COUNT=$(echo "$NS_LIST" | wc -l)
|
||||
echo "pre_cleanup.sh: найдено $COUNT тест-namespace-ов, удаляем..."
|
||||
|
||||
# cleanup_test_namespaces.sh принимает email-субы, а не namespace-ы.
|
||||
# Но мы можем вызвать kubectl delete напрямую для namespace-ов.
|
||||
echo "$NS_LIST" | xargs -r kubectl delete namespace --ignore-not-found 2>&1 | \
|
||||
grep -v "^$" || true
|
||||
|
||||
echo "pre_cleanup.sh: очистка завершена."
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# run_all.sh — запускает все тест-скрипты по очереди, выводит итог.
|
||||
# Каждый скрипт независим и имеет собственный cleanup.
|
||||
# При падении одного скрипта остальные продолжают выполняться.
|
||||
#
|
||||
# Запуск: bash run_all.sh
|
||||
# Запуск одного: bash run_all.sh test_auth.sh
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
# Авто-логирование: если stdout — терминал, пишем одновременно в файл
|
||||
LOG_DIR="${ROOT_DIR}/../test-results"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="${LOG_DIR}/$(date +%Y-%m-%d_%H-%M-%S).log"
|
||||
# Перезапуск с tee только если ещё не логируем (нет переменной _LOGGING)
|
||||
if [ -z "${_LOGGING:-}" ]; then
|
||||
export _LOGGING=1
|
||||
exec bash "$0" "$@" 2>&1 | tee "$LOG_FILE"
|
||||
exit $?
|
||||
fi
|
||||
echo "LOG: $LOG_FILE"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
TOTAL_PASS=0
|
||||
TOTAL_FAIL=0
|
||||
FAILED_SCRIPTS=()
|
||||
|
||||
run_script() {
|
||||
local script="$1"
|
||||
local name
|
||||
name=$(basename "$script")
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " ЗАПУСК: $name"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
local exit_code=0
|
||||
bash "$script" || exit_code=$?
|
||||
|
||||
if [ "$exit_code" -eq 0 ]; then
|
||||
echo -e "${GREEN} ✓ $name — PASSED${NC}"
|
||||
TOTAL_PASS=$((TOTAL_PASS+1))
|
||||
else
|
||||
echo -e "${RED} ✗ $name — FAILED (exit $exit_code)${NC}"
|
||||
TOTAL_FAIL=$((TOTAL_FAIL+1))
|
||||
FAILED_SCRIPTS+=("$name")
|
||||
fi
|
||||
}
|
||||
|
||||
# Список скриптов в порядке выполнения
|
||||
# (быстрые сначала, invoke/stress — последними)
|
||||
SCRIPTS=(
|
||||
"${ROOT_DIR}/test_auth.sh"
|
||||
"${ROOT_DIR}/test_functions_crud.sh"
|
||||
"${ROOT_DIR}/test_ttl.sh"
|
||||
"${ROOT_DIR}/test_isolation.sh"
|
||||
"${ROOT_DIR}/test_update.sh"
|
||||
"${ROOT_DIR}/test_invoke.sh"
|
||||
"${ROOT_DIR}/test_linters.sh"
|
||||
"${ROOT_DIR}/test_stress.sh"
|
||||
)
|
||||
|
||||
# Если передан аргумент — запускаем только его
|
||||
if [ "$#" -ge 1 ]; then
|
||||
SCRIPTS=("${ROOT_DIR}/$1")
|
||||
fi
|
||||
|
||||
# Предварительная очистка тест-namespace-ов от прошлых прогонов.
|
||||
# Запускается только если cleanup_test_namespaces.sh умеет wildcard.
|
||||
PRE_CLEANUP="${ROOT_DIR}/pre_cleanup.sh"
|
||||
if [ -x "$PRE_CLEANUP" ]; then
|
||||
echo ""
|
||||
echo ">>> Предварительная очистка тест-namespace-ов..."
|
||||
KUBECTL_DIRECT=1 bash "$PRE_CLEANUP" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ fission-console — полный тест-сьют ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
for script in "${SCRIPTS[@]}"; do
|
||||
if [ ! -f "$script" ]; then
|
||||
echo -e "${YELLOW} SKIP: $script не найден${NC}"
|
||||
continue
|
||||
fi
|
||||
run_script "$script"
|
||||
done
|
||||
|
||||
END_TIME=$(date +%s)
|
||||
ELAPSED=$((END_TIME - START_TIME))
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
printf "║ Скриптов: PASS=%-3d FAIL=%-3d %3ds ║\n" "$TOTAL_PASS" "$TOTAL_FAIL" "$ELAPSED"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
|
||||
if [ "${#FAILED_SCRIPTS[@]}" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Упавшие скрипты:"
|
||||
for s in "${FAILED_SCRIPTS[@]}"; do
|
||||
echo -e " ${RED}✗ $s${NC}"
|
||||
done
|
||||
fi
|
||||
|
||||
[ "$TOTAL_FAIL" -eq 0 ]
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# run_remaining.sh — ВРЕМЕННЫЙ: только test_stress.sh (после обрыва SSH)
|
||||
# Удалить после прогона.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
LOG_DIR="${ROOT_DIR}/../test-results"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="${LOG_DIR}/$(date +%Y-%m-%d_%H-%M-%S)_remaining.log"
|
||||
if [ -z "${_LOGGING:-}" ]; then
|
||||
export _LOGGING=1
|
||||
exec bash "$0" "$@" 2>&1 | tee "$LOG_FILE"
|
||||
exit $?
|
||||
fi
|
||||
echo "LOG: $LOG_FILE"
|
||||
|
||||
bash "${ROOT_DIR}/test_stress.sh"
|
||||
EXIT=$?
|
||||
echo ""
|
||||
if [ $EXIT -eq 0 ]; then
|
||||
echo " ✓ test_stress.sh — PASSED"
|
||||
else
|
||||
echo " ✗ test_stress.sh — FAILED (exit $EXIT)"
|
||||
fi
|
||||
exit $EXIT
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# smoke_live_multiuser.sh — smoke test для параллельного создания функций несколькими пользователями
|
||||
#
|
||||
# ВАЖНО: Cleanup правило
|
||||
# - Тест отслеживает пользователей в USERS массиве
|
||||
# - cleanup() передает ТОЛЬКО этих пользователей в cleanup_test_namespaces.sh
|
||||
# - cleanup_test_namespaces.sh удаляет ТОЛЬКО NS для переданных пользователей
|
||||
# - Это предотвращает случайное удаление NS от других тестов
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE="https://fission.kube5s.ru/console/api"
|
||||
@@ -27,7 +35,7 @@ init_call() {
|
||||
local code=""
|
||||
local try
|
||||
for try in 1 2 3; do
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 -X GET "${BASE}/functions" -H "X-Auth-Token: ${user}" 2>/dev/null || echo 000)
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 -X GET "${BASE}/functions" -H "X-Test-Sub: ${user}" 2>/dev/null || echo 000)
|
||||
echo "INIT_TRY user=${user} try=${try} http=${code}" >&2
|
||||
if [ "$code" = "200" ]; then
|
||||
printf '%s' "$code"
|
||||
@@ -44,7 +52,7 @@ invoke_call() {
|
||||
local code=""
|
||||
local try
|
||||
for try in 1 2 3 4; do
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 40 -X POST "${BASE}/functions/${fn}/invoke" -H "X-Auth-Token: ${user}" -H "Content-Type: application/json" -d '{}' 2>/dev/null || echo 000)
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 40 -X POST "${BASE}/functions/${fn}/invoke" -H "X-Test-Sub: ${user}" -H "Content-Type: application/json" -d '{}' 2>/dev/null || echo 000)
|
||||
echo "INVOKE_TRY user=${user} fn=${fn} try=${try} http=${code}" >&2
|
||||
if [ "$code" = "200" ]; then
|
||||
printf '%s' "$code"
|
||||
@@ -58,9 +66,9 @@ invoke_call() {
|
||||
echo "=== MULTIUSER TEST START run=$RUN_ID count=$COUNT ==="
|
||||
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
USERS+=("${RUN_ID}-u${i}@test.local")
|
||||
USERS+=("test-${RUN_ID}-u${i}@test.local")
|
||||
(
|
||||
U="${RUN_ID}-u${i}@test.local"
|
||||
U="test-${RUN_ID}-u${i}@test.local"
|
||||
CODE=$(init_call "${U}")
|
||||
echo "$i:$CODE:$U" > "${TMP}/init-$i"
|
||||
) &
|
||||
@@ -81,12 +89,12 @@ echo "INIT_OK=$INIT_OK/$COUNT"
|
||||
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
(
|
||||
U="${RUN_ID}-u${i}@test.local"
|
||||
U="test-${RUN_ID}-u${i}@test.local"
|
||||
FN="mtfn-${RUN_ID}-${i}"
|
||||
CODE_TEXT=$(printf 'def main():\n return "ok-%s-%s"\n' "$RUN_ID" "$i")
|
||||
CODE_JSON=$(printf '%s' "$CODE_TEXT" | json_escape)
|
||||
BODY=$(printf '{"name":"%s","language":"python","code":%s}' "$FN" "$CODE_JSON")
|
||||
C=$(curl -s -o /dev/null -w "%{http_code}" --max-time 60 -X POST "${BASE}/functions" -H "X-Auth-Token: ${U}" -H "Content-Type: application/json" -d "$BODY" 2>/dev/null || echo 000)
|
||||
C=$(curl -s -o /dev/null -w "%{http_code}" --max-time 60 -X POST "${BASE}/functions" -H "X-Test-Sub: ${U}" -H "Content-Type: application/json" -d "$BODY" 2>/dev/null || echo 000)
|
||||
echo "$i:$C:$FN:$U" > "${TMP}/create-$i"
|
||||
) &
|
||||
done
|
||||
@@ -108,7 +116,7 @@ sleep 8
|
||||
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
(
|
||||
U="${RUN_ID}-u${i}@test.local"
|
||||
U="test-${RUN_ID}-u${i}@test.local"
|
||||
FN="mtfn-${RUN_ID}-${i}"
|
||||
C=$(invoke_call "${U}" "${FN}")
|
||||
echo "$i:$C" > "${TMP}/invoke-$i"
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# smoke_user_journeys.sh — smoke test для параллельных пользовательских journey (CRUD функций)
|
||||
#
|
||||
# ВАЖНО: Cleanup правило
|
||||
# - Тест отслеживает пользователей в USERS массиве
|
||||
# - cleanup() передает ТОЛЬКО этих пользователей в cleanup_test_namespaces.sh
|
||||
# - cleanup_test_namespaces.sh удаляет ТОЛЬКО NS для переданных пользователей
|
||||
# - Это предотвращает случайное удаление NS от других тестов
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE="https://fission.kube5s.ru/console/api"
|
||||
@@ -39,22 +47,22 @@ create_call() {
|
||||
local sub="$1"
|
||||
local body="$2"
|
||||
curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${sub}" -H 'Content-Type: application/json' -d "$body"
|
||||
-H "X-Test-Sub: ${sub}" -H 'Content-Type: application/json' -d "$body"
|
||||
}
|
||||
|
||||
invoke_call() {
|
||||
local sub="$1"
|
||||
local name="$2"
|
||||
curl -s --max-time 45 -X POST "${BASE}/functions/${name}/invoke" \
|
||||
-H "X-Auth-Token: ${sub}" -H 'Content-Type: application/json' -d '{}' || true
|
||||
-H "X-Test-Sub: ${sub}" -H 'Content-Type: application/json' -d '{}' || true
|
||||
}
|
||||
|
||||
echo "=== USER JOURNEYS START run=$RUN_ID count=$COUNT ==="
|
||||
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
USERS+=("${RUN_ID}-u${i}@test.local")
|
||||
USERS+=("test-${RUN_ID}-u${i}@test.local")
|
||||
(
|
||||
U="${RUN_ID}-u${i}@test.local"
|
||||
U="test-${RUN_ID}-u${i}@test.local"
|
||||
FN="journey-${RUN_ID}-${i}"
|
||||
RESULT="PASS"
|
||||
|
||||
@@ -90,9 +98,9 @@ for i in $(seq 1 "$COUNT"); do
|
||||
done
|
||||
[ "$INV_STATUS" = "200" ] || RESULT="FAIL invoke=${INV_STATUS}"
|
||||
|
||||
DEL1=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/${FN}" -H "X-Auth-Token: ${U}")
|
||||
DEL1=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/${FN}" -H "X-Test-Sub: ${U}")
|
||||
[ "$DEL1" = "200" ] || RESULT="FAIL delete1=${DEL1}"
|
||||
DEL2=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/${FN}" -H "X-Auth-Token: ${U}")
|
||||
DEL2=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/${FN}" -H "X-Test-Sub: ${U}")
|
||||
[ "$DEL2" = "404" ] || RESULT="FAIL delete2=${DEL2}"
|
||||
|
||||
echo "$i:$RESULT" > "${TMP}/user-$i"
|
||||
|
||||
+1
-12
@@ -1,21 +1,10 @@
|
||||
#!/bin/bash
|
||||
BASE="https://fission.kube5s.ru/console/api"
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
|
||||
TEST_SUB="len59direct@test.local"
|
||||
|
||||
cleanup() {
|
||||
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
|
||||
"$NS_CLEANUP_SCRIPT" "$TEST_SUB" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
NAME59=$(python3 -c "print('a'*59)")
|
||||
echo "Name len: ${#NAME59}"
|
||||
echo "Pkg name would be: ${NAME59}-pkg (len=$(python3 -c "print(59+4)"))"
|
||||
curl -s -w "\nHTTP:%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${TEST_SUB}" \
|
||||
-H "X-Test-Sub: len59direct@test.local" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw "{\"name\":\"${NAME59}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>42\"}"
|
||||
echo
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_auth.sh — тестирует аутентификацию:
|
||||
# - POST /console/api/auth (login endpoint)
|
||||
# - X-Auth-Token header (authMiddleware)
|
||||
# - Authorization: Bearer header
|
||||
# - DemoAuth: любая строка ≥6 символов
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
|
||||
RUN_ID=$(mk_run_id)
|
||||
SUB="test-auth-${RUN_ID}@test.local"
|
||||
|
||||
cleanup() {
|
||||
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
|
||||
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_auth.sh run=$RUN_ID"
|
||||
echo "========================================"
|
||||
|
||||
# ── POST /auth ──────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> POST /auth: пустой токен → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \
|
||||
-H "Content-Type: application/json" -d '{"token":""}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "POST /auth пустой token → 400"
|
||||
|
||||
echo ""
|
||||
echo ">>> POST /auth: токен < 6 символов → 401"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \
|
||||
-H "Content-Type: application/json" -d '{"token":"ab"}')
|
||||
check_http "$(echo "$R" | tail -1)" "401" "POST /auth токен 2 символа → 401"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \
|
||||
-H "Content-Type: application/json" -d '{"token":"abcde"}')
|
||||
check_http "$(echo "$R" | tail -1)" "401" "POST /auth токен 5 символов → 401"
|
||||
|
||||
echo ""
|
||||
echo ">>> POST /auth: токен ≥6 символов → 200"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \
|
||||
-H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$CODE" "200" "POST /auth валидный токен → 200"
|
||||
NS=$(json_field "$BODY" namespace)
|
||||
printf '%s' "$NS" | grep -q '^fission-' && pass "POST /auth namespace=fission-..." || fail "POST /auth namespace format: $NS"
|
||||
OK=$(json_field "$BODY" ok)
|
||||
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "POST /auth ok=true" || fail "POST /auth ok=$OK"
|
||||
|
||||
echo ""
|
||||
echo ">>> POST /auth: невалидный JSON → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/auth" \
|
||||
-H "Content-Type: application/json" -d 'not-json')
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
[ "$CODE" = "400" ] && pass "POST /auth невалидный JSON → 400" || fail "POST /auth невалидный JSON → $CODE"
|
||||
|
||||
echo ""
|
||||
echo ">>> POST /auth: GET метод → 405"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/auth")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
[ "$CODE" = "405" ] && pass "GET /auth → 405" || fail "GET /auth → $CODE"
|
||||
|
||||
# ── X-Auth-Token header ──────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> X-Auth-Token: отсутствует → 401"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions")
|
||||
check_http "$(echo "$R" | tail -1)" "401" "GET /functions без токена → 401"
|
||||
|
||||
echo ""
|
||||
echo ">>> X-Auth-Token: токен < 6 символов → 401"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ab")
|
||||
check_http "$(echo "$R" | tail -1)" "401" "GET /functions токен 2 символа → 401"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: abcde")
|
||||
check_http "$(echo "$R" | tail -1)" "401" "GET /functions токен 5 символов → 401"
|
||||
|
||||
echo ""
|
||||
echo ">>> X-Auth-Token: токен ≥6 символов → 200"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${SUB}")
|
||||
check_http "$(echo "$R" | tail -1)" "200" "GET /functions валидный X-Auth-Token → 200"
|
||||
|
||||
echo ""
|
||||
echo ">>> Authorization: Bearer → 200"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" \
|
||||
-H "Authorization: Bearer ${SUB}")
|
||||
check_http "$(echo "$R" | tail -1)" "200" "GET /functions Authorization: Bearer → 200"
|
||||
|
||||
# ── Namespace детерминирован ─────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Namespace детерминирован (одинаковый токен → одинаковый NS)"
|
||||
R1=$(curl -s -X POST "${BASE}/auth" -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}")
|
||||
R2=$(curl -s -X POST "${BASE}/auth" -H "Content-Type: application/json" -d "{\"token\":\"${SUB}\"}")
|
||||
NS1=$(json_field "$R1" namespace)
|
||||
NS2=$(json_field "$R2" namespace)
|
||||
[ "$NS1" = "$NS2" ] && [ -n "$NS1" ] && pass "Namespace детерминирован: $NS1" || fail "Namespace не детерминирован: '$NS1' vs '$NS2'"
|
||||
|
||||
print_summary
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Тест загрузки функций из архива для 4 языков
|
||||
# НЕ запускать автоматически! Только ручной вызов из run_all.sh или отдельно.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
API_URL="https://fission.kube5s.ru/console/api"
|
||||
|
||||
# Примеры: test-archive-py, test-archive-node, ...
|
||||
LANGS=(py node ruby php)
|
||||
FILES=(py-hello.zip node-hello.zip ruby-hello.zip php-hello.zip)
|
||||
RUNTIMES=(python nodejs ruby php)
|
||||
|
||||
for i in ${!LANGS[@]}; do
|
||||
lang="${LANGS[$i]}"
|
||||
file="${FILES[$i]}"
|
||||
runtime="${RUNTIMES[$i]}"
|
||||
FN="t-archive-${lang}-$(date +%s%N | tail -c 6)"
|
||||
SUB="test-archive-${lang}@test.local"
|
||||
echo "=== [${lang}] Тест архива: $file, runtime: $runtime, fn: $FN ==="
|
||||
# 1. Загрузка архива (multipart)
|
||||
RESP=$(curl -s -X POST "$API_URL/packages" \
|
||||
-H "X-Auth-Token: $SUB" \
|
||||
-F "file=@../test-archives/$file" \
|
||||
-F "runtime=$runtime")
|
||||
PKG_ID=$(echo "$RESP" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
if [[ -z "$PKG_ID" ]]; then
|
||||
echo "[FAIL] Не удалось загрузить архив ($file): $RESP"
|
||||
continue
|
||||
fi
|
||||
echo "[OK] Архив загружен, package id: $PKG_ID"
|
||||
# 2. Создание функции
|
||||
RESP=$(curl -s -X POST "$API_URL/functions" \
|
||||
-H "X-Auth-Token: $SUB" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$FN\",\"package\":\"$PKG_ID\",\"runtime\":\"$runtime\"}")
|
||||
if echo "$RESP" | grep -q '"error"'; then
|
||||
echo "[FAIL] Не удалось создать функцию: $RESP"
|
||||
continue
|
||||
fi
|
||||
echo "[OK] Функция создана: $FN"
|
||||
# 3. Вызов функции
|
||||
RESP=$(curl -s -X POST "$API_URL/functions/$FN/invoke" \
|
||||
-H "X-Auth-Token: $SUB" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}')
|
||||
echo "Invoke response: $RESP"
|
||||
# 4. Удаление функции
|
||||
curl -s -X DELETE "$API_URL/functions/$FN" -H "X-Auth-Token: $SUB" > /dev/null
|
||||
echo "[OK] Функция удалена: $FN"
|
||||
done
|
||||
|
||||
echo "Тесты архивов подготовлены. Для запуска — вручную или из run_all.sh."
|
||||
@@ -1,207 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_functions_crud.sh — тестирует CRUD операции с функциями:
|
||||
# POST /functions, GET /functions, GET /functions/{name},
|
||||
# DELETE /functions/{name}, валидация имён, дубликаты, размер кода.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
|
||||
RUN_ID=$(mk_run_id)
|
||||
SUB="test-crud-${RUN_ID}@test.local"
|
||||
|
||||
cleanup() {
|
||||
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
|
||||
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_functions_crud.sh run=$RUN_ID"
|
||||
echo "========================================"
|
||||
|
||||
JS_CODE='module.exports = async function(ctx) { return { status: 200, body: "crud-ok" }; }'
|
||||
|
||||
# ── CREATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: базовый nodejs → 201"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"t-crud-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$CODE" "201" "CREATE nodejs → 201"
|
||||
FN_NAME=$(json_field "$BODY" name)
|
||||
FN_ROUTE=$(json_field "$BODY" route)
|
||||
FN_PKG=$(json_field "$BODY" package)
|
||||
[ "$FN_NAME" = "t-crud-fn-${RUN_ID}" ] && pass "CREATE: name в ответе" || fail "CREATE: name='$FN_NAME'"
|
||||
printf '%s' "$FN_ROUTE" | grep -q '/' && pass "CREATE: route непустой" || fail "CREATE: route='$FN_ROUTE'"
|
||||
[ -n "$FN_PKG" ] && pass "CREATE: package непустой" || fail "CREATE: package пустой"
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: дубликат → 409"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"t-crud-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}")
|
||||
check_http "$(echo "$R" | tail -1)" "409" "CREATE дубликат → 409"
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: без name → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"language":"nodejs","code":"x"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE без name → 400"
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: без language → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"no-lang","code":"x"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE без language → 400"
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: без code → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"no-code","language":"nodejs"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE без code → 400"
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: невалидный язык → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"bad-lang","language":"cobol","code":"x"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE язык=cobol → 400"
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: валидация имени"
|
||||
|
||||
# Заглавная буква
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"BadName","language":"nodejs","code":"x"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с заглавной → 400"
|
||||
|
||||
# Начинается с дефиса
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"-bad","language":"nodejs","code":"x"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с ведущим дефисом → 400"
|
||||
|
||||
# Заканчивается дефисом
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"bad-","language":"nodejs","code":"x"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с завершающим дефисом → 400"
|
||||
|
||||
# Пробел в имени
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"bad name","language":"nodejs","code":"x"}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE имя с пробелом → 400"
|
||||
|
||||
# 58 символов — должно быть 400 (лимит 57)
|
||||
NAME58=$(python3 -c "print('a'*58)")
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${NAME58}\",\"language\":\"nodejs\",\"code\":\"x\"}")
|
||||
check_http "$(echo "$R" | tail -1)" "400" "CREATE имя 58 символов → 400"
|
||||
|
||||
# 57 символов — должно быть 201
|
||||
NAME57=$(python3 -c "print('a'*57)")
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${NAME57}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}")
|
||||
check_http "$(echo "$R" | tail -1)" "201" "CREATE имя 57 символов → 201"
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: код > 1MB → 400 или 413"
|
||||
# Используем pipe чтобы избежать Argument list too long
|
||||
R=$(python3 -c "
|
||||
import json, sys
|
||||
payload = json.dumps({'name':'big-code','language':'nodejs','code':'x'*(1024*1024+1)})
|
||||
print(payload)
|
||||
" | curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
--data-binary @- --max-time 30)
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
( [ "$CODE" = "400" ] || [ "$CODE" = "413" ] || [ "$CODE" = "000" ] ) \
|
||||
&& pass "CREATE код > 1MB → отклонён ($CODE)" \
|
||||
|| fail "CREATE код > 1MB → got $CODE"
|
||||
|
||||
# ── LIST ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> LIST: GET /functions → 200, массив"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${SUB}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$CODE" "200" "LIST GET /functions → 200"
|
||||
COUNT=$(json_len "$BODY")
|
||||
[ "${COUNT:-0}" -ge 1 ] && pass "LIST: содержит ≥1 функцию (создали выше)" || fail "LIST: пустой массив count=$COUNT"
|
||||
|
||||
echo ""
|
||||
echo ">>> LIST: новый пользователь → пустой массив"
|
||||
EMPTY_USER="empty-${RUN_ID}@test.local"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions" -H "X-Auth-Token: ${EMPTY_USER}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$CODE" "200" "LIST новый пользователь → 200"
|
||||
COUNT=$(json_len "$BODY")
|
||||
[ "${COUNT:-99}" -eq 0 ] && pass "LIST новый пользователь → []" || fail "LIST новый пользователь → count=$COUNT"
|
||||
|
||||
# ── GET ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> GET: /functions/{name} → 200"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/t-crud-fn-${RUN_ID}" \
|
||||
-H "X-Auth-Token: ${SUB}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$CODE" "200" "GET /functions/t-crud-fn-${RUN_ID} → 200"
|
||||
GOT_NAME=$(json_field "$BODY" name)
|
||||
[ "$GOT_NAME" = "t-crud-fn-${RUN_ID}" ] && pass "GET: name совпадает" || fail "GET: name='$GOT_NAME'"
|
||||
GOT_ENV=$(json_field "$BODY" environment)
|
||||
[ -n "$GOT_ENV" ] && pass "GET: environment непустой ($GOT_ENV)" || fail "GET: environment пустой"
|
||||
|
||||
echo ""
|
||||
echo ">>> GET: несуществующая функция → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/does-not-exist-xyz" \
|
||||
-H "X-Auth-Token: ${SUB}")
|
||||
check_http "$(echo "$R" | tail -1)" "404" "GET несуществующая → 404"
|
||||
|
||||
# ── DELETE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> DELETE: /functions/{name} → 200"
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/t-crud-fn-${RUN_ID}" \
|
||||
-H "X-Auth-Token: ${SUB}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$CODE" "200" "DELETE crud-fn → 200"
|
||||
DELETED=$(json_field "$BODY" deleted)
|
||||
[ "$DELETED" = "True" ] || [ "$DELETED" = "true" ] && pass "DELETE: deleted=true" || fail "DELETE: deleted='$DELETED'"
|
||||
|
||||
echo ""
|
||||
echo ">>> DELETE: повторное удаление → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/t-crud-fn-${RUN_ID}" \
|
||||
-H "X-Auth-Token: ${SUB}")
|
||||
check_http "$(echo "$R" | tail -1)" "404" "DELETE повторное → 404"
|
||||
|
||||
echo ""
|
||||
echo ">>> GET: после DELETE → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/t-crud-fn-${RUN_ID}" \
|
||||
-H "X-Auth-Token: ${SUB}")
|
||||
check_http "$(echo "$R" | tail -1)" "404" "GET после DELETE → 404"
|
||||
|
||||
echo ""
|
||||
echo ">>> DELETE: несуществующая функция → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/does-not-exist-xyz" \
|
||||
-H "X-Auth-Token: ${SUB}")
|
||||
check_http "$(echo "$R" | tail -1)" "404" "DELETE несуществующая → 404"
|
||||
|
||||
print_summary
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_invoke.sh — тестирует вызов функций:
|
||||
# NodeJS и Python: create → wait cold start → invoke → проверка тела.
|
||||
# Invoke несуществующей функции → 404.
|
||||
# ВАЖНО: использует livetest@test.local — прогретый namespace в kube кластере.
|
||||
# Для новых namespace fission router требует перезапуска (watchedNamespaces).
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
RUN_ID=$(mk_run_id)
|
||||
# Используем прогретый namespace — fission router его знает.
|
||||
SUB="livetest@test.local"
|
||||
FN_NODE="t-node-fn-${RUN_ID}"
|
||||
FN_PY="t-py-fn-${RUN_ID}"
|
||||
|
||||
cleanup() {
|
||||
# Удаляем только конкретные функции через API, не весь namespace.
|
||||
for fn in "$FN_NODE" "$FN_PY"; do
|
||||
curl -s -X DELETE "${BASE}/functions/${fn}" -H "X-Auth-Token: ${SUB}" > /dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_invoke.sh run=$RUN_ID"
|
||||
echo "========================================"
|
||||
|
||||
# ── Invoke несуществующей ─────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> INVOKE несуществующей функции → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions/no-such-fn-xyz-${RUN_ID}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
check_http "$(echo "$R" | tail -1)" "404" "INVOKE несуществующей → 404"
|
||||
|
||||
# ── NodeJS ────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> NodeJS: create + invoke"
|
||||
JS_CODE='module.exports = async function(ctx) { return { status: 200, body: "node-invoke-ok" }; }'
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${FN_NODE}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}")
|
||||
check_http "$(echo "$R" | tail -1)" "201" "NodeJS CREATE → 201"
|
||||
|
||||
if wait_invoke "$SUB" "$FN_NODE" "node-invoke-ok" 12 5; then
|
||||
pass "NodeJS invoke → 200, тело содержит node-invoke-ok"
|
||||
else
|
||||
fail "NodeJS invoke не стабилизировался за 60s"
|
||||
fi
|
||||
|
||||
# Второй вызов — проверяем что работает warm path
|
||||
R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_NODE}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
STATUS=$(json_field "$R" status)
|
||||
[ "$STATUS" = "200" ] && pass "NodeJS второй invoke → 200 (warm)" || fail "NodeJS второй invoke → status=$STATUS"
|
||||
|
||||
# ── Python ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Python: create + invoke"
|
||||
PY_CODE=$(printf 'def main():\n return "py-invoke-ok", 200' | json_escape)
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${FN_PY}\",\"language\":\"python\",\"code\":${PY_CODE}}")
|
||||
check_http "$(echo "$R" | tail -1)" "201" "Python CREATE → 201"
|
||||
|
||||
if wait_invoke "$SUB" "$FN_PY" "py-invoke-ok" 15 5; then
|
||||
pass "Python invoke → 200, тело содержит py-invoke-ok"
|
||||
else
|
||||
fail "Python invoke не стабилизировался за 75s"
|
||||
fi
|
||||
|
||||
# ── Invoke возвращает корректные поля ─────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Invoke response fields"
|
||||
R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_NODE}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
STATUS=$(json_field "$R" status)
|
||||
LATENCY=$(json_field "$R" latency_ms)
|
||||
RAW=$(json_field "$R" response_raw)
|
||||
[ "$STATUS" = "200" ] && pass "invoke.status = 200" || fail "invoke.status = $STATUS"
|
||||
printf '%s' "$LATENCY" | grep -qE '^[0-9]+$' && pass "invoke.latency_ms = $LATENCY ms" || fail "invoke.latency_ms не число: $LATENCY"
|
||||
[ -n "$RAW" ] && pass "invoke.response_raw непустой" || fail "invoke.response_raw пустой"
|
||||
|
||||
print_summary
|
||||
@@ -1,162 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_isolation.sh — тестирует namespace изоляцию:
|
||||
# Пользователь A не видит и не может изменить функции пользователя B.
|
||||
# LIST каждого пользователя возвращает только его функции.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
|
||||
RUN_ID=$(mk_run_id)
|
||||
ALICE="test-alice-${RUN_ID}@test.local"
|
||||
BOB="test-bob-${RUN_ID}@test.local"
|
||||
|
||||
cleanup() {
|
||||
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
|
||||
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$ALICE" "$BOB" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_isolation.sh run=$RUN_ID"
|
||||
echo "========================================"
|
||||
|
||||
JS_CODE='module.exports = async function() { return { status: 200, body: "ok" }; }'
|
||||
|
||||
# ── Alice создаёт функцию ─────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Alice создаёт функцию"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${ALICE}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"t-alice-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}")
|
||||
check_http "$(echo "$R" | tail -1)" "201" "Alice CREATE t-alice-fn → 201"
|
||||
|
||||
# ── Bob не может получить функцию Alice ──────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Bob GET t-alice-fn → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/t-alice-fn" \
|
||||
-H "X-Auth-Token: ${BOB}")
|
||||
check_http "$(echo "$R" | tail -1)" "404" "Bob GET t-alice-fn → 404"
|
||||
|
||||
echo ""
|
||||
echo ">>> Bob DELETE t-alice-fn → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/t-alice-fn" \
|
||||
-H "X-Auth-Token: ${BOB}")
|
||||
check_http "$(echo "$R" | tail -1)" "404" "Bob DELETE t-alice-fn → 404"
|
||||
|
||||
echo ""
|
||||
echo ">>> Bob PUT t-alice-fn/code → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/t-alice-fn/code" \
|
||||
-H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" \
|
||||
-d '{"code":"module.exports = async () => ({ body: \"hacked\" });"}')
|
||||
check_http "$(echo "$R" | tail -1)" "404" "Bob PUT t-alice-fn/code → 404"
|
||||
|
||||
echo ""
|
||||
echo ">>> Bob INVOKE t-alice-fn → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions/t-alice-fn/invoke" \
|
||||
-H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" -d '{}')
|
||||
check_http "$(echo "$R" | tail -1)" "404" "Bob INVOKE t-alice-fn → 404"
|
||||
|
||||
# ── Alice убеждается что её функция цела ────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Alice GET t-alice-fn → 200 (не повреждена)"
|
||||
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/t-alice-fn" \
|
||||
-H "X-Auth-Token: ${ALICE}")
|
||||
check_http "$(echo "$R" | tail -1)" "200" "Alice GET t-alice-fn → 200"
|
||||
|
||||
# ── LIST изоляция ────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> LIST изоляция: Alice и Bob видят только свои функции"
|
||||
|
||||
# Bob создаёт свою функцию
|
||||
curl -s -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"t-bob-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}" > /dev/null
|
||||
|
||||
sleep 1
|
||||
|
||||
ALICE_LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${ALICE}")
|
||||
BOB_LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${BOB}")
|
||||
|
||||
# Alice видит t-alice-fn но не t-bob-fn
|
||||
ALICE_COUNT=$(json_len "$ALICE_LIST")
|
||||
ALICE_HAS_OWN=$(printf '%s' "$ALICE_LIST" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
fns = json.load(sys.stdin)
|
||||
print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 't-alice-fn'))
|
||||
except:
|
||||
print(0)" 2>/dev/null)
|
||||
ALICE_HAS_BOB=$(printf '%s' "$ALICE_LIST" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
fns = json.load(sys.stdin)
|
||||
print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 't-bob-fn'))
|
||||
except:
|
||||
print(0)" 2>/dev/null)
|
||||
|
||||
[ "${ALICE_HAS_OWN:-0}" -ge 1 ] && pass "Alice LIST содержит t-alice-fn" || fail "Alice LIST не содержит t-alice-fn (count=$ALICE_COUNT)"
|
||||
[ "${ALICE_HAS_BOB:-0}" -eq 0 ] && pass "Alice LIST не содержит t-bob-fn" || fail "Alice LIST содержит t-bob-fn — УТЕЧКА ДАННЫХ!"
|
||||
|
||||
# Bob видит t-bob-fn но не t-alice-fn
|
||||
BOB_HAS_OWN=$(printf '%s' "$BOB_LIST" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
fns = json.load(sys.stdin)
|
||||
print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 't-bob-fn'))
|
||||
except:
|
||||
print(0)" 2>/dev/null)
|
||||
BOB_HAS_ALICE=$(printf '%s' "$BOB_LIST" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
fns = json.load(sys.stdin)
|
||||
print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == 't-alice-fn'))
|
||||
except:
|
||||
print(0)" 2>/dev/null)
|
||||
|
||||
[ "${BOB_HAS_OWN:-0}" -ge 1 ] && pass "Bob LIST содержит t-bob-fn" || fail "Bob LIST не содержит t-bob-fn"
|
||||
[ "${BOB_HAS_ALICE:-0}" -eq 0 ] && pass "Bob LIST не содержит t-alice-fn" || fail "Bob LIST содержит t-alice-fn — УТЕЧКА ДАННЫХ!"
|
||||
|
||||
# ── Route изоляция ───────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Route изоляция: одинаковое имя → разные routes"
|
||||
ROUTE_A=$(printf '%s' "$ALICE_LIST" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
fns = json.load(sys.stdin)
|
||||
for f in fns:
|
||||
if f.get('metadata',{}).get('name','') == 't-alice-fn':
|
||||
print(f.get('spec',{}).get('relativeurl',''))
|
||||
break
|
||||
except:
|
||||
print('')" 2>/dev/null)
|
||||
|
||||
ROUTE_B=$(printf '%s' "$BOB_LIST" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
fns = json.load(sys.stdin)
|
||||
for f in fns:
|
||||
if f.get('metadata',{}).get('name','') == 't-bob-fn':
|
||||
print(f.get('spec',{}).get('relativeurl',''))
|
||||
break
|
||||
except:
|
||||
print('')" 2>/dev/null)
|
||||
|
||||
# Проверяем через GET (возвращает route)
|
||||
ALICE_GET=$(curl -s "${BASE}/functions/t-alice-fn" -H "X-Auth-Token: ${ALICE}")
|
||||
BOB_GET=$(curl -s "${BASE}/functions/t-bob-fn" -H "X-Auth-Token: ${BOB}")
|
||||
ROUTE_A=$(json_field "$ALICE_GET" route)
|
||||
ROUTE_B=$(json_field "$BOB_GET" route)
|
||||
[ "$ROUTE_A" != "$ROUTE_B" ] && [ -n "$ROUTE_A" ] && [ -n "$ROUTE_B" ] \
|
||||
&& pass "Route изоляция: $ROUTE_A ≠ $ROUTE_B" \
|
||||
|| fail "Route коллизия: alice=$ROUTE_A bob=$ROUTE_B"
|
||||
|
||||
print_summary
|
||||
@@ -3,6 +3,11 @@
|
||||
# Без консоли. Только kubectl + fission CLI.
|
||||
#
|
||||
# Что проверяем:
|
||||
#
|
||||
# ВАЖНО: Cleanup правило
|
||||
# - Тест создает namespace с префиксом test-l1-<timestamp>
|
||||
# - cleanup() удаляет этот NS явно через kubectl delete ns
|
||||
# - Это гарантирует немедленное удаление и не зависит от cleanup_test_namespaces.sh
|
||||
# 1. Создаём NS с меткой fission.io/managed=true
|
||||
# 2. Executor регистрирует его через NSWatcher (лог)
|
||||
# 3. Создаём Python env + function в новом NS
|
||||
@@ -12,7 +17,7 @@
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
NS="l1-test-$(date +%s | tail -c 6)"
|
||||
NS="test-l1-$(date +%s | tail -c 6)"
|
||||
PASS=0; FAIL=0
|
||||
|
||||
green() { echo -e "\033[32m PASS: $1\033[0m"; PASS=$((PASS+1)); }
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_lint_archive.sh — тестирует POST /console/api/ai/lint-archive
|
||||
# Валидные архивы всех 4 языков → ok: true
|
||||
# Архив с синтаксической ошибкой → ok: false
|
||||
# Архив > 100 KB → HTTP 413
|
||||
# Zip bomb (маленький zip, большой распакованный) → HTTP 413
|
||||
# Архив без поддерживаемых файлов → HTTP 400
|
||||
# Без поля archive → HTTP 400
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
SUB="livetest@test.local"
|
||||
ARCHIVES_DIR="${ROOT_DIR}/../test-archives"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
cleanup() { rm -rf "$TMP"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_lint_archive.sh"
|
||||
echo "========================================"
|
||||
|
||||
lint_post() {
|
||||
local zip_path="$1"
|
||||
curl -s -w "\n%{http_code}" -X POST "${BASE}/ai/lint-archive" \
|
||||
-H "X-Auth-Token: ${SUB}" \
|
||||
-F "archive=@${zip_path}"
|
||||
}
|
||||
|
||||
# ── Вспомогательная функция: создать zip с одним файлом ─────────────────────
|
||||
|
||||
make_zip() {
|
||||
local name="$1" filename="$2" content="$3"
|
||||
local dir="${TMP}/${name}"
|
||||
mkdir -p "$dir"
|
||||
printf '%s' "$content" > "${dir}/${filename}"
|
||||
(cd "$dir" && zip -q "../${name}.zip" "$filename")
|
||||
echo "${TMP}/${name}.zip"
|
||||
}
|
||||
|
||||
# ── Python валидный ───────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Python: валидный архив → ok:true"
|
||||
R=$(lint_post "${ARCHIVES_DIR}/py-hello.zip")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$HTTP" "200" "Python lint → 200"
|
||||
OK=$(json_field "$BODY" ok)
|
||||
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "Python lint ok=true" || fail "Python lint ok=$OK"
|
||||
|
||||
# ── Node.js валидный ─────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Node.js: валидный архив → ok:true"
|
||||
R=$(lint_post "${ARCHIVES_DIR}/node-hello.zip")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$HTTP" "200" "Node.js lint → 200"
|
||||
OK=$(json_field "$BODY" ok)
|
||||
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "Node.js lint ok=true" || fail "Node.js lint ok=$OK"
|
||||
|
||||
# ── Ruby валидный ────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Ruby: валидный архив → ok:true"
|
||||
R=$(lint_post "${ARCHIVES_DIR}/ruby-hello.zip")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$HTTP" "200" "Ruby lint → 200"
|
||||
OK=$(json_field "$BODY" ok)
|
||||
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "Ruby lint ok=true" || fail "Ruby lint ok=$OK"
|
||||
|
||||
# ── PHP валидный ─────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> PHP: валидный архив → ok:true"
|
||||
R=$(lint_post "${ARCHIVES_DIR}/php-hello.zip")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$HTTP" "200" "PHP lint → 200"
|
||||
OK=$(json_field "$BODY" ok)
|
||||
[ "$OK" = "True" ] || [ "$OK" = "true" ] && pass "PHP lint ok=true" || fail "PHP lint ok=$OK"
|
||||
|
||||
# ── Синтаксическая ошибка Python ─────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Python: синтаксическая ошибка → ok:false"
|
||||
BAD_PY=$(make_zip "bad-py" "bad.py" "def broken(:\n pass")
|
||||
R=$(lint_post "$BAD_PY")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$HTTP" "200" "Python bad lint → 200"
|
||||
OK=$(json_field "$BODY" ok)
|
||||
[ "$OK" = "False" ] || [ "$OK" = "false" ] && pass "Python bad lint ok=false" || fail "Python bad lint ok=$OK"
|
||||
|
||||
# ── Синтаксическая ошибка Node.js ────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Node.js: синтаксическая ошибка → ok:false"
|
||||
BAD_JS=$(make_zip "bad-js" "bad.js" "module.exports = function( { return 'broken'; };")
|
||||
R=$(lint_post "$BAD_JS")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$HTTP" "200" "Node.js bad lint → 200"
|
||||
OK=$(json_field "$BODY" ok)
|
||||
[ "$OK" = "False" ] || [ "$OK" = "false" ] && pass "Node.js bad lint ok=false" || fail "Node.js bad lint ok=$OK"
|
||||
|
||||
# ── Архив > 100 KB → 413 ─────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Архив > 100 KB → 413"
|
||||
BIG_ZIP="${TMP}/big.zip"
|
||||
# Случайные бинарные данные не сжимаются → zip гарантированно > 100 KB
|
||||
dd if=/dev/urandom bs=1024 count=130 2>/dev/null > "${TMP}/big.bin"
|
||||
(cd "$TMP" && zip -q big.zip big.bin)
|
||||
R=$(lint_post "$BIG_ZIP")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
check_http "$HTTP" "413" "Большой архив → 413"
|
||||
|
||||
# ── Нет поддерживаемых файлов → 400 ─────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Архив без поддерживаемых файлов → 400"
|
||||
NO_CODE=$(make_zip "no-code" "README.md" "# Hello")
|
||||
R=$(lint_post "$NO_CODE")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
check_http "$HTTP" "400" "Нет поддерживаемых файлов → 400"
|
||||
|
||||
# ── Без поля archive → 400 ───────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Без поля archive → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/ai/lint-archive" \
|
||||
-H "X-Auth-Token: ${SUB}")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
check_http "$HTTP" "400" "Без поля archive → 400"
|
||||
|
||||
# ── Невалидный zip (не zip-файл) → 400 ───────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> Невалидный zip (текстовый файл) → 400"
|
||||
echo "not a zip" > "${TMP}/fake.zip"
|
||||
R=$(lint_post "${TMP}/fake.zip")
|
||||
HTTP=$(echo "$R" | tail -1)
|
||||
check_http "$HTTP" "400" "Невалидный zip → 400"
|
||||
|
||||
print_summary
|
||||
Executable → Regular
+1
-7
@@ -10,7 +10,7 @@ chk() {
|
||||
escaped=$(printf '%s' "$code" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||
local resp
|
||||
resp=$(curl -s -X POST "$API" \
|
||||
-H "X-Auth-Token: livetest@test.local" \
|
||||
-H "X-Test-Sub: livetest@test.local" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"language\":\"${lang}\",\"code\":${escaped}}")
|
||||
local ok_val result_val
|
||||
@@ -58,12 +58,6 @@ chk H04 php fail $'<?php\n$x = @@@;'
|
||||
chk H05 php fail $'<?php\nfunction foo( { return 1; }'
|
||||
|
||||
echo ""
|
||||
echo "=== GO ==="
|
||||
chk G01 go ok $'package main\nimport "net/http"\nfunc Handler(w http.ResponseWriter, r *http.Request) {}'
|
||||
chk G02 go ok $'package main\nimport (\n "net/http"\n "fmt"\n)\nfunc Handler(w http.ResponseWriter, r *http.Request) {\n fmt.Fprintf(w, "ok")\n}'
|
||||
chk G03 go fail $'package main\nfunc Handler( {'
|
||||
chk G04 go fail $'package main\nimport (\n "net/http"\nfunc Handler(w http.ResponseWriter) {}'
|
||||
chk G05 go fail $'package main\nfunc Handler(w http.ResponseWriter, r *http.Request) {\n x := @@@\n}'
|
||||
|
||||
echo ""
|
||||
echo "=== ГРАНИЧНЫЕ ==="
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
#
|
||||
# Что проверяем:
|
||||
# 1. N пользователей создаются параллельно (каждый → свой NS через консоль API)
|
||||
#
|
||||
# ВАЖНО: Cleanup правило
|
||||
# - Тест отслеживает пользователей в CREATED_USERS массиве
|
||||
# - cleanup_namespaces() передает ТОЛЬКО этих пользователей в cleanup_test_namespaces.sh
|
||||
# - cleanup_test_namespaces.sh удаляет ТОЛЬКО NS для переданных пользователей
|
||||
# - Это предотвращает случайное удаление NS от других тестов
|
||||
# 2. Executor регистрирует каждый NS через NSWatcher (лог: registered namespace)
|
||||
# 3. В каждом NS создаётся функция через консоль API
|
||||
# 4. Каждая функция вызывается и отвечает правильно
|
||||
@@ -52,7 +58,7 @@ echo " Layer 1 NSWatcher — стресс-тест"
|
||||
echo " Пользователей: $COUNT run: $RUN_ID"
|
||||
echo "========================================"
|
||||
|
||||
# Каждый "пользователь" — уникальный X-Auth-Token → уникальный NS
|
||||
# Каждый "пользователь" — уникальный X-Test-Sub → уникальный NS
|
||||
# Консоль вызывает SetupFissionNamespace → NS получает fission.io/managed=true
|
||||
# Executor NSWatcher подхватывает → регистрирует без рестарта
|
||||
|
||||
@@ -63,12 +69,12 @@ echo ">>> [1/4] Создаём NS для $COUNT пользователей па
|
||||
TMPDIR_RESULTS=$(mktemp -d)
|
||||
|
||||
for i in $(seq 1 $COUNT); do
|
||||
USER="${RUN_ID}-user${i}@test.local"
|
||||
USER="test-${RUN_ID}-user${i}@test.local"
|
||||
CREATED_USERS+=("$USER")
|
||||
(
|
||||
CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 \
|
||||
-X GET "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${USER}" 2>/dev/null || echo 000)
|
||||
-H "X-Test-Sub: ${USER}" 2>/dev/null || echo 000)
|
||||
echo "$i:$CODE:$USER" > "${TMPDIR_RESULTS}/init-${i}"
|
||||
) &
|
||||
done
|
||||
@@ -133,13 +139,13 @@ json_escape() {
|
||||
}
|
||||
|
||||
for i in $(seq 1 $COUNT); do
|
||||
USER="${USERS[$i]:-${RUN_ID}-user${i}@test.local}"
|
||||
USER="${USERS[$i]:-test-${RUN_ID}-user${i}@test.local}"
|
||||
FN_NAME="testfn-${RUN_ID}"
|
||||
(
|
||||
# Создаём функцию
|
||||
CREATE=$(curl -s -w "\n%{http_code}" --max-time 60 \
|
||||
-X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${USER}" \
|
||||
-H "X-Test-Sub: ${USER}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${FN_NAME}\",\"language\":\"python\",\"code\":$(printf '%s' \"${PYTHON_CODE}\" | json_escape)}" \
|
||||
2>/dev/null)
|
||||
@@ -175,12 +181,12 @@ echo ">>> [4/4] Параллельные вызовы всех $COUNT функц
|
||||
sleep 5
|
||||
|
||||
for i in $(seq 1 $COUNT); do
|
||||
USER="${USERS[$i]:-${RUN_ID}-user${i}@test.local}"
|
||||
USER="${USERS[$i]:-test-${RUN_ID}-user${i}@test.local}"
|
||||
FN_NAME="testfn-${RUN_ID}"
|
||||
(
|
||||
CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 \
|
||||
-X POST "${BASE}/functions/${FN_NAME}/invoke" \
|
||||
-H "X-Auth-Token: ${USER}" \
|
||||
-H "X-Test-Sub: ${USER}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}' 2>/dev/null || echo 000)
|
||||
echo "$i:$CODE" > "${TMPDIR_RESULTS}/invoke-${i}"
|
||||
|
||||
@@ -158,14 +158,6 @@ sub main {
|
||||
EOF
|
||||
}
|
||||
|
||||
EOF
|
||||
cat > "${dir}/go.mod" <<'EOF'
|
||||
module fn
|
||||
|
||||
go 1.21
|
||||
EOF
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_stress.sh — стресс-тест параллельного использования:
|
||||
# COUNT пользователей создают функции параллельно
|
||||
# LIST изоляция: каждый видит только свои функции
|
||||
# Invoke стресс: INVOKE_COUNT параллельных invoke для livetest@test.local (watched namespace)
|
||||
# Cleanup: удаляем namespace-ы тест-пользователей, функции livetest — через API DELETE.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
|
||||
RUN_ID=$(mk_run_id)
|
||||
COUNT="${1:-8}"
|
||||
TMP=$(mktemp -d)
|
||||
INVOKE_SUB="livetest@test.local"
|
||||
INVOKE_COUNT=8
|
||||
|
||||
declare -a SUBS=()
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
SUBS+=("test-stress-${RUN_ID}-u${i}@test.local")
|
||||
done
|
||||
|
||||
declare -a INVOKE_FNS=()
|
||||
for i in $(seq 1 "$INVOKE_COUNT"); do
|
||||
INVOKE_FNS+=("t-sinv-${RUN_ID}-${i}")
|
||||
done
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP" 2>/dev/null || true
|
||||
if [ -x "$NS_CLEANUP_SCRIPT" ] && [ "${#SUBS[@]}" -gt 0 ]; then
|
||||
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "${SUBS[@]}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
for fn in "${INVOKE_FNS[@]}"; do
|
||||
curl -s -X DELETE "${BASE}/functions/${fn}" -H "X-Auth-Token: ${INVOKE_SUB}" > /dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_stress.sh run=$RUN_ID users=$COUNT invoke_parallel=$INVOKE_COUNT"
|
||||
echo "========================================"
|
||||
|
||||
JS_CODE='module.exports = async function() { return { status: 200, body: "stress-ok" }; }'
|
||||
|
||||
# ── Шаг 1: параллельное создание ─────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> [1/3] $COUNT пользователей создают функции параллельно..."
|
||||
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
(
|
||||
SUB="${SUBS[$((i-1))]}"
|
||||
FN="stress-fn-${RUN_ID}"
|
||||
R=$(curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}" \
|
||||
2>/dev/null)
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
echo "$i:$CODE" > "${TMP}/create-$i"
|
||||
) &
|
||||
done
|
||||
wait
|
||||
|
||||
CREATE_OK=0
|
||||
CREATE_FAIL=0
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
CODE=$(cut -d: -f2 "${TMP}/create-$i" 2>/dev/null || echo "000")
|
||||
if [ "$CODE" = "201" ] || [ "$CODE" = "200" ]; then
|
||||
CREATE_OK=$((CREATE_OK+1))
|
||||
else
|
||||
CREATE_FAIL=$((CREATE_FAIL+1))
|
||||
echo " user$i: CREATE HTTP $CODE"
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$CREATE_FAIL" -eq 0 ] \
|
||||
&& pass "Параллельный CREATE: все $COUNT пользователей → 201" \
|
||||
|| fail "Параллельный CREATE: $CREATE_FAIL/$COUNT упали"
|
||||
|
||||
# ── Шаг 2: изоляция LIST ─────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> [2/3] Изоляция: каждый видит только свою функцию"
|
||||
|
||||
ISO_OK=0
|
||||
for i in $(seq 1 "$COUNT"); do
|
||||
SUB="${SUBS[$((i-1))]}"
|
||||
FN="stress-fn-${RUN_ID}"
|
||||
LIST=$(curl -s --max-time 20 "${BASE}/functions" -H "X-Auth-Token: ${SUB}")
|
||||
OWN_COUNT=$(printf '%s' "$LIST" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
fns = json.load(sys.stdin)
|
||||
print(sum(1 for f in fns if f.get('metadata',{}).get('name','') == '${FN}'))
|
||||
except:
|
||||
print(0)" 2>/dev/null)
|
||||
TOTAL_COUNT=$(json_len "$LIST")
|
||||
if [ "${OWN_COUNT:-0}" -eq 1 ] && [ "${TOTAL_COUNT:-0}" -eq 1 ]; then
|
||||
ISO_OK=$((ISO_OK+1))
|
||||
else
|
||||
echo " user$i: own=$OWN_COUNT total=$TOTAL_COUNT"
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$ISO_OK" -eq "$COUNT" ] \
|
||||
&& pass "Изоляция: все $COUNT пользователей видят только свои функции" \
|
||||
|| fail "Изоляция: только $ISO_OK/$COUNT OK"
|
||||
|
||||
# ── Шаг 3: параллельный invoke (livetest — watched namespace) ─────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> [3/3] $INVOKE_COUNT параллельных invoke для livetest@test.local"
|
||||
|
||||
INV_JS='module.exports = async function() { return { status: 200, body: "sinv-ok" }; }'
|
||||
for i in $(seq 1 "$INVOKE_COUNT"); do
|
||||
FN_I="${INVOKE_FNS[$((i-1))]}"
|
||||
curl -s -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${FN_I}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$INV_JS" | json_escape)}" > /dev/null
|
||||
done
|
||||
|
||||
echo "Ждём прогрева $INVOKE_COUNT функций..."
|
||||
for i in $(seq 1 "$INVOKE_COUNT"); do
|
||||
FN_I="${INVOKE_FNS[$((i-1))]}"
|
||||
for try in $(seq 1 12); do
|
||||
R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_I}/invoke" \
|
||||
-H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
ST=$(json_field "$R" status)
|
||||
[ "$ST" = "200" ] && break
|
||||
echo " прогрев функции $i/$INVOKE_COUNT: попытка $try/12..."
|
||||
sleep 5
|
||||
done
|
||||
echo " функция $i/$INVOKE_COUNT — готова"
|
||||
done
|
||||
|
||||
for i in $(seq 1 "$INVOKE_COUNT"); do
|
||||
(
|
||||
FN_I="${INVOKE_FNS[$((i-1))]}"
|
||||
R=$(curl -s --max-time 30 -X POST "${BASE}/functions/${FN_I}/invoke" \
|
||||
-H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
ST=$(json_field "$R" status)
|
||||
echo "$i:$ST" > "${TMP}/sinvoke-$i"
|
||||
) &
|
||||
done
|
||||
wait
|
||||
|
||||
SINV_OK=0
|
||||
SINV_FAIL=0
|
||||
for i in $(seq 1 "$INVOKE_COUNT"); do
|
||||
CODE=$(cut -d: -f2 "${TMP}/sinvoke-$i" 2>/dev/null || echo "MISS")
|
||||
[ "$CODE" = "200" ] && SINV_OK=$((SINV_OK+1)) || SINV_FAIL=$((SINV_FAIL+1))
|
||||
done
|
||||
|
||||
[ "$SINV_FAIL" -le 1 ] \
|
||||
&& pass "Параллельный INVOKE (livetest): $SINV_OK/$INVOKE_COUNT OK" \
|
||||
|| fail "Параллельный INVOKE (livetest): $SINV_FAIL/$INVOKE_COUNT упали"
|
||||
|
||||
print_summary
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_ttl.sh — тестирует TTL функций:
|
||||
# CREATE с TTL → expires_at присутствует в ответе
|
||||
# CREATE без TTL → expires_at отсутствует
|
||||
# Невалидный TTL → 400 до создания объектов в k8s
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
|
||||
RUN_ID=$(mk_run_id)
|
||||
SUB="test-ttl-${RUN_ID}@test.local"
|
||||
|
||||
cleanup() {
|
||||
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
|
||||
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$SUB" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_ttl.sh run=$RUN_ID"
|
||||
echo "========================================"
|
||||
|
||||
JS_CODE='module.exports = async function() { return { status: 200, body: "ok" }; }'
|
||||
|
||||
# ── Без TTL ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE без TTL → expires_at отсутствует"
|
||||
R=$(curl -s -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"t-nttl-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}")
|
||||
CODE_HTTP=$(printf '%s' "$R" | python3 -c "import sys; lines=sys.stdin.read().split('\n'); print(lines[-1] if lines else '')" 2>/dev/null || echo "?")
|
||||
EXPIRES=$(json_field "$R" expires_at)
|
||||
[ -z "$EXPIRES" ] && pass "CREATE без TTL: expires_at отсутствует" || fail "CREATE без TTL: expires_at='$EXPIRES'"
|
||||
|
||||
# ── С TTL=15m ────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE с TTL=15m → expires_at присутствует"
|
||||
R=$(curl -s -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ttl15m-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"15m\"}")
|
||||
EXPIRES=$(json_field "$R" expires_at)
|
||||
[ -n "$EXPIRES" ] && pass "CREATE TTL=15m: expires_at=$EXPIRES" || fail "CREATE TTL=15m: expires_at пустой"
|
||||
# expires_at должен быть RFC3339
|
||||
printf '%s' "$EXPIRES" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T' && pass "expires_at формат RFC3339" || fail "expires_at не RFC3339: $EXPIRES"
|
||||
|
||||
# ── С TTL=1h ─────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE с TTL=1h → expires_at присутствует"
|
||||
R=$(curl -s -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ttl1h-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"1h\"}")
|
||||
EXPIRES=$(json_field "$R" expires_at)
|
||||
[ -n "$EXPIRES" ] && pass "CREATE TTL=1h: expires_at=$EXPIRES" || fail "CREATE TTL=1h: expires_at пустой"
|
||||
|
||||
# ── С TTL=2d ─────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE с TTL=2d → expires_at присутствует"
|
||||
R=$(curl -s -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ttl2d-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape),\"ttl\":\"2d\"}")
|
||||
EXPIRES=$(json_field "$R" expires_at)
|
||||
[ -n "$EXPIRES" ] && pass "CREATE TTL=2d: expires_at=$EXPIRES" || fail "CREATE TTL=2d: expires_at пустой"
|
||||
|
||||
# ── Невалидный TTL ───────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE TTL невалидный → 400 (без создания объектов)"
|
||||
for badttl in "99z" "abc" "0d" "-1h" ""; do
|
||||
if [ -z "$badttl" ]; then continue; fi
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"t-badttl-fn\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"${badttl}\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
[ "$CODE" = "400" ] && pass "TTL='$badttl' → 400" || fail "TTL='$badttl' → got $CODE"
|
||||
done
|
||||
|
||||
# ── expires_at в LIST response ───────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> LIST: функция с TTL видна в списке"
|
||||
LIST=$(curl -s "${BASE}/functions" -H "X-Auth-Token: ${SUB}")
|
||||
COUNT=$(json_len "$LIST")
|
||||
[ "${COUNT:-0}" -ge 3 ] && pass "LIST: видно ≥3 функции (2 с TTL, 1 без)" || fail "LIST: count=$COUNT"
|
||||
|
||||
print_summary
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_ui_user_flows.sh — тесты пользовательских journey как UI (auth, create, edit, delete, invoke)
|
||||
#
|
||||
# ВАЖНО: Cleanup правило
|
||||
# - Тест создает 5 пользователей (LOGIN, LANG, NODE, STACK, NEG)
|
||||
# - cleanup_namespaces() передает ТОЛЬКО этих 5 пользователей в cleanup_test_namespaces.sh
|
||||
# - cleanup_test_namespaces.sh удаляет ТОЛЬКО NS для переданных пользователей
|
||||
# - Это предотвращает случайное удаление NS от других тестов
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE="https://fission.kube5s.ru/console/api"
|
||||
@@ -8,11 +16,11 @@ RUN_ID="ui$(date +%s | tail -c 6)"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
LOGIN_SUB="ui-login-${RUN_ID}@test.local"
|
||||
LANG_SUB="ui-lang-${RUN_ID}@test.local"
|
||||
NODE_SUB="ui-node-${RUN_ID}@test.local"
|
||||
STACK_SUB="ui-stack-${RUN_ID}@test.local"
|
||||
NEG_SUB="ui-neg-${RUN_ID}@test.local"
|
||||
LOGIN_SUB="test-ui-login-${RUN_ID}@test.local"
|
||||
LANG_SUB="test-ui-lang-${RUN_ID}@test.local"
|
||||
NODE_SUB="test-ui-node-${RUN_ID}@test.local"
|
||||
STACK_SUB="test-ui-stack-${RUN_ID}@test.local"
|
||||
NEG_SUB="test-ui-neg-${RUN_ID}@test.local"
|
||||
|
||||
pass() { PASS=$((PASS+1)); echo " PASS: $1"; }
|
||||
fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; }
|
||||
@@ -64,7 +72,7 @@ create_call() {
|
||||
local sub="$1"
|
||||
local body="$2"
|
||||
curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${sub}" -H 'Content-Type: application/json' -d "$body"
|
||||
-H "X-Test-Sub: ${sub}" -H 'Content-Type: application/json' -d "$body"
|
||||
}
|
||||
|
||||
invoke_call() {
|
||||
@@ -76,7 +84,7 @@ invoke_call() {
|
||||
err_file=$(mktemp)
|
||||
rc=0
|
||||
http_code=$(curl -sS -o "$out_file" -w "%{http_code}" --max-time 60 -X POST "${BASE}/functions/${name}/invoke" \
|
||||
-H "X-Auth-Token: ${sub}" -H 'Content-Type: application/json' -d "$body" 2>"$err_file") || rc=$?
|
||||
-H "X-Test-Sub: ${sub}" -H 'Content-Type: application/json' -d "$body" 2>"$err_file") || rc=$?
|
||||
resp=$(cat "$out_file" 2>/dev/null || true)
|
||||
err_text=$(tr '\n' ' ' <"$err_file" | sed 's/[[:space:]]\+/ /g')
|
||||
rm -f "$out_file" "$err_file"
|
||||
@@ -243,9 +251,9 @@ R=$(create_call "$NEG_SUB" '{"name":"dup-ui","language":"python","entrypoint":"m
|
||||
CODE=$(printf '%s' "$R" | tail -1)
|
||||
([ "$CODE" = "409" ] || [ "$CODE" = "502" ]) && pass "duplicate create rejected" || fail "duplicate create -> got $CODE"
|
||||
|
||||
DC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/dup-ui" -H "X-Auth-Token: ${NEG_SUB}")
|
||||
DC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/dup-ui" -H "X-Test-Sub: ${NEG_SUB}")
|
||||
[ "$DC" = "200" ] && pass "first delete -> 200" || fail "first delete -> got $DC"
|
||||
DC2=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/dup-ui" -H "X-Auth-Token: ${NEG_SUB}")
|
||||
DC2=$(curl -s -o /dev/null -w "%{http_code}" --max-time 45 -X DELETE "${BASE}/functions/dup-ui" -H "X-Test-Sub: ${NEG_SUB}")
|
||||
[ "$DC2" = "404" ] && pass "second delete -> 404" || fail "second delete -> got $DC2"
|
||||
|
||||
echo ""
|
||||
@@ -274,7 +282,7 @@ else
|
||||
fail "nodejs invoke did not stabilize"
|
||||
fi
|
||||
UPD=$(curl -s -w "\n%{http_code}" --max-time 60 -X PUT "${BASE}/functions/node-ui/code" \
|
||||
-H "X-Auth-Token: ${NODE_SUB}" -H 'Content-Type: application/json' \
|
||||
-H "X-Test-Sub: ${NODE_SUB}" -H 'Content-Type: application/json' \
|
||||
-d '{"code":"module.exports = async function() { return { status: 200, body: \"node-ui-updated\" }; }"}')
|
||||
CODE=$(printf '%s' "$UPD" | tail -1)
|
||||
[ "$CODE" = "200" ] && pass "nodejs update -> 200" || fail "nodejs update -> got $CODE"
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_update.sh — тестирует обновление кода функции:
|
||||
# PUT /functions/{name}/code → invoke возвращает новый код
|
||||
# PUT несуществующей → 404
|
||||
# PUT с пустым кодом → 400
|
||||
# ВАЖНО: использует livetest@test.local — прогретый namespace в kube кластере.
|
||||
# Для новых namespace fission router требует перезапуска (watchedNamespaces).
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
RUN_ID=$(mk_run_id)
|
||||
SUB="livetest@test.local"
|
||||
FN="t-upd-fn-${RUN_ID}"
|
||||
|
||||
cleanup() {
|
||||
curl -s -X DELETE "${BASE}/functions/${FN}" -H "X-Auth-Token: ${SUB}" > /dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " test_update.sh run=$RUN_ID"
|
||||
echo "========================================"
|
||||
|
||||
# ── Создаём функцию с начальным кодом ────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> CREATE: версия v1"
|
||||
CODE_V1='module.exports = async function() { return { status: 200, body: "version-one" }; }'
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$CODE_V1" | json_escape)}")
|
||||
check_http "$(echo "$R" | tail -1)" "201" "CREATE v1 → 201"
|
||||
|
||||
echo "Ждём прогрева..."
|
||||
if wait_invoke "$SUB" "$FN" "version-one" 12 5; then
|
||||
pass "Invoke v1 → version-one"
|
||||
else
|
||||
fail "Invoke v1 не стабилизировался"
|
||||
fi
|
||||
|
||||
# ── PUT /code → 200 ──────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> PUT /code: версия v2"
|
||||
CODE_V2='module.exports = async function() { return { status: 200, body: "version-two" }; }'
|
||||
R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"code\":$(printf '%s' "$CODE_V2" | json_escape)}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | sed '$d')
|
||||
check_http "$CODE" "200" "PUT /code v2 → 200"
|
||||
UPDATED=$(json_field "$BODY" updated)
|
||||
[ "$UPDATED" = "True" ] || [ "$UPDATED" = "true" ] && pass "PUT /code: updated=true" || fail "PUT /code: updated='$UPDATED'"
|
||||
NEW_PKG=$(json_field "$BODY" package)
|
||||
[ -n "$NEW_PKG" ] && pass "PUT /code: новый package=$NEW_PKG" || fail "PUT /code: package пустой"
|
||||
|
||||
echo "Ждём пока executor подхватит новый код..."
|
||||
if wait_invoke "$SUB" "$FN" "version-two" 15 5; then
|
||||
pass "Invoke после update → version-two"
|
||||
else
|
||||
fail "Invoke после update не вернул version-two"
|
||||
fi
|
||||
|
||||
# ── PUT с пустым кодом → 400 ──────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> PUT /code: пустой код → 400"
|
||||
R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"code":""}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "PUT /code пустой код → 400"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/${FN}/code" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d '{"code":" "}')
|
||||
check_http "$(echo "$R" | tail -1)" "400" "PUT /code пробельный код → 400"
|
||||
|
||||
# ── PUT несуществующей → 404 ──────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> PUT /code: несуществующая функция → 404"
|
||||
R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/no-such-fn-xyz/code" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"code\":$(printf '%s' "$CODE_V1" | json_escape)}")
|
||||
check_http "$(echo "$R" | tail -1)" "404" "PUT /code несуществующая → 404"
|
||||
|
||||
# ── Ещё одно обновление (v3) — убеждаемся что можно делать цепочку ────────────
|
||||
|
||||
echo ""
|
||||
echo ">>> PUT /code: версия v3 (цепочка)"
|
||||
CODE_V3='module.exports = async function() { return { status: 200, body: "version-three" }; }'
|
||||
curl -s -X PUT "${BASE}/functions/${FN}/code" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"code\":$(printf '%s' "$CODE_V3" | json_escape)}" > /dev/null
|
||||
|
||||
if wait_invoke "$SUB" "$FN" "version-three" 15 5; then
|
||||
pass "Invoke после v3 update → version-three"
|
||||
else
|
||||
fail "Invoke после v3 update не стабилизировался"
|
||||
fi
|
||||
|
||||
print_summary
|
||||
Regular → Executable
+106
-98
@@ -1,4 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests_v2.sh — интеграционные тесты Fission Console (73 блока)
|
||||
#
|
||||
# ВАЖНО: Cleanup правило
|
||||
# - Каждый тест должен передавать cleanup_test_namespaces.sh ТОЛЬКО те пользователей, которых он создал
|
||||
# - cleanup_test_namespaces.sh удаляет ТОЛЬКО NS для переданных пользователей (не удаляет по маске test-*)
|
||||
# - Это предотвращает случайное удаление NS от других тестов или фоновых процессов
|
||||
# - Все пользователи должны иметь префикс "test-" для явной идентификации
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
BASE="https://fission.kube5s.ru/console/api"
|
||||
@@ -10,23 +18,23 @@ RUN_ID=$(echo "$RUN_ID" | sha256sum 2>/dev/null | head -c 8 || echo "${RUN_ID}"
|
||||
|
||||
cleanup_namespaces() {
|
||||
local subs=()
|
||||
subs+=("alice@test.local" "routetest-alice@test.local" "routetest-bob@test.local")
|
||||
subs+=("val-user@test.local" "dup-user@test.local" "upd-user@test.local" "get-user@test.local")
|
||||
subs+=("ns-u1@test.local" "ns-u2@test.local" "alice-del@test.local" "carol-del@test.local")
|
||||
subs+=("py-user@test.local" "js-user@test.local" "ttl-user@test.local" "body-user@test.local")
|
||||
subs+=("stress-user@test.local" "brand-new-empty-user-xyzqwerty@test.local" "chain-user@test.local")
|
||||
subs+=("dup2-user-${RUN_ID}@test.local" "name-user-${RUN_ID}@test.local")
|
||||
subs+=("len57-user-${RUN_ID}@test.local" "big-user-${RUN_ID}@test.local" "size-user-${RUN_ID}@test.local")
|
||||
subs+=("ttl-exp-${RUN_ID}@test.local" "race-${RUN_ID}@test.local")
|
||||
subs+=("mfunc-${RUN_ID}@test.local" "victim-${RUN_ID}@test.local" "attacker-${RUN_ID}@test.local")
|
||||
subs+=("lang-multi-${RUN_ID}@test.local" "lang-inv-${RUN_ID}@test.local")
|
||||
subs+=("test-alice@test.local" "test-routetest-alice@test.local" "test-routetest-bob@test.local")
|
||||
subs+=("test-val-user@test.local" "test-dup-user@test.local" "test-upd-user@test.local" "test-get-user@test.local")
|
||||
subs+=("test-ns-u1@test.local" "test-ns-u2@test.local" "test-alice-del@test.local" "test-carol-del@test.local")
|
||||
subs+=("test-py-user@test.local" "test-js-user@test.local" "test-ttl-user@test.local" "test-body-user@test.local")
|
||||
subs+=("test-stress-user@test.local" "test-brand-new-empty-user-xyzqwerty@test.local" "test-chain-user@test.local")
|
||||
subs+=("test-dup2-user-${RUN_ID}@test.local" "test-name-user-${RUN_ID}@test.local")
|
||||
subs+=("test-len57-user-${RUN_ID}@test.local" "test-big-user-${RUN_ID}@test.local" "test-size-user-${RUN_ID}@test.local")
|
||||
subs+=("test-ttl-exp-${RUN_ID}@test.local" "test-race-${RUN_ID}@test.local")
|
||||
subs+=("test-mfunc-${RUN_ID}@test.local" "test-victim-${RUN_ID}@test.local" "test-attacker-${RUN_ID}@test.local")
|
||||
subs+=("test-lang-multi-${RUN_ID}@test.local" "test-lang-inv-${RUN_ID}@test.local")
|
||||
|
||||
local uid
|
||||
for uid in $(seq 1 10); do
|
||||
subs+=("u${uid}-${RUN_ID}@test.local")
|
||||
subs+=("test-u${uid}-${RUN_ID}@test.local")
|
||||
done
|
||||
for uid in $(seq 1 3); do
|
||||
subs+=("fullcycle-u${uid}-${RUN_ID}@test.local")
|
||||
subs+=("test-fullcycle-u${uid}-${RUN_ID}@test.local")
|
||||
done
|
||||
|
||||
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
|
||||
@@ -51,23 +59,23 @@ echo ""
|
||||
echo ">>> БЛОК A: Фиксы v0.6.8 — регресс"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions/nonexistent-xyz-abc/invoke \
|
||||
-H "X-Auth-Token: alice@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
-H "X-Test-Sub: test-alice@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
CODE=$(echo "$R" | tail -1); BODY=$(echo "$R" | head -1)
|
||||
check_http "$CODE" "404" "INVOKE nonexistent → 404"
|
||||
echo "$BODY" | grep -q "not found" && pass "INVOKE nonexistent — тело содержит 'not found'" || fail "INVOKE nonexistent — тело: $BODY"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/nonexistent-xyz-abc \
|
||||
-H "X-Auth-Token: alice@test.local")
|
||||
-H "X-Test-Sub: test-alice@test.local")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "404" "DELETE nonexistent → 404"
|
||||
|
||||
# Создаём изолированные функции и проверяем route isolation
|
||||
ISO_NAME="iso-${RUN_ID}"
|
||||
R_A=$(curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: routetest-alice@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: test-routetest-alice@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${ISO_NAME}\",\"language\":\"nodejs\",\"code\":\"module.exports=async(ctx)=>({body:\\\"alice-iso\\\"})\",\"ttl\":\"5m\"}")
|
||||
R_B=$(curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: routetest-bob@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: test-routetest-bob@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${ISO_NAME}\",\"language\":\"nodejs\",\"code\":\"module.exports=async(ctx)=>({body:\\\"bob-iso\\\"})\",\"ttl\":\"5m\"}")
|
||||
ROUTE_A=$(echo "$R_A" | python3 -c "import sys,json; print(json.load(sys.stdin).get('route','?'))" 2>/dev/null)
|
||||
ROUTE_B=$(echo "$R_B" | python3 -c "import sys,json; print(json.load(sys.stdin).get('route','?'))" 2>/dev/null)
|
||||
@@ -77,9 +85,9 @@ sleep 10
|
||||
|
||||
# Invoke после изоляции
|
||||
INV_A=$(curl -s -X POST ${BASE}/functions/${ISO_NAME}/invoke \
|
||||
-H "X-Auth-Token: routetest-alice@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
-H "X-Test-Sub: test-routetest-alice@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
INV_B=$(curl -s -X POST ${BASE}/functions/${ISO_NAME}/invoke \
|
||||
-H "X-Auth-Token: routetest-bob@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
-H "X-Test-Sub: test-routetest-bob@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
BODY_A=$(echo "$INV_A" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null)
|
||||
BODY_B=$(echo "$INV_B" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null)
|
||||
[ "$BODY_A" = "alice-iso" ] && pass "Route isolation invoke — alice получает alice" || fail "Route isolation — alice получила: $BODY_A"
|
||||
@@ -90,27 +98,27 @@ echo ""
|
||||
echo ">>> БЛОК B: Валидация входных данных"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: test-val-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"language":"nodejs","code":"x"}')
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание без name → 400"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"nocode-fn","language":"nodejs"}')
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание без code → 400"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"nolang-fn","code":"x"}')
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание без language → 400"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"","language":"nodejs","code":"x"}')
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "400" "Создание с пустым name → 400"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: val-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"bad lang fn","language":"nodejs","code":"x"}')
|
||||
# Fission не позволяет пробелы в имени — ожидаем 4xx или 502
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
@@ -121,11 +129,11 @@ echo ""
|
||||
echo ">>> БЛОК C: Дублирующее создание"
|
||||
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: dup-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: dup-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"dup-fn","language":"nodejs","code":"module.exports=async()=>({body:\"v1\"});","ttl":"5m"}' > /dev/null
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: dup-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: dup-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"dup-fn","language":"nodejs","code":"module.exports=async()=>({body:\"v2\"});","ttl":"5m"}')
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
( [ "$CODE" = "409" ] || [ "$CODE" = "502" ] ) && pass "Дублирующее создание → конфликт (got $CODE)" || fail "Дублирующее создание → got $CODE"
|
||||
@@ -135,20 +143,20 @@ echo ""
|
||||
echo ">>> БЛОК D: UPDATE кода и повторный invoke"
|
||||
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: upd-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: upd-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"upd-fn","language":"nodejs","code":"module.exports=async()=>({body:\"before\"});","ttl":"10m"}' > /dev/null
|
||||
|
||||
sleep 3
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X PUT ${BASE}/functions/upd-fn/code \
|
||||
-H "X-Auth-Token: upd-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: upd-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"code":"module.exports=async()=>({body:\"after\"});"}')
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "UPDATE code → 200"
|
||||
|
||||
sleep 10
|
||||
|
||||
INV=$(curl -s -X POST ${BASE}/functions/upd-fn/invoke \
|
||||
-H "X-Auth-Token: upd-user@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
-H "X-Test-Sub: upd-user@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
BODY=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null)
|
||||
[ "$BODY" = "after" ] && pass "После UPDATE — invoke возвращает новый код" || fail "После UPDATE — invoke вернул: $BODY"
|
||||
|
||||
@@ -157,18 +165,18 @@ echo ""
|
||||
echo ">>> БЛОК E: GET функции"
|
||||
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: get-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: get-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"get-fn","language":"nodejs","code":"x","ttl":"5m"}' > /dev/null
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/get-fn \
|
||||
-H "X-Auth-Token: get-user@test.local")
|
||||
-H "X-Test-Sub: get-user@test.local")
|
||||
CODE=$(echo "$R" | tail -1); BODY=$(echo "$R" | head -1)
|
||||
check_http "$CODE" "200" "GET существующей функции → 200"
|
||||
echo "$BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('name')=='get-fn' else 1)" 2>/dev/null \
|
||||
&& pass "GET — имя функции в ответе" || fail "GET — имя не найдено: $BODY"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/nonexist-get-xyz \
|
||||
-H "X-Auth-Token: get-user@test.local")
|
||||
-H "X-Test-Sub: get-user@test.local")
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "GET несуществующей → 404"
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -178,16 +186,16 @@ echo ">>> БЛОК F: Namespace изоляция LIST"
|
||||
# u1 создаёт 3 функции, u2 — 1. LIST u1 должен вернуть только свои
|
||||
for i in 1 2 3; do
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ns-u1@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ns-u1@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ns-fn-$i\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"5m\"}" > /dev/null
|
||||
done
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ns-u2@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ns-u2@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"ns-fn-u2","language":"nodejs","code":"x","ttl":"5m"}' > /dev/null
|
||||
|
||||
COUNT_U1=$(curl -s ${BASE}/functions -H "X-Auth-Token: ns-u1@test.local" \
|
||||
COUNT_U1=$(curl -s ${BASE}/functions -H "X-Test-Sub: ns-u1@test.local" \
|
||||
| python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
COUNT_U2=$(curl -s ${BASE}/functions -H "X-Auth-Token: ns-u2@test.local" \
|
||||
COUNT_U2=$(curl -s ${BASE}/functions -H "X-Test-Sub: ns-u2@test.local" \
|
||||
| python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
|
||||
[ "$COUNT_U1" -ge 3 ] 2>/dev/null && pass "LIST изоляция: u1 видит $COUNT_U1 функций (>= 3)" || fail "LIST u1 — $COUNT_U1"
|
||||
@@ -199,21 +207,21 @@ echo ">>> БЛОК G: Namespace изоляция DELETE"
|
||||
|
||||
# Carol пытается удалить функцию Alice
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: alice-del@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: alice-del@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"alice-secret","language":"nodejs","code":"x","ttl":"5m"}' > /dev/null
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/alice-secret \
|
||||
-H "X-Auth-Token: carol-del@test.local")
|
||||
-H "X-Test-Sub: carol-del@test.local")
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "Carol DELETE alice функции → 404"
|
||||
|
||||
# Alice сама удаляет — должно быть 200
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/alice-secret \
|
||||
-H "X-Auth-Token: alice-del@test.local")
|
||||
-H "X-Test-Sub: alice-del@test.local")
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "Alice DELETE своей функции → 200"
|
||||
|
||||
# Alice повторно удаляет — 404
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/alice-secret \
|
||||
-H "X-Auth-Token: alice-del@test.local")
|
||||
-H "X-Test-Sub: alice-del@test.local")
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "Alice повторный DELETE → 404"
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -222,13 +230,13 @@ echo ">>> БЛОК H: Python — разные форматы кода"
|
||||
|
||||
# Формат 1: def main() без аргумента — Fission Python env вызывает main() без аргументов
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: py-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: py-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"py-main-'${RUN_ID}'","language":"python","code":"def main():\n return \"py-main\", 200","ttl":"10m"}' > /dev/null
|
||||
|
||||
sleep 15
|
||||
|
||||
INV=$(curl -s -X POST ${BASE}/functions/py-main-${RUN_ID}/invoke \
|
||||
-H "X-Auth-Token: py-user@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
-H "X-Test-Sub: py-user@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null)
|
||||
[ "$STATUS" = "200" ] && pass "Python def main() (no-arg) → invoke 200" || fail "Python def main → status=$STATUS body=$(echo $INV)"
|
||||
|
||||
@@ -238,24 +246,24 @@ echo ">>> БЛОК I: NodeJS — несколько export-паттернов"
|
||||
|
||||
# module.exports = async function
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: js-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"js-anon","language":"nodejs","code":"module.exports=async function(ctx){return {body:\"anon\"};}","ttl":"10m"}' > /dev/null
|
||||
|
||||
# exports.handler
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: js-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"js-handler","language":"nodejs","code":"exports.handler=async(ctx)=>({body:\"handler\"});","ttl":"10m"}' > /dev/null
|
||||
|
||||
# exports.default
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: js-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"js-default","language":"nodejs","code":"exports.default=async(ctx)=>({body:\"default\"});","ttl":"10m"}' > /dev/null
|
||||
|
||||
sleep 10
|
||||
|
||||
for fn_name in js-anon js-handler js-default; do
|
||||
INV=$(curl -s -X POST ${BASE}/functions/${fn_name}/invoke \
|
||||
-H "X-Auth-Token: js-user@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
-H "X-Test-Sub: js-user@test.local" -H "Content-Type: application/json" -d '{}')
|
||||
STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null)
|
||||
[ "$STATUS" = "200" ] && pass "NodeJS $fn_name → 200" || fail "NodeJS $fn_name → status=$STATUS / $(echo $INV)"
|
||||
done
|
||||
@@ -265,20 +273,20 @@ echo ""
|
||||
echo ">>> БЛОК J: TTL — создание и проверка"
|
||||
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ttl-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ttl-user@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ttl-has-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"15m\"}" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('expires_at') else 1)" 2>/dev/null \
|
||||
&& pass "TTL=15m — expires_at присутствует" || fail "TTL=15m — expires_at отсутствует"
|
||||
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ttl-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ttl-user@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ttl-noexp-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"x\"}" \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if not d.get('expires_at') else 1)" 2>/dev/null \
|
||||
&& pass "Без TTL — expires_at отсутствует" || fail "Без TTL — expires_at присутствует (не ожидалось)"
|
||||
|
||||
# Невалидный TTL
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ttl-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ttl-user@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ttl-bad-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"x\",\"ttl\":\"99z\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
[ "$CODE" = "400" ] && pass "Невалидный TTL=99z → 400" || fail "Невалидный TTL=99z → got $CODE"
|
||||
@@ -289,13 +297,13 @@ echo ">>> БЛОК K: Invoke — тело запроса передаётся в
|
||||
|
||||
# ctx.request.body может быть undefined — тестируем через ctx.request.url (всегда доступен)
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: body-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: body-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"echo-url","language":"nodejs","code":"module.exports=async(ctx)=>({body:\"got:\"+ctx.request.url});","ttl":"10m"}' > /dev/null
|
||||
|
||||
sleep 12
|
||||
|
||||
INV=$(curl -s -X POST ${BASE}/functions/echo-url/invoke \
|
||||
-H "X-Auth-Token: body-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: body-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"hello":"world"}')
|
||||
RESP=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null)
|
||||
STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null)
|
||||
@@ -307,14 +315,14 @@ echo ""
|
||||
echo ">>> БЛОК L: Stress — 10 параллельных invoke"
|
||||
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: stress-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: stress-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"stress-fn","language":"nodejs","code":"module.exports=async()=>({body:\"ok\"});","ttl":"10m"}' > /dev/null
|
||||
sleep 8
|
||||
|
||||
STRESS_OK=0
|
||||
for i in $(seq 1 10); do
|
||||
ST=$(curl -s -X POST ${BASE}/functions/stress-fn/invoke \
|
||||
-H "X-Auth-Token: stress-user@test.local" -H "Content-Type: application/json" -d '{}' \
|
||||
-H "X-Test-Sub: stress-user@test.local" -H "Content-Type: application/json" -d '{}' \
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null)
|
||||
[ "$ST" = "200" ] && STRESS_OK=$((STRESS_OK+1))
|
||||
done &
|
||||
@@ -326,7 +334,7 @@ PIDS=()
|
||||
TMPDIR_S=$(mktemp -d)
|
||||
for i in $(seq 1 10); do
|
||||
( curl -s -X POST ${BASE}/functions/stress-fn/invoke \
|
||||
-H "X-Auth-Token: stress-user@test.local" -H "Content-Type: application/json" -d '{}' \
|
||||
-H "X-Test-Sub: stress-user@test.local" -H "Content-Type: application/json" -d '{}' \
|
||||
| python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('status')==200 else 1)" 2>/dev/null \
|
||||
&& touch "${TMPDIR_S}/ok_$i" || true ) &
|
||||
done
|
||||
@@ -339,7 +347,7 @@ rm -rf "${TMPDIR_S}"
|
||||
echo ""
|
||||
echo ">>> БЛОК M: LIST пустого namespace"
|
||||
|
||||
COUNT=$(curl -s ${BASE}/functions -H "X-Auth-Token: brand-new-empty-user-xyzqwerty@test.local" \
|
||||
COUNT=$(curl -s ${BASE}/functions -H "X-Test-Sub: brand-new-empty-user-xyzqwerty@test.local" \
|
||||
| python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
[ "$COUNT" = "0" ] && pass "LIST новый пользователь → пустой список" || fail "LIST новый пользователь → $COUNT"
|
||||
|
||||
@@ -348,15 +356,15 @@ echo ""
|
||||
echo ">>> БЛОК N: DELETE полная цепочка (fn + pkg + trigger)"
|
||||
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: chain-user@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: chain-user@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"chain-fn","language":"nodejs","code":"module.exports=async()=>({body:\"ok\"});","ttl":"5m"}' > /dev/null
|
||||
|
||||
# Получаем NS для chain-user из LIST (до DELETE)
|
||||
NS_CHAIN=$(curl -s ${BASE}/functions -H "X-Auth-Token: chain-user@test.local" \
|
||||
NS_CHAIN=$(curl -s ${BASE}/functions -H "X-Test-Sub: chain-user@test.local" \
|
||||
| python3 -c "import sys,json; items=json.load(sys.stdin); print(items[0]['metadata']['namespace'] if items else '')" 2>/dev/null)
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X DELETE ${BASE}/functions/chain-fn \
|
||||
-H "X-Auth-Token: chain-user@test.local")
|
||||
-H "X-Test-Sub: chain-user@test.local")
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "DELETE chain-fn → 200"
|
||||
|
||||
# Ждём propagation удаления в k8s
|
||||
@@ -368,7 +376,7 @@ if echo "$FN_RAW" | grep -q "not found"; then
|
||||
elif echo "$FN_RAW" | grep -q "credentials\|logged in\|Unauthorized"; then
|
||||
# kubectl недоступен — проверяем через API (GET должен давать 404)
|
||||
FN_API=$(curl -s -o /dev/null -w "%{http_code}" ${BASE}/functions/chain-fn \
|
||||
-H "X-Auth-Token: chain-user@test.local")
|
||||
-H "X-Test-Sub: chain-user@test.local")
|
||||
[ "$FN_API" = "404" ] && pass "k8s: Function удалена (подтверждено API, kubectl без прав)" || fail "k8s: Function не удалена, API=$FN_API"
|
||||
else
|
||||
fail "k8s: Function ещё существует в $NS_CHAIN"
|
||||
@@ -377,7 +385,7 @@ TRG_EXISTS=$(kubectl get httptriggers.fission.io -n ${NS_CHAIN} 2>&1 | grep -c "
|
||||
|
||||
# После удаления GET должен вернуть 404
|
||||
R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/chain-fn \
|
||||
-H "X-Auth-Token: chain-user@test.local")
|
||||
-H "X-Test-Sub: chain-user@test.local")
|
||||
CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "GET после DELETE → 404"
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -386,7 +394,7 @@ echo ">>> БЛОК O: Auth — без заголовка"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" ${BASE}/functions)
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
( [ "$CODE" = "401" ] || [ "$CODE" = "403" ] ) && pass "Без X-Auth-Token → 401/403 (got $CODE)" || fail "Без X-Auth-Token → got $CODE"
|
||||
( [ "$CODE" = "401" ] || [ "$CODE" = "403" ] ) && pass "Без X-Test-Sub → 401/403 (got $CODE)" || fail "Без X-Test-Sub → got $CODE"
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
echo ""
|
||||
@@ -411,39 +419,39 @@ echo ">>> БЛОК Q: Дубликат → 409, невалидное имя →
|
||||
|
||||
# Создаём функцию, потом снова с тем же именем → 409
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: dup2-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: dup2-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"dup-q-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'x'})\"}" > /dev/null
|
||||
sleep 3
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: dup2-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: dup2-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"dup-q-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'x'})\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "409" "Дубликат → 409 Conflict"
|
||||
|
||||
# Имя с заглавной буквой → 400
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"BadName\",\"language\":\"nodejs\",\"code\":\"x\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "400" "Имя с заглавной буквой → 400"
|
||||
|
||||
# Имя начинается с дефиса → 400
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"-badstart\",\"language\":\"nodejs\",\"code\":\"x\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "400" "Имя с ведущим дефисом → 400"
|
||||
|
||||
# Имя заканчивается дефисом → 400
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"badend-\",\"language\":\"nodejs\",\"code\":\"x\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "400" "Имя с завершающим дефисом → 400"
|
||||
|
||||
# Имя с пробелами → 400 (было 502)
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"bad name\",\"language\":\"nodejs\",\"code\":\"x\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "400" "Имя с пробелом → 400 (не 502)"
|
||||
@@ -451,7 +459,7 @@ check_http "$CODE" "400" "Имя с пробелом → 400 (не 502)"
|
||||
# Имя 64 символа → 400
|
||||
LONGNAME=$(python3 -c "print('a'*64)")
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${LONGNAME}\",\"language\":\"nodejs\",\"code\":\"x\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "400" "Имя 64 символа → 400"
|
||||
@@ -459,7 +467,7 @@ check_http "$CODE" "400" "Имя 64 символа → 400"
|
||||
# Имя 58 символов → 400 (route = 64 > 63, Fission webhook отклонит)
|
||||
NAME58=$(python3 -c "print('a'*58)")
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: name-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${NAME58}\",\"language\":\"nodejs\",\"code\":\"x\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "400" "Имя 58 символов → 400 (route будет 64 символа)"
|
||||
@@ -468,11 +476,11 @@ check_http "$CODE" "400" "Имя 58 символов → 400 (route будет 6
|
||||
NAME57=$(python3 -c "print('a'*57)")
|
||||
# Первый прогрев namespace
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: len57-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: len57-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"warmup","language":"nodejs","code":"module.exports=async()=>({body:\"w\"})"}' > /dev/null
|
||||
sleep 8
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: len57-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: len57-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${NAME57}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'ok'})\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "201" "Имя 57 символов → принято (201)"
|
||||
@@ -490,7 +498,7 @@ body = json.dumps({'name':'big-fn','language':'nodejs','code':code})
|
||||
sys.stdout.write(body)
|
||||
" > "$BIG_TMP"
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: big-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: big-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
--data @"$BIG_TMP")
|
||||
rm -f "$BIG_TMP"
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
@@ -502,7 +510,7 @@ CODE=$(echo "$R" | tail -1)
|
||||
# Код ровно 1000 символов → не 400
|
||||
CODE1K=$(python3 -c "print('module.exports=async()=>({body:' + '\"x\"' + '});' + ' '*(1000-38))")
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: size-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: size-user-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"size-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'ok'})\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
[ "$CODE" = "201" ] && pass "Код 1000 байт → принят (201)" || fail "Код 1000 байт → got $CODE"
|
||||
@@ -513,14 +521,14 @@ echo ">>> БЛОК S: TTL expire + reaper очистка"
|
||||
|
||||
# Создаём функцию с TTL=1m
|
||||
R=$(curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ttl-exp-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ttl-exp-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"expire-fn-${RUN_ID}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:'bye'})\",\"ttl\":\"1m\"}")
|
||||
CREATED=$(echo "$R" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('name',''))" 2>/dev/null)
|
||||
[ "$CREATED" = "expire-fn-${RUN_ID}" ] && pass "TTL=1m функция создана" || fail "TTL=1m создание: $R"
|
||||
|
||||
# Сразу должна быть видна в LIST
|
||||
COUNT=$(curl -s ${BASE}/functions \
|
||||
-H "X-Auth-Token: ttl-exp-${RUN_ID}@test.local" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
-H "X-Test-Sub: ttl-exp-${RUN_ID}@test.local" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
[ "$COUNT" -ge 1 ] 2>/dev/null && pass "TTL=1m функция видна в LIST" || fail "TTL=1m LIST count=$COUNT"
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -533,14 +541,14 @@ RACE_RESULTS=""
|
||||
for i in $(seq 1 5); do
|
||||
RACE_CODE="module.exports=async()=>({body:'r${i}'})"
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${NS_USER}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${NS_USER}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"race-fn-${i}\",\"language\":\"nodejs\",\"code\":\"${RACE_CODE}\"}" &
|
||||
done
|
||||
wait
|
||||
# Проверяем что namespace создался и функции (хоть часть) прошли
|
||||
sleep 5
|
||||
COUNT=$(curl -s ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${NS_USER}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
-H "X-Test-Sub: ${NS_USER}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
[ "$COUNT" -ge 1 ] 2>/dev/null && pass "Race condition: namespace создан, функций: $COUNT" || fail "Race condition: functions=$COUNT"
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -552,7 +560,7 @@ for uid in $(seq 1 10); do
|
||||
U="u${uid}-${RUN_ID}@test.local"
|
||||
CODE="module.exports=async()=>({body:'user${uid}'})"
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${U}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${U}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"fn-${uid}\",\"language\":\"nodejs\",\"code\":\"${CODE}\"}" > /dev/null
|
||||
) &
|
||||
done
|
||||
@@ -562,7 +570,7 @@ sleep 3
|
||||
OK_USERS=0
|
||||
for uid in $(seq 1 10); do
|
||||
U="u${uid}-${RUN_ID}@test.local"
|
||||
C=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
C=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
[ "$C" -ge 1 ] 2>/dev/null && OK_USERS=$((OK_USERS+1))
|
||||
done
|
||||
[ "$OK_USERS" -ge 8 ] && pass "10 юзеров параллельно: ${OK_USERS}/10 создали функции" || fail "10 юзеров параллельно: только ${OK_USERS}/10"
|
||||
@@ -574,7 +582,7 @@ echo ">>> БЛОК V: Изоляция — 10 юзеров не видят чу
|
||||
ISOLATION_OK=0
|
||||
for uid in $(seq 1 10); do
|
||||
U="u${uid}-${RUN_ID}@test.local"
|
||||
LIST=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}")
|
||||
LIST=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}")
|
||||
# Проверяем что НЕТ чужих функций (fn-X от других пользователей не попадают)
|
||||
# Каждый видит ровно 1 свою (fn-{uid}) и ни одной fn-{other}
|
||||
MY_COUNT=$(echo "$LIST" | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='fn-${uid}'))" 2>/dev/null)
|
||||
@@ -592,7 +600,7 @@ echo ">>> БЛОК W: 5 юзеров одновременно вызывают
|
||||
for uid in $(seq 1 5); do
|
||||
(
|
||||
U="u${uid}-${RUN_ID}@test.local"
|
||||
NS12=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" \
|
||||
NS12=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" \
|
||||
| python3 -c "import sys,json; fns=json.load(sys.stdin); print(fns[0]['route'].split('/')[1] if fns else '')" 2>/dev/null)
|
||||
if [ -n "$NS12" ]; then
|
||||
INVOKE_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
@@ -620,20 +628,20 @@ for uid in $(seq 1 3); do
|
||||
|
||||
# CREATE
|
||||
CR=$(curl -s -o /dev/null -w "%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${U}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${U}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${FN}\",\"language\":\"nodejs\",\"code\":\"${CODE}\"}")
|
||||
|
||||
# LIST — функция есть (sleep чтобы k8s успел зарегистрировать)
|
||||
sleep 1
|
||||
LC=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" \
|
||||
LC=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" \
|
||||
| python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='${FN}'))" 2>/dev/null)
|
||||
|
||||
# DELETE
|
||||
DC=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
"${BASE}/functions/${FN}" -H "X-Auth-Token: ${U}")
|
||||
"${BASE}/functions/${FN}" -H "X-Test-Sub: ${U}")
|
||||
|
||||
# LIST после DELETE — функции нет
|
||||
LC2=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${U}" \
|
||||
LC2=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${U}" \
|
||||
| python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='${FN}'))" 2>/dev/null)
|
||||
|
||||
if [ "$CR" = "201" ] && [ "${LC:-0}" -ge 1 ] && [ "$DC" = "200" ] && [ "${LC2:-1}" -eq 0 ]; then
|
||||
@@ -651,26 +659,26 @@ echo ">>> БЛОК Y: Один юзер создаёт 10 функций, про
|
||||
MFUNC_USER="mfunc-${RUN_ID}@test.local"
|
||||
for i in $(seq 1 10); do
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${MFUNC_USER}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${MFUNC_USER}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"mf-${i}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>({body:${i}})\"}" > /dev/null &
|
||||
done
|
||||
wait
|
||||
sleep 3
|
||||
|
||||
MFUNC_COUNT=$(curl -s ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${MFUNC_USER}" \
|
||||
-H "X-Test-Sub: ${MFUNC_USER}" \
|
||||
| python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
[ "${MFUNC_COUNT:-0}" -ge 8 ] && pass "1 юзер создал 10 функций: видно ${MFUNC_COUNT}" || fail "1 юзер: создано только ${MFUNC_COUNT:-0}"
|
||||
|
||||
# Удаляем все
|
||||
for i in $(seq 1 10); do
|
||||
curl -s -X DELETE "${BASE}/functions/mf-${i}" \
|
||||
-H "X-Auth-Token: ${MFUNC_USER}" > /dev/null &
|
||||
-H "X-Test-Sub: ${MFUNC_USER}" > /dev/null &
|
||||
done
|
||||
wait
|
||||
sleep 3
|
||||
MFUNC_AFTER=$(curl -s ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${MFUNC_USER}" \
|
||||
-H "X-Test-Sub: ${MFUNC_USER}" \
|
||||
| python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
[ "${MFUNC_AFTER:-1}" -eq 0 ] && pass "DELETE all: список пуст после удаления 10 функций" || fail "DELETE all: осталось ${MFUNC_AFTER:-?} функций"
|
||||
|
||||
@@ -683,29 +691,29 @@ ATTACKER_USER="attacker-${RUN_ID}@test.local"
|
||||
|
||||
# Жертва создаёт функцию
|
||||
curl -s -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${VICTIM_USER}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${VICTIM_USER}" -H "Content-Type: application/json" \
|
||||
-d '{"name":"secret-fn","language":"nodejs","code":"module.exports=async()=>({body:42})"}' > /dev/null
|
||||
sleep 2
|
||||
|
||||
# Атакующий пытается GET чужой функции
|
||||
Z_GET=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
"${BASE}/functions/secret-fn" -H "X-Auth-Token: ${ATTACKER_USER}")
|
||||
"${BASE}/functions/secret-fn" -H "X-Test-Sub: ${ATTACKER_USER}")
|
||||
[ "$Z_GET" = "404" ] && pass "Cross-user GET чужой функции → 404" || fail "Cross-user GET → ожидали 404, got $Z_GET"
|
||||
|
||||
# Атакующий пытается UPDATE чужой функции (PUT /functions/{name}/code)
|
||||
Z_PUT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
|
||||
"${BASE}/functions/secret-fn/code" \
|
||||
-H "X-Auth-Token: ${ATTACKER_USER}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${ATTACKER_USER}" -H "Content-Type: application/json" \
|
||||
-d '{"code":"module.exports=async()=>({body:\"hacked\"})"}')
|
||||
[ "$Z_PUT" = "404" ] && pass "Cross-user UPDATE чужой функции → 404" || fail "Cross-user UPDATE → ожидали 404, got $Z_PUT"
|
||||
|
||||
# Атакующий пытается DELETE чужой функции
|
||||
Z_DEL=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
"${BASE}/functions/secret-fn" -H "X-Auth-Token: ${ATTACKER_USER}")
|
||||
"${BASE}/functions/secret-fn" -H "X-Test-Sub: ${ATTACKER_USER}")
|
||||
[ "$Z_DEL" = "404" ] && pass "Cross-user DELETE чужой функции → 404" || fail "Cross-user DELETE → ожидали 404, got $Z_DEL"
|
||||
|
||||
# Жертва проверяет что функция цела
|
||||
Z_VICTIM_LIST=$(curl -s ${BASE}/functions -H "X-Auth-Token: ${VICTIM_USER}" \
|
||||
Z_VICTIM_LIST=$(curl -s ${BASE}/functions -H "X-Test-Sub: ${VICTIM_USER}" \
|
||||
| python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name']=='secret-fn'))" 2>/dev/null)
|
||||
[ "${Z_VICTIM_LIST:-0}" -eq 1 ] && pass "Cross-user: функция жертвы цела после атаки" || fail "Cross-user: функция жертвы исчезла!"
|
||||
|
||||
@@ -720,7 +728,7 @@ LANG_NS="lang-multi-${RUN_ID}@test.local"
|
||||
|
||||
PHP_CODE=$(python3 -c "import json; print(json.dumps('<?php\nfunction handler(\$context) {\n \$context[\"response\"]->getBody()->write(\"php-ok\");\n}'))")
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${LANG_NS}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${LANG_NS}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"php-fn\",\"language\":\"php\",\"code\":${PHP_CODE},\"ttl\":\"10m\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "201" "PHP: create → 201"
|
||||
@@ -731,7 +739,7 @@ STATUS=""; RAW=""
|
||||
for _try in $(seq 1 20); do
|
||||
sleep 10
|
||||
INV=$(curl -s -X POST ${BASE}/functions/php-fn/invoke \
|
||||
-H "X-Auth-Token: ${LANG_NS}")
|
||||
-H "X-Test-Sub: ${LANG_NS}")
|
||||
STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null)
|
||||
RAW=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null)
|
||||
[ "$STATUS" = "200" ] && break
|
||||
@@ -745,7 +753,7 @@ echo ">>> БЛОК AB: Ruby — создание и вызов"
|
||||
|
||||
RUBY_CODE=$(python3 -c "import json; print(json.dumps('def handler\n \"ruby-ok\"\nend'))")
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: ${LANG_NS}" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: ${LANG_NS}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"ruby-fn\",\"language\":\"ruby\",\"code\":${RUBY_CODE},\"ttl\":\"10m\"}")
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "201" "Ruby: create → 201"
|
||||
@@ -756,7 +764,7 @@ STATUS=""; RAW=""
|
||||
for _try in $(seq 1 20); do
|
||||
sleep 10
|
||||
INV=$(curl -s -X POST ${BASE}/functions/ruby-fn/invoke \
|
||||
-H "X-Auth-Token: ${LANG_NS}")
|
||||
-H "X-Test-Sub: ${LANG_NS}")
|
||||
STATUS=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','?'))" 2>/dev/null)
|
||||
RAW=$(echo "$INV" | python3 -c "import sys,json; print(json.load(sys.stdin).get('response_raw','?'))" 2>/dev/null)
|
||||
[ "$STATUS" = "200" ] && break
|
||||
@@ -769,7 +777,7 @@ echo ""
|
||||
echo ">>> БЛОК AD: Невалидный язык → 400"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
|
||||
-H "X-Auth-Token: lang-inv-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-H "X-Test-Sub: lang-inv-${RUN_ID}@test.local" -H "Content-Type: application/json" \
|
||||
-d '{"name":"bad-lang-fn","language":"cobol","code":"x"}')
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
check_http "$CODE" "400" "Невалидный язык cobol → 400"
|
||||
|
||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
module.exports = async function(context) {
|
||||
return { status: 200, body: "Hello from Node.js!" };
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "node-hello",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js"
|
||||
}
|
||||
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "php-hello",
|
||||
"require": {}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<?php
|
||||
function main($event, $context) {
|
||||
return ["statusCode" => 200, "body" => "Hello from PHP!"];
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
bash: scripts/run_all.sh: No such file or directory
|
||||
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
def main(event, context):
|
||||
return {"statusCode": 200, "body": "Hello from Python!"}
|
||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
def main(event, context)
|
||||
{ statusCode: 200, body: "Hello from Ruby!" }
|
||||
end
|
||||
@@ -1,389 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_15-32-11.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=6f6d96d0
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-9402f4a2fc84dfab
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=ee11e143
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/crud-fn-ee11e143 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=f0ebb7af
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T12:48:18Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T13:33:21Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T12:33:24Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=5e3d3430
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET alice-fn → 404
|
||||
PASS: Bob GET alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE alice-fn → 404
|
||||
PASS: Bob DELETE alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT alice-fn/code → 404
|
||||
PASS: Bob PUT alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE alice-fn → 404
|
||||
PASS: Bob INVOKE alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
PASS: Alice LIST содержит alice-fn
|
||||
PASS: Alice LIST не содержит bob-fn
|
||||
PASS: Bob LIST содержит bob-fn
|
||||
PASS: Bob LIST не содержит alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /f99555a1d109/alice-fn ≠ /336aedce32fe/bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=11 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_isolation.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=1a3190a9
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=upd-fn-1a3190a9 status=200 raw=version-one err=
|
||||
PASS: Invoke v1 → version-one
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=upd-fn-1a3190a9-pkg-mopr5iev
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=upd-fn-1a3190a9 status=200 raw=version-two err=
|
||||
PASS: Invoke после update → version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=upd-fn-1a3190a9 status=200 raw=version-three err=
|
||||
PASS: Invoke после v3 update → version-three
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_update.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_invoke.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=da5a7732
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=node-fn-da5a7732 status=200 raw=node-invoke-ok err=
|
||||
PASS: NodeJS invoke → 200, тело содержит node-invoke-ok
|
||||
PASS: NodeJS второй invoke → 200 (warm)
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=py-fn-da5a7732 status=200 raw=py-invoke-ok err=
|
||||
PASS: Python invoke → 200, тело содержит py-invoke-ok
|
||||
|
||||
>>> Invoke response fields
|
||||
PASS: invoke.status = 200
|
||||
PASS: invoke.latency_ms = 8 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_invoke.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_linters.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
=== NODEJS ===
|
||||
[PASS] N01 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N02 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N03 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) { const x = @@@; };
|
||||
|
||||
[PASS] N04 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) {
|
||||
|
||||
|
||||
SyntaxError: Unexpected
|
||||
[PASS] N05 | nodejs | exp=fail got=False | <code>:1
|
||||
const x = {a: 1,, b: 2};
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token ','
|
||||
at wrapSa
|
||||
[PASS] N06 | nodejs | exp=fail got=False | <code>:1
|
||||
function foo( { return 1; }
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected number
|
||||
at
|
||||
|
||||
=== PYTHON ===
|
||||
[PASS] P01 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P02 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P03 | python | exp=fail got=False | Sorry: IndentationError: expected an indented block after function definition on line 1 (fission-lin
|
||||
[PASS] P04 | python | exp=fail got=False | File "<code>", line 2
|
||||
x = @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] P05 | python | exp=fail got=False | File "<code>", line 1
|
||||
def main(data)
|
||||
^
|
||||
SyntaxError: expected ':'
|
||||
[PASS] P06 | python | exp=fail got=False | File "<code>", line 2
|
||||
def main(
|
||||
^
|
||||
SyntaxError: '(' was never closed
|
||||
|
||||
=== RUBY ===
|
||||
[PASS] R01 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R02 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R03 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected {, expecting ')' (SyntaxError)
|
||||
{ status: 200 }
|
||||
^
|
||||
[PASS] R04 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected local variable or method, expecting '}' (SyntaxError)
|
||||
{ s
|
||||
[PASS] R05 | ruby | exp=fail got=False | ruby: <code>:3: syntax error, unexpected `end', expecting ')' (SyntaxError)
|
||||
|
||||
=== PHP ===
|
||||
[PASS] H01 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H02 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H03 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "return" in <code> on line 4
|
||||
Errors parsing <code>
|
||||
[PASS] H04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token ";" in <code> on line 2
|
||||
Errors parsing <code>
|
||||
[PASS] H05 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "{", expecting variable in <code> on line 2
|
||||
Errors
|
||||
|
||||
=== GO ===
|
||||
[PASS] G01 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G02 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G03 | go | exp=fail got=False | code.go:2:15: expected ')', found '{' (and 3 more errors)
|
||||
[PASS] G04 | go | exp=fail got=False | code.go:4:1: missing import path (and 1 more errors)
|
||||
[PASS] G05 | go | exp=fail got=False | code.go:3:10: expected operand, found 'ILLEGAL' (and 6 more errors)
|
||||
|
||||
=== ГРАНИЧНЫЕ ===
|
||||
[PASS] E01 | nodejs | exp=fail got=False | Код пустой.
|
||||
[PASS] E02 | python | exp=fail got=False | File "<code>", line 1
|
||||
asd @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] E03 | ruby | exp=fail got=False | ruby: <code>:1: syntax error, unexpected ';', expecting ')' (SyntaxError)
|
||||
def foo(;;; end
|
||||
^
|
||||
[PASS] E04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected end of file in <code> on line 1
|
||||
Errors parsing <code>
|
||||
|
||||
========================
|
||||
PASS=31 FAIL=0
|
||||
========================
|
||||
[0;32m ✓ test_linters.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_stress.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_stress.sh run=35f99cc0 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
PASS: Параллельный INVOKE (livetest): 8/8 OK
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=3 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_stress.sh — PASSED[0m
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ Скриптов: PASS=8 FAIL=0 338s ║
|
||||
╚══════════════════════════════════════╝
|
||||
@@ -1,384 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_15-47-34.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=ec11c644
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-b9441cd4ddb76388
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=470cbd90
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/crud-fn-470cbd90 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=053caa8e
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T13:03:42Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T13:48:45Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T12:48:48Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=2f150449
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET alice-fn → 404
|
||||
PASS: Bob GET alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE alice-fn → 404
|
||||
PASS: Bob DELETE alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT alice-fn/code → 404
|
||||
PASS: Bob PUT alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE alice-fn → 404
|
||||
PASS: Bob INVOKE alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
PASS: Alice LIST содержит alice-fn
|
||||
PASS: Alice LIST не содержит bob-fn
|
||||
PASS: Bob LIST содержит bob-fn
|
||||
PASS: Bob LIST не содержит alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /1fb06339c2b5/alice-fn ≠ /9073d18c6d33/bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=11 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_isolation.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=0fcd0348
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=upd-fn-0fcd0348 status=200 raw=version-one err=
|
||||
PASS: Invoke v1 → version-one
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=upd-fn-0fcd0348-pkg-moprpbdk
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=upd-fn-0fcd0348 status=200 raw=version-two err=
|
||||
PASS: Invoke после update → version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=upd-fn-0fcd0348 status=200 raw=version-three err=
|
||||
PASS: Invoke после v3 update → version-three
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_update.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_invoke.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=f70fc571
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=node-fn-f70fc571 status=200 raw=node-invoke-ok err=
|
||||
PASS: NodeJS invoke → 200, тело содержит node-invoke-ok
|
||||
PASS: NodeJS второй invoke → 200 (warm)
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=py-fn-f70fc571 status=200 raw=py-invoke-ok err=
|
||||
PASS: Python invoke → 200, тело содержит py-invoke-ok
|
||||
|
||||
>>> Invoke response fields
|
||||
PASS: invoke.status = 200
|
||||
PASS: invoke.latency_ms = 9 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_invoke.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_linters.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
=== NODEJS ===
|
||||
[PASS] N01 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N02 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N03 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) { const x = @@@; };
|
||||
|
||||
[PASS] N04 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) {
|
||||
|
||||
|
||||
SyntaxError: Unexpected
|
||||
[PASS] N05 | nodejs | exp=fail got=False | <code>:1
|
||||
const x = {a: 1,, b: 2};
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token ','
|
||||
at wrapSa
|
||||
[PASS] N06 | nodejs | exp=fail got=False | <code>:1
|
||||
function foo( { return 1; }
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected number
|
||||
at
|
||||
|
||||
=== PYTHON ===
|
||||
[PASS] P01 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P02 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P03 | python | exp=fail got=False | Sorry: IndentationError: expected an indented block after function definition on line 1 (fission-lin
|
||||
[PASS] P04 | python | exp=fail got=False | File "<code>", line 2
|
||||
x = @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] P05 | python | exp=fail got=False | File "<code>", line 1
|
||||
def main(data)
|
||||
^
|
||||
SyntaxError: expected ':'
|
||||
[PASS] P06 | python | exp=fail got=False | File "<code>", line 2
|
||||
def main(
|
||||
^
|
||||
SyntaxError: '(' was never closed
|
||||
|
||||
=== RUBY ===
|
||||
[PASS] R01 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R02 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R03 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected {, expecting ')' (SyntaxError)
|
||||
{ status: 200 }
|
||||
^
|
||||
[PASS] R04 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected local variable or method, expecting '}' (SyntaxError)
|
||||
{ s
|
||||
[PASS] R05 | ruby | exp=fail got=False | ruby: <code>:3: syntax error, unexpected `end', expecting ')' (SyntaxError)
|
||||
|
||||
=== PHP ===
|
||||
[PASS] H01 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H02 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H03 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "return" in <code> on line 4
|
||||
Errors parsing <code>
|
||||
[PASS] H04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token ";" in <code> on line 2
|
||||
Errors parsing <code>
|
||||
[PASS] H05 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "{", expecting variable in <code> on line 2
|
||||
Errors
|
||||
|
||||
=== GO ===
|
||||
[PASS] G01 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G02 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G03 | go | exp=fail got=False | code.go:2:15: expected ')', found '{' (and 3 more errors)
|
||||
[PASS] G04 | go | exp=fail got=False | code.go:4:1: missing import path (and 1 more errors)
|
||||
[PASS] G05 | go | exp=fail got=False | code.go:3:10: expected operand, found 'ILLEGAL' (and 6 more errors)
|
||||
|
||||
=== ГРАНИЧНЫЕ ===
|
||||
[PASS] E01 | nodejs | exp=fail got=False | Код пустой.
|
||||
[PASS] E02 | python | exp=fail got=False | File "<code>", line 1
|
||||
asd @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] E03 | ruby | exp=fail got=False | ruby: <code>:1: syntax error, unexpected ';', expecting ')' (SyntaxError)
|
||||
def foo(;;; end
|
||||
^
|
||||
[PASS] E04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected end of file in <code> on line 1
|
||||
Errors parsing <code>
|
||||
|
||||
========================
|
||||
PASS=31 FAIL=0
|
||||
========================
|
||||
[0;32m ✓ test_linters.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_stress.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_stress.sh run=dc4bc594 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
PASS: Параллельный INVOKE (livetest): 7/8 OK
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=3 FAIL=0
|
||||
========================================
|
||||
@@ -1,21 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_16-02-40_remaining.log
|
||||
|
||||
========================================
|
||||
test_stress.sh run=b6d326c7 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
PASS: Параллельный INVOKE (livetest): 8/8 OK
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=3 FAIL=0
|
||||
========================================
|
||||
|
||||
✓ test_stress.sh — PASSED
|
||||
@@ -1,379 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_15-47-34.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=ec11c644
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-b9441cd4ddb76388
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=470cbd90
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/crud-fn-470cbd90 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=053caa8e
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T13:03:42Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T13:48:45Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T12:48:48Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=2f150449
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET alice-fn → 404
|
||||
PASS: Bob GET alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE alice-fn → 404
|
||||
PASS: Bob DELETE alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT alice-fn/code → 404
|
||||
PASS: Bob PUT alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE alice-fn → 404
|
||||
PASS: Bob INVOKE alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
PASS: Alice LIST содержит alice-fn
|
||||
PASS: Alice LIST не содержит bob-fn
|
||||
PASS: Bob LIST содержит bob-fn
|
||||
PASS: Bob LIST не содержит alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /1fb06339c2b5/alice-fn ≠ /9073d18c6d33/bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=11 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_isolation.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=0fcd0348
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=upd-fn-0fcd0348 status=200 raw=version-one err=
|
||||
PASS: Invoke v1 → version-one
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=upd-fn-0fcd0348-pkg-moprpbdk
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=upd-fn-0fcd0348 status=200 raw=version-two err=
|
||||
PASS: Invoke после update → version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=upd-fn-0fcd0348 status=200 raw=version-three err=
|
||||
PASS: Invoke после v3 update → version-three
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_update.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_invoke.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=f70fc571
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=node-fn-f70fc571 status=200 raw=node-invoke-ok err=
|
||||
PASS: NodeJS invoke → 200, тело содержит node-invoke-ok
|
||||
PASS: NodeJS второй invoke → 200 (warm)
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=py-fn-f70fc571 status=200 raw=py-invoke-ok err=
|
||||
PASS: Python invoke → 200, тело содержит py-invoke-ok
|
||||
|
||||
>>> Invoke response fields
|
||||
PASS: invoke.status = 200
|
||||
PASS: invoke.latency_ms = 9 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_invoke.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_linters.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
=== NODEJS ===
|
||||
[PASS] N01 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N02 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N03 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) { const x = @@@; };
|
||||
|
||||
[PASS] N04 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) {
|
||||
|
||||
|
||||
SyntaxError: Unexpected
|
||||
[PASS] N05 | nodejs | exp=fail got=False | <code>:1
|
||||
const x = {a: 1,, b: 2};
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token ','
|
||||
at wrapSa
|
||||
[PASS] N06 | nodejs | exp=fail got=False | <code>:1
|
||||
function foo( { return 1; }
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected number
|
||||
at
|
||||
|
||||
=== PYTHON ===
|
||||
[PASS] P01 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P02 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P03 | python | exp=fail got=False | Sorry: IndentationError: expected an indented block after function definition on line 1 (fission-lin
|
||||
[PASS] P04 | python | exp=fail got=False | File "<code>", line 2
|
||||
x = @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] P05 | python | exp=fail got=False | File "<code>", line 1
|
||||
def main(data)
|
||||
^
|
||||
SyntaxError: expected ':'
|
||||
[PASS] P06 | python | exp=fail got=False | File "<code>", line 2
|
||||
def main(
|
||||
^
|
||||
SyntaxError: '(' was never closed
|
||||
|
||||
=== RUBY ===
|
||||
[PASS] R01 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R02 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R03 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected {, expecting ')' (SyntaxError)
|
||||
{ status: 200 }
|
||||
^
|
||||
[PASS] R04 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected local variable or method, expecting '}' (SyntaxError)
|
||||
{ s
|
||||
[PASS] R05 | ruby | exp=fail got=False | ruby: <code>:3: syntax error, unexpected `end', expecting ')' (SyntaxError)
|
||||
|
||||
=== PHP ===
|
||||
[PASS] H01 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H02 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H03 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "return" in <code> on line 4
|
||||
Errors parsing <code>
|
||||
[PASS] H04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token ";" in <code> on line 2
|
||||
Errors parsing <code>
|
||||
[PASS] H05 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "{", expecting variable in <code> on line 2
|
||||
Errors
|
||||
|
||||
=== GO ===
|
||||
[PASS] G01 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G02 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G03 | go | exp=fail got=False | code.go:2:15: expected ')', found '{' (and 3 more errors)
|
||||
[PASS] G04 | go | exp=fail got=False | code.go:4:1: missing import path (and 1 more errors)
|
||||
[PASS] G05 | go | exp=fail got=False | code.go:3:10: expected operand, found 'ILLEGAL' (and 6 more errors)
|
||||
|
||||
=== ГРАНИЧНЫЕ ===
|
||||
[PASS] E01 | nodejs | exp=fail got=False | Код пустой.
|
||||
[PASS] E02 | python | exp=fail got=False | File "<code>", line 1
|
||||
asd @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] E03 | ruby | exp=fail got=False | ruby: <code>:1: syntax error, unexpected ';', expecting ')' (SyntaxError)
|
||||
def foo(;;; end
|
||||
^
|
||||
[PASS] E04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected end of file in <code> on line 1
|
||||
Errors parsing <code>
|
||||
|
||||
========================
|
||||
PASS=31 FAIL=0
|
||||
========================
|
||||
[0;32m ✓ test_linters.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_stress.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_stress.sh run=dc4bc594 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
@@ -1,566 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_17-05-22.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: найдено 2 тест-namespace-ов, удаляем...
|
||||
namespace "fission-4f75ef1325a69fe9" deleted
|
||||
namespace "fission-c3511430abe5675b" deleted
|
||||
pre_cleanup.sh: очистка завершена.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=d7d6b77e
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-6dca95c9a39f6956
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=01b2c8d4
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-01b2c8d4 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=306f2666
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T14:21:07Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T15:06:09Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T14:06:09Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=e4e19caa
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /473720accdd7/t-alice-fn ≠ /dc8693d9dde4/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=e846253c
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
FAIL: Invoke v1 не стабилизировался
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=t-upd-fn-e846253c-pkg-mopuhngw
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после update не вернул version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после v3 update не стабилизировался
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=7 FAIL=3
|
||||
========================================
|
||||
[0;31m ✗ test_update.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_invoke.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=030b2715
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
FAIL: NodeJS invoke не стабилизировался за 60s
|
||||
FAIL: NodeJS второй invoke → status=404
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
FAIL: Python invoke не стабилизировался за 75s
|
||||
|
||||
>>> Invoke response fields
|
||||
FAIL: invoke.status = 404
|
||||
PASS: invoke.latency_ms = 1 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=5 FAIL=4
|
||||
========================================
|
||||
[0;31m ✗ test_invoke.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_linters.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
=== NODEJS ===
|
||||
[PASS] N01 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N02 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N03 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) { const x = @@@; };
|
||||
|
||||
[PASS] N04 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) {
|
||||
|
||||
|
||||
SyntaxError: Unexpected
|
||||
[PASS] N05 | nodejs | exp=fail got=False | <code>:1
|
||||
const x = {a: 1,, b: 2};
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token ','
|
||||
at wrapSa
|
||||
[PASS] N06 | nodejs | exp=fail got=False | <code>:1
|
||||
function foo( { return 1; }
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected number
|
||||
at
|
||||
|
||||
=== PYTHON ===
|
||||
[PASS] P01 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P02 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P03 | python | exp=fail got=False | Sorry: IndentationError: expected an indented block after function definition on line 1 (fission-lin
|
||||
[PASS] P04 | python | exp=fail got=False | File "<code>", line 2
|
||||
x = @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] P05 | python | exp=fail got=False | File "<code>", line 1
|
||||
def main(data)
|
||||
^
|
||||
SyntaxError: expected ':'
|
||||
[PASS] P06 | python | exp=fail got=False | File "<code>", line 2
|
||||
def main(
|
||||
^
|
||||
SyntaxError: '(' was never closed
|
||||
|
||||
=== RUBY ===
|
||||
[PASS] R01 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R02 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R03 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected {, expecting ')' (SyntaxError)
|
||||
{ status: 200 }
|
||||
^
|
||||
[PASS] R04 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected local variable or method, expecting '}' (SyntaxError)
|
||||
{ s
|
||||
[PASS] R05 | ruby | exp=fail got=False | ruby: <code>:3: syntax error, unexpected `end', expecting ')' (SyntaxError)
|
||||
|
||||
=== PHP ===
|
||||
[PASS] H01 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H02 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H03 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "return" in <code> on line 4
|
||||
Errors parsing <code>
|
||||
[PASS] H04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token ";" in <code> on line 2
|
||||
Errors parsing <code>
|
||||
[PASS] H05 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "{", expecting variable in <code> on line 2
|
||||
Errors
|
||||
|
||||
=== GO ===
|
||||
[PASS] G01 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G02 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G03 | go | exp=fail got=False | code.go:2:15: expected ')', found '{' (and 3 more errors)
|
||||
[PASS] G04 | go | exp=fail got=False | code.go:4:1: missing import path (and 1 more errors)
|
||||
[PASS] G05 | go | exp=fail got=False | code.go:3:10: expected operand, found 'ILLEGAL' (and 6 more errors)
|
||||
|
||||
=== ГРАНИЧНЫЕ ===
|
||||
[PASS] E01 | nodejs | exp=fail got=False | Код пустой.
|
||||
[PASS] E02 | python | exp=fail got=False | File "<code>", line 1
|
||||
asd @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] E03 | ruby | exp=fail got=False | ruby: <code>:1: syntax error, unexpected ';', expecting ')' (SyntaxError)
|
||||
def foo(;;; end
|
||||
^
|
||||
[PASS] E04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected end of file in <code> on line 1
|
||||
Errors parsing <code>
|
||||
|
||||
========================
|
||||
PASS=31 FAIL=0
|
||||
========================
|
||||
[0;32m ✓ test_linters.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_stress.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_stress.sh run=699cd177 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
прогрев функции 1/8: попытка 1/12...
|
||||
прогрев функции 1/8: попытка 2/12...
|
||||
прогрев функции 1/8: попытка 3/12...
|
||||
прогрев функции 1/8: попытка 4/12...
|
||||
прогрев функции 1/8: попытка 5/12...
|
||||
прогрев функции 1/8: попытка 6/12...
|
||||
прогрев функции 1/8: попытка 7/12...
|
||||
прогрев функции 1/8: попытка 8/12...
|
||||
прогрев функции 1/8: попытка 9/12...
|
||||
прогрев функции 1/8: попытка 10/12...
|
||||
прогрев функции 1/8: попытка 11/12...
|
||||
прогрев функции 1/8: попытка 12/12...
|
||||
функция 1/8 — готова
|
||||
прогрев функции 2/8: попытка 1/12...
|
||||
прогрев функции 2/8: попытка 2/12...
|
||||
прогрев функции 2/8: попытка 3/12...
|
||||
прогрев функции 2/8: попытка 4/12...
|
||||
прогрев функции 2/8: попытка 5/12...
|
||||
прогрев функции 2/8: попытка 6/12...
|
||||
прогрев функции 2/8: попытка 7/12...
|
||||
прогрев функции 2/8: попытка 8/12...
|
||||
прогрев функции 2/8: попытка 9/12...
|
||||
прогрев функции 2/8: попытка 10/12...
|
||||
прогрев функции 2/8: попытка 11/12...
|
||||
прогрев функции 2/8: попытка 12/12...
|
||||
функция 2/8 — готова
|
||||
прогрев функции 3/8: попытка 1/12...
|
||||
прогрев функции 3/8: попытка 2/12...
|
||||
прогрев функции 3/8: попытка 3/12...
|
||||
прогрев функции 3/8: попытка 4/12...
|
||||
прогрев функции 3/8: попытка 5/12...
|
||||
прогрев функции 3/8: попытка 6/12...
|
||||
прогрев функции 3/8: попытка 7/12...
|
||||
прогрев функции 3/8: попытка 8/12...
|
||||
прогрев функции 3/8: попытка 9/12...
|
||||
прогрев функции 3/8: попытка 10/12...
|
||||
прогрев функции 3/8: попытка 11/12...
|
||||
прогрев функции 3/8: попытка 12/12...
|
||||
функция 3/8 — готова
|
||||
прогрев функции 4/8: попытка 1/12...
|
||||
прогрев функции 4/8: попытка 2/12...
|
||||
прогрев функции 4/8: попытка 3/12...
|
||||
прогрев функции 4/8: попытка 4/12...
|
||||
прогрев функции 4/8: попытка 5/12...
|
||||
прогрев функции 4/8: попытка 6/12...
|
||||
прогрев функции 4/8: попытка 7/12...
|
||||
прогрев функции 4/8: попытка 8/12...
|
||||
прогрев функции 4/8: попытка 9/12...
|
||||
прогрев функции 4/8: попытка 10/12...
|
||||
прогрев функции 4/8: попытка 11/12...
|
||||
прогрев функции 4/8: попытка 12/12...
|
||||
функция 4/8 — готова
|
||||
прогрев функции 5/8: попытка 1/12...
|
||||
прогрев функции 5/8: попытка 2/12...
|
||||
прогрев функции 5/8: попытка 3/12...
|
||||
прогрев функции 5/8: попытка 4/12...
|
||||
прогрев функции 5/8: попытка 5/12...
|
||||
прогрев функции 5/8: попытка 6/12...
|
||||
прогрев функции 5/8: попытка 7/12...
|
||||
прогрев функции 5/8: попытка 8/12...
|
||||
прогрев функции 5/8: попытка 9/12...
|
||||
прогрев функции 5/8: попытка 10/12...
|
||||
прогрев функции 5/8: попытка 11/12...
|
||||
прогрев функции 5/8: попытка 12/12...
|
||||
функция 5/8 — готова
|
||||
прогрев функции 6/8: попытка 1/12...
|
||||
прогрев функции 6/8: попытка 2/12...
|
||||
прогрев функции 6/8: попытка 3/12...
|
||||
прогрев функции 6/8: попытка 4/12...
|
||||
прогрев функции 6/8: попытка 5/12...
|
||||
прогрев функции 6/8: попытка 6/12...
|
||||
прогрев функции 6/8: попытка 7/12...
|
||||
прогрев функции 6/8: попытка 8/12...
|
||||
прогрев функции 6/8: попытка 9/12...
|
||||
прогрев функции 6/8: попытка 10/12...
|
||||
прогрев функции 6/8: попытка 11/12...
|
||||
прогрев функции 6/8: попытка 12/12...
|
||||
функция 6/8 — готова
|
||||
прогрев функции 7/8: попытка 1/12...
|
||||
прогрев функции 7/8: попытка 2/12...
|
||||
прогрев функции 7/8: попытка 3/12...
|
||||
прогрев функции 7/8: попытка 4/12...
|
||||
прогрев функции 7/8: попытка 5/12...
|
||||
прогрев функции 7/8: попытка 6/12...
|
||||
прогрев функции 7/8: попытка 7/12...
|
||||
прогрев функции 7/8: попытка 8/12...
|
||||
прогрев функции 7/8: попытка 9/12...
|
||||
прогрев функции 7/8: попытка 10/12...
|
||||
прогрев функции 7/8: попытка 11/12...
|
||||
прогрев функции 7/8: попытка 12/12...
|
||||
функция 7/8 — готова
|
||||
прогрев функции 8/8: попытка 1/12...
|
||||
прогрев функции 8/8: попытка 2/12...
|
||||
прогрев функции 8/8: попытка 3/12...
|
||||
прогрев функции 8/8: попытка 4/12...
|
||||
прогрев функции 8/8: попытка 5/12...
|
||||
прогрев функции 8/8: попытка 6/12...
|
||||
прогрев функции 8/8: попытка 7/12...
|
||||
прогрев функции 8/8: попытка 8/12...
|
||||
прогрев функции 8/8: попытка 9/12...
|
||||
прогрев функции 8/8: попытка 10/12...
|
||||
прогрев функции 8/8: попытка 11/12...
|
||||
прогрев функции 8/8: попытка 12/12...
|
||||
функция 8/8 — готова
|
||||
FAIL: Параллельный INVOKE (livetest): 8/8 упали
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=2 FAIL=1
|
||||
========================================
|
||||
[0;31m ✗ test_stress.sh — FAILED (exit 1)[0m
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ Скриптов: PASS=4 FAIL=4 1020s ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
Упавшие скрипты:
|
||||
[0;31m✗ test_isolation.sh[0m
|
||||
[0;31m✗ test_update.sh[0m
|
||||
[0;31m✗ test_invoke.sh[0m
|
||||
[0;31m✗ test_stress.sh[0m
|
||||
@@ -1,566 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_17-05-22.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: найдено 2 тест-namespace-ов, удаляем...
|
||||
namespace "fission-4f75ef1325a69fe9" deleted
|
||||
namespace "fission-c3511430abe5675b" deleted
|
||||
pre_cleanup.sh: очистка завершена.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=d7d6b77e
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-6dca95c9a39f6956
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=01b2c8d4
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-01b2c8d4 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=306f2666
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T14:21:07Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T15:06:09Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T14:06:09Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=e4e19caa
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /473720accdd7/t-alice-fn ≠ /dc8693d9dde4/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=e846253c
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
FAIL: Invoke v1 не стабилизировался
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=t-upd-fn-e846253c-pkg-mopuhngw
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после update не вернул version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e846253c status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после v3 update не стабилизировался
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=7 FAIL=3
|
||||
========================================
|
||||
[0;31m ✗ test_update.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_invoke.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=030b2715
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-node-fn-030b2715 status=404 raw=404 page not found err=
|
||||
FAIL: NodeJS invoke не стабилизировался за 60s
|
||||
FAIL: NodeJS второй invoke → status=404
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-py-fn-030b2715 status=404 raw=404 page not found err=
|
||||
FAIL: Python invoke не стабилизировался за 75s
|
||||
|
||||
>>> Invoke response fields
|
||||
FAIL: invoke.status = 404
|
||||
PASS: invoke.latency_ms = 1 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=5 FAIL=4
|
||||
========================================
|
||||
[0;31m ✗ test_invoke.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_linters.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
=== NODEJS ===
|
||||
[PASS] N01 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N02 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N03 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) { const x = @@@; };
|
||||
|
||||
[PASS] N04 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) {
|
||||
|
||||
|
||||
SyntaxError: Unexpected
|
||||
[PASS] N05 | nodejs | exp=fail got=False | <code>:1
|
||||
const x = {a: 1,, b: 2};
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token ','
|
||||
at wrapSa
|
||||
[PASS] N06 | nodejs | exp=fail got=False | <code>:1
|
||||
function foo( { return 1; }
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected number
|
||||
at
|
||||
|
||||
=== PYTHON ===
|
||||
[PASS] P01 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P02 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P03 | python | exp=fail got=False | Sorry: IndentationError: expected an indented block after function definition on line 1 (fission-lin
|
||||
[PASS] P04 | python | exp=fail got=False | File "<code>", line 2
|
||||
x = @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] P05 | python | exp=fail got=False | File "<code>", line 1
|
||||
def main(data)
|
||||
^
|
||||
SyntaxError: expected ':'
|
||||
[PASS] P06 | python | exp=fail got=False | File "<code>", line 2
|
||||
def main(
|
||||
^
|
||||
SyntaxError: '(' was never closed
|
||||
|
||||
=== RUBY ===
|
||||
[PASS] R01 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R02 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R03 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected {, expecting ')' (SyntaxError)
|
||||
{ status: 200 }
|
||||
^
|
||||
[PASS] R04 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected local variable or method, expecting '}' (SyntaxError)
|
||||
{ s
|
||||
[PASS] R05 | ruby | exp=fail got=False | ruby: <code>:3: syntax error, unexpected `end', expecting ')' (SyntaxError)
|
||||
|
||||
=== PHP ===
|
||||
[PASS] H01 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H02 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H03 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "return" in <code> on line 4
|
||||
Errors parsing <code>
|
||||
[PASS] H04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token ";" in <code> on line 2
|
||||
Errors parsing <code>
|
||||
[PASS] H05 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "{", expecting variable in <code> on line 2
|
||||
Errors
|
||||
|
||||
=== GO ===
|
||||
[PASS] G01 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G02 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G03 | go | exp=fail got=False | code.go:2:15: expected ')', found '{' (and 3 more errors)
|
||||
[PASS] G04 | go | exp=fail got=False | code.go:4:1: missing import path (and 1 more errors)
|
||||
[PASS] G05 | go | exp=fail got=False | code.go:3:10: expected operand, found 'ILLEGAL' (and 6 more errors)
|
||||
|
||||
=== ГРАНИЧНЫЕ ===
|
||||
[PASS] E01 | nodejs | exp=fail got=False | Код пустой.
|
||||
[PASS] E02 | python | exp=fail got=False | File "<code>", line 1
|
||||
asd @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] E03 | ruby | exp=fail got=False | ruby: <code>:1: syntax error, unexpected ';', expecting ')' (SyntaxError)
|
||||
def foo(;;; end
|
||||
^
|
||||
[PASS] E04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected end of file in <code> on line 1
|
||||
Errors parsing <code>
|
||||
|
||||
========================
|
||||
PASS=31 FAIL=0
|
||||
========================
|
||||
[0;32m ✓ test_linters.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_stress.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_stress.sh run=699cd177 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
прогрев функции 1/8: попытка 1/12...
|
||||
прогрев функции 1/8: попытка 2/12...
|
||||
прогрев функции 1/8: попытка 3/12...
|
||||
прогрев функции 1/8: попытка 4/12...
|
||||
прогрев функции 1/8: попытка 5/12...
|
||||
прогрев функции 1/8: попытка 6/12...
|
||||
прогрев функции 1/8: попытка 7/12...
|
||||
прогрев функции 1/8: попытка 8/12...
|
||||
прогрев функции 1/8: попытка 9/12...
|
||||
прогрев функции 1/8: попытка 10/12...
|
||||
прогрев функции 1/8: попытка 11/12...
|
||||
прогрев функции 1/8: попытка 12/12...
|
||||
функция 1/8 — готова
|
||||
прогрев функции 2/8: попытка 1/12...
|
||||
прогрев функции 2/8: попытка 2/12...
|
||||
прогрев функции 2/8: попытка 3/12...
|
||||
прогрев функции 2/8: попытка 4/12...
|
||||
прогрев функции 2/8: попытка 5/12...
|
||||
прогрев функции 2/8: попытка 6/12...
|
||||
прогрев функции 2/8: попытка 7/12...
|
||||
прогрев функции 2/8: попытка 8/12...
|
||||
прогрев функции 2/8: попытка 9/12...
|
||||
прогрев функции 2/8: попытка 10/12...
|
||||
прогрев функции 2/8: попытка 11/12...
|
||||
прогрев функции 2/8: попытка 12/12...
|
||||
функция 2/8 — готова
|
||||
прогрев функции 3/8: попытка 1/12...
|
||||
прогрев функции 3/8: попытка 2/12...
|
||||
прогрев функции 3/8: попытка 3/12...
|
||||
прогрев функции 3/8: попытка 4/12...
|
||||
прогрев функции 3/8: попытка 5/12...
|
||||
прогрев функции 3/8: попытка 6/12...
|
||||
прогрев функции 3/8: попытка 7/12...
|
||||
прогрев функции 3/8: попытка 8/12...
|
||||
прогрев функции 3/8: попытка 9/12...
|
||||
прогрев функции 3/8: попытка 10/12...
|
||||
прогрев функции 3/8: попытка 11/12...
|
||||
прогрев функции 3/8: попытка 12/12...
|
||||
функция 3/8 — готова
|
||||
прогрев функции 4/8: попытка 1/12...
|
||||
прогрев функции 4/8: попытка 2/12...
|
||||
прогрев функции 4/8: попытка 3/12...
|
||||
прогрев функции 4/8: попытка 4/12...
|
||||
прогрев функции 4/8: попытка 5/12...
|
||||
прогрев функции 4/8: попытка 6/12...
|
||||
прогрев функции 4/8: попытка 7/12...
|
||||
прогрев функции 4/8: попытка 8/12...
|
||||
прогрев функции 4/8: попытка 9/12...
|
||||
прогрев функции 4/8: попытка 10/12...
|
||||
прогрев функции 4/8: попытка 11/12...
|
||||
прогрев функции 4/8: попытка 12/12...
|
||||
функция 4/8 — готова
|
||||
прогрев функции 5/8: попытка 1/12...
|
||||
прогрев функции 5/8: попытка 2/12...
|
||||
прогрев функции 5/8: попытка 3/12...
|
||||
прогрев функции 5/8: попытка 4/12...
|
||||
прогрев функции 5/8: попытка 5/12...
|
||||
прогрев функции 5/8: попытка 6/12...
|
||||
прогрев функции 5/8: попытка 7/12...
|
||||
прогрев функции 5/8: попытка 8/12...
|
||||
прогрев функции 5/8: попытка 9/12...
|
||||
прогрев функции 5/8: попытка 10/12...
|
||||
прогрев функции 5/8: попытка 11/12...
|
||||
прогрев функции 5/8: попытка 12/12...
|
||||
функция 5/8 — готова
|
||||
прогрев функции 6/8: попытка 1/12...
|
||||
прогрев функции 6/8: попытка 2/12...
|
||||
прогрев функции 6/8: попытка 3/12...
|
||||
прогрев функции 6/8: попытка 4/12...
|
||||
прогрев функции 6/8: попытка 5/12...
|
||||
прогрев функции 6/8: попытка 6/12...
|
||||
прогрев функции 6/8: попытка 7/12...
|
||||
прогрев функции 6/8: попытка 8/12...
|
||||
прогрев функции 6/8: попытка 9/12...
|
||||
прогрев функции 6/8: попытка 10/12...
|
||||
прогрев функции 6/8: попытка 11/12...
|
||||
прогрев функции 6/8: попытка 12/12...
|
||||
функция 6/8 — готова
|
||||
прогрев функции 7/8: попытка 1/12...
|
||||
прогрев функции 7/8: попытка 2/12...
|
||||
прогрев функции 7/8: попытка 3/12...
|
||||
прогрев функции 7/8: попытка 4/12...
|
||||
прогрев функции 7/8: попытка 5/12...
|
||||
прогрев функции 7/8: попытка 6/12...
|
||||
прогрев функции 7/8: попытка 7/12...
|
||||
прогрев функции 7/8: попытка 8/12...
|
||||
прогрев функции 7/8: попытка 9/12...
|
||||
прогрев функции 7/8: попытка 10/12...
|
||||
прогрев функции 7/8: попытка 11/12...
|
||||
прогрев функции 7/8: попытка 12/12...
|
||||
функция 7/8 — готова
|
||||
прогрев функции 8/8: попытка 1/12...
|
||||
прогрев функции 8/8: попытка 2/12...
|
||||
прогрев функции 8/8: попытка 3/12...
|
||||
прогрев функции 8/8: попытка 4/12...
|
||||
прогрев функции 8/8: попытка 5/12...
|
||||
прогрев функции 8/8: попытка 6/12...
|
||||
прогрев функции 8/8: попытка 7/12...
|
||||
прогрев функции 8/8: попытка 8/12...
|
||||
прогрев функции 8/8: попытка 9/12...
|
||||
прогрев функции 8/8: попытка 10/12...
|
||||
прогрев функции 8/8: попытка 11/12...
|
||||
прогрев функции 8/8: попытка 12/12...
|
||||
функция 8/8 — готова
|
||||
FAIL: Параллельный INVOKE (livetest): 8/8 упали
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=2 FAIL=1
|
||||
========================================
|
||||
[0;31m ✗ test_stress.sh — FAILED (exit 1)[0m
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ Скриптов: PASS=4 FAIL=4 1020s ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
Упавшие скрипты:
|
||||
[0;31m✗ test_isolation.sh[0m
|
||||
[0;31m✗ test_update.sh[0m
|
||||
[0;31m✗ test_invoke.sh[0m
|
||||
[0;31m✗ test_stress.sh[0m
|
||||
@@ -1,287 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_18-08-19.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: найдено 16 тест-namespace-ов, удаляем...
|
||||
namespace "fission-144f17f38393c868" deleted
|
||||
namespace "fission-15b9941047ab2ca0" deleted
|
||||
namespace "fission-436ddc8693d9dde4" deleted
|
||||
namespace "fission-4374fb3483a0d192" deleted
|
||||
namespace "fission-45ffb6f300b47b43" deleted
|
||||
namespace "fission-49fdb3ee07292199" deleted
|
||||
namespace "fission-4b897bd205a45e43" deleted
|
||||
namespace "fission-6b5d473720accdd7" deleted
|
||||
namespace "fission-6dca95c9a39f6956" deleted
|
||||
namespace "fission-8fade6140d3543be" deleted
|
||||
namespace "fission-9d30ab49ffc0a54c" deleted
|
||||
namespace "fission-ab0feb7a9dfb290c" deleted
|
||||
namespace "fission-b8c041b48a2ff140" deleted
|
||||
namespace "fission-c3511430abe5675b" deleted
|
||||
namespace "fission-ca1c1fc777879918" deleted
|
||||
namespace "fission-e510a990c82f30b2" deleted
|
||||
pre_cleanup.sh: очистка завершена.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=c265d58b
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-4f2623887cc7119c
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=0be6b674
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-0be6b674 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=954bd7d6
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T15:24:08Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T16:09:09Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T15:09:10Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=601bf90d
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /cb6533e3a6b0/t-alice-fn ≠ /c49acc82f3ed/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=3adb41f6
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
FAIL: CREATE v1 → 201 — got 502, want 201
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-3adb41f6 status= raw= err=
|
||||
TRY=2 fn=t-upd-fn-3adb41f6 status= raw= err=
|
||||
TRY=3 fn=t-upd-fn-3adb41f6 status= raw= err=
|
||||
TRY=4 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=5 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=6 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=7 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=8 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=9 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=10 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=11 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=12 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
FAIL: Invoke v1 не стабилизировался
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
FAIL: PUT /code v2 → 200 — got 404, want 200
|
||||
FAIL: PUT /code: updated=''
|
||||
FAIL: PUT /code: package пустой
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=2 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=3 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=4 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=5 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=6 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=7 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=8 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=9 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=10 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=11 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=12 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=13 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=14 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=15 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
FAIL: Invoke после update не вернул version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=2 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=3 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=4 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=5 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=6 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=7 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=8 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=9 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
TRY=10 fn=t-upd-fn-3adb41f6 status= raw= err=function "t-upd-fn-3adb41f6" not found
|
||||
@@ -1,220 +0,0 @@
|
||||
LOG: /home/naeel/terra/fission/scripts/../test-results/2026-05-03_18-25-27.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=72c7e028
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-test-39a887b8346480e2
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=20730380
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-20730380 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=3526cd93
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T15:41:03Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T16:26:04Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T15:26:05Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=e8eadbdb
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /50968da3258f/t-alice-fn ≠ /4b3450dbcc1f/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=93d4e907
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-93d4e907 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-93d4e907 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-93d4e907 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-93d4e907 status=404 raw=404 page not found err=
|
||||
@@ -1,238 +0,0 @@
|
||||
LOG: /home/naeel/fission/scripts/../test-results/2026-05-03_19-13-39.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=71e4887e
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-a8031d7c91fb0913
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=7428416b
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-7428416b → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=cc412f6c
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T15:29:41Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T16:14:42Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T15:14:44Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=d921f62c
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /4d5a8a9ec92c/t-alice-fn ≠ /bba3a109e2c3/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=e9cdb465
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke v1 не стабилизировался
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=t-upd-fn-e9cdb465-pkg-mopwy76a
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
@@ -1,238 +0,0 @@
|
||||
LOG: /home/naeel/fission/scripts/../test-results/2026-05-03_19-13-39.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=71e4887e
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-a8031d7c91fb0913
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=7428416b
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (000)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-7428416b → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=cc412f6c
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T15:29:41Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T16:14:42Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T15:14:44Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=d921f62c
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /4d5a8a9ec92c/t-alice-fn ≠ /bba3a109e2c3/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=e9cdb465
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke v1 не стабилизировался
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=t-upd-fn-e9cdb465-pkg-mopwy76a
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e9cdb465 status=404 raw=404 page not found err=
|
||||
@@ -1,563 +0,0 @@
|
||||
LOG: /home/naeel/fission/scripts/../test-results/2026-05-03_19-27-29.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=60c056c8
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-test-eefba24734b47f9a
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=4389efa7
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (400)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-4389efa7 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=41287101
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T15:43:08Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T16:28:09Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T15:28:10Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=e51b5b24
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /3a28b708954b/t-alice-fn ≠ /cd66c0880a7a/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=e557f2e3
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke v1 не стабилизировался
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=t-upd-fn-e557f2e3-pkg-mopxfjxq
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после update не вернул version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после v3 update не стабилизировался
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=7 FAIL=3
|
||||
========================================
|
||||
[0;31m ✗ test_update.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_invoke.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=b9f17c9f
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
FAIL: NodeJS invoke не стабилизировался за 60s
|
||||
FAIL: NodeJS второй invoke → status=404
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
FAIL: Python invoke не стабилизировался за 75s
|
||||
|
||||
>>> Invoke response fields
|
||||
FAIL: invoke.status = 404
|
||||
PASS: invoke.latency_ms = 1 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=5 FAIL=4
|
||||
========================================
|
||||
[0;31m ✗ test_invoke.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_linters.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
=== NODEJS ===
|
||||
[PASS] N01 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N02 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N03 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) { const x = @@@; };
|
||||
|
||||
[PASS] N04 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) {
|
||||
|
||||
|
||||
SyntaxError: Unexpected
|
||||
[PASS] N05 | nodejs | exp=fail got=False | <code>:1
|
||||
const x = {a: 1,, b: 2};
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token ','
|
||||
at wrapSa
|
||||
[PASS] N06 | nodejs | exp=fail got=False | <code>:1
|
||||
function foo( { return 1; }
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected number
|
||||
at
|
||||
|
||||
=== PYTHON ===
|
||||
[PASS] P01 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P02 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P03 | python | exp=fail got=False | Sorry: IndentationError: expected an indented block after function definition on line 1 (fission-lin
|
||||
[PASS] P04 | python | exp=fail got=False | File "<code>", line 2
|
||||
x = @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] P05 | python | exp=fail got=False | File "<code>", line 1
|
||||
def main(data)
|
||||
^
|
||||
SyntaxError: expected ':'
|
||||
[PASS] P06 | python | exp=fail got=False | File "<code>", line 2
|
||||
def main(
|
||||
^
|
||||
SyntaxError: '(' was never closed
|
||||
|
||||
=== RUBY ===
|
||||
[PASS] R01 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R02 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R03 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected {, expecting ')' (SyntaxError)
|
||||
{ status: 200 }
|
||||
^
|
||||
[PASS] R04 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected local variable or method, expecting '}' (SyntaxError)
|
||||
{ s
|
||||
[PASS] R05 | ruby | exp=fail got=False | ruby: <code>:3: syntax error, unexpected `end', expecting ')' (SyntaxError)
|
||||
|
||||
=== PHP ===
|
||||
[PASS] H01 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H02 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H03 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "return" in <code> on line 4
|
||||
Errors parsing <code>
|
||||
[PASS] H04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token ";" in <code> on line 2
|
||||
Errors parsing <code>
|
||||
[PASS] H05 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "{", expecting variable in <code> on line 2
|
||||
Errors
|
||||
|
||||
=== GO ===
|
||||
[PASS] G01 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G02 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G03 | go | exp=fail got=False | code.go:2:15: expected ')', found '{' (and 3 more errors)
|
||||
[PASS] G04 | go | exp=fail got=False | code.go:4:1: missing import path (and 1 more errors)
|
||||
[PASS] G05 | go | exp=fail got=False | code.go:3:10: expected operand, found 'ILLEGAL' (and 6 more errors)
|
||||
|
||||
=== ГРАНИЧНЫЕ ===
|
||||
[PASS] E01 | nodejs | exp=fail got=False | Код пустой.
|
||||
[PASS] E02 | python | exp=fail got=False | File "<code>", line 1
|
||||
asd @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] E03 | ruby | exp=fail got=False | ruby: <code>:1: syntax error, unexpected ';', expecting ')' (SyntaxError)
|
||||
def foo(;;; end
|
||||
^
|
||||
[PASS] E04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected end of file in <code> on line 1
|
||||
Errors parsing <code>
|
||||
|
||||
========================
|
||||
PASS=31 FAIL=0
|
||||
========================
|
||||
[0;32m ✓ test_linters.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_stress.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_stress.sh run=d08a7e02 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
прогрев функции 1/8: попытка 1/12...
|
||||
прогрев функции 1/8: попытка 2/12...
|
||||
прогрев функции 1/8: попытка 3/12...
|
||||
прогрев функции 1/8: попытка 4/12...
|
||||
прогрев функции 1/8: попытка 5/12...
|
||||
прогрев функции 1/8: попытка 6/12...
|
||||
прогрев функции 1/8: попытка 7/12...
|
||||
прогрев функции 1/8: попытка 8/12...
|
||||
прогрев функции 1/8: попытка 9/12...
|
||||
прогрев функции 1/8: попытка 10/12...
|
||||
прогрев функции 1/8: попытка 11/12...
|
||||
прогрев функции 1/8: попытка 12/12...
|
||||
функция 1/8 — готова
|
||||
прогрев функции 2/8: попытка 1/12...
|
||||
прогрев функции 2/8: попытка 2/12...
|
||||
прогрев функции 2/8: попытка 3/12...
|
||||
прогрев функции 2/8: попытка 4/12...
|
||||
прогрев функции 2/8: попытка 5/12...
|
||||
прогрев функции 2/8: попытка 6/12...
|
||||
прогрев функции 2/8: попытка 7/12...
|
||||
прогрев функции 2/8: попытка 8/12...
|
||||
прогрев функции 2/8: попытка 9/12...
|
||||
прогрев функции 2/8: попытка 10/12...
|
||||
прогрев функции 2/8: попытка 11/12...
|
||||
прогрев функции 2/8: попытка 12/12...
|
||||
функция 2/8 — готова
|
||||
прогрев функции 3/8: попытка 1/12...
|
||||
прогрев функции 3/8: попытка 2/12...
|
||||
прогрев функции 3/8: попытка 3/12...
|
||||
прогрев функции 3/8: попытка 4/12...
|
||||
прогрев функции 3/8: попытка 5/12...
|
||||
прогрев функции 3/8: попытка 6/12...
|
||||
прогрев функции 3/8: попытка 7/12...
|
||||
прогрев функции 3/8: попытка 8/12...
|
||||
прогрев функции 3/8: попытка 9/12...
|
||||
прогрев функции 3/8: попытка 10/12...
|
||||
прогрев функции 3/8: попытка 11/12...
|
||||
прогрев функции 3/8: попытка 12/12...
|
||||
функция 3/8 — готова
|
||||
прогрев функции 4/8: попытка 1/12...
|
||||
прогрев функции 4/8: попытка 2/12...
|
||||
прогрев функции 4/8: попытка 3/12...
|
||||
прогрев функции 4/8: попытка 4/12...
|
||||
прогрев функции 4/8: попытка 5/12...
|
||||
прогрев функции 4/8: попытка 6/12...
|
||||
прогрев функции 4/8: попытка 7/12...
|
||||
прогрев функции 4/8: попытка 8/12...
|
||||
прогрев функции 4/8: попытка 9/12...
|
||||
прогрев функции 4/8: попытка 10/12...
|
||||
прогрев функции 4/8: попытка 11/12...
|
||||
прогрев функции 4/8: попытка 12/12...
|
||||
функция 4/8 — готова
|
||||
прогрев функции 5/8: попытка 1/12...
|
||||
прогрев функции 5/8: попытка 2/12...
|
||||
прогрев функции 5/8: попытка 3/12...
|
||||
прогрев функции 5/8: попытка 4/12...
|
||||
прогрев функции 5/8: попытка 5/12...
|
||||
прогрев функции 5/8: попытка 6/12...
|
||||
прогрев функции 5/8: попытка 7/12...
|
||||
прогрев функции 5/8: попытка 8/12...
|
||||
прогрев функции 5/8: попытка 9/12...
|
||||
прогрев функции 5/8: попытка 10/12...
|
||||
прогрев функции 5/8: попытка 11/12...
|
||||
прогрев функции 5/8: попытка 12/12...
|
||||
функция 5/8 — готова
|
||||
прогрев функции 6/8: попытка 1/12...
|
||||
прогрев функции 6/8: попытка 2/12...
|
||||
прогрев функции 6/8: попытка 3/12...
|
||||
прогрев функции 6/8: попытка 4/12...
|
||||
прогрев функции 6/8: попытка 5/12...
|
||||
прогрев функции 6/8: попытка 6/12...
|
||||
прогрев функции 6/8: попытка 7/12...
|
||||
прогрев функции 6/8: попытка 8/12...
|
||||
прогрев функции 6/8: попытка 9/12...
|
||||
прогрев функции 6/8: попытка 10/12...
|
||||
прогрев функции 6/8: попытка 11/12...
|
||||
прогрев функции 6/8: попытка 12/12...
|
||||
функция 6/8 — готова
|
||||
прогрев функции 7/8: попытка 1/12...
|
||||
прогрев функции 7/8: попытка 2/12...
|
||||
прогрев функции 7/8: попытка 3/12...
|
||||
прогрев функции 7/8: попытка 4/12...
|
||||
прогрев функции 7/8: попытка 5/12...
|
||||
прогрев функции 7/8: попытка 6/12...
|
||||
прогрев функции 7/8: попытка 7/12...
|
||||
прогрев функции 7/8: попытка 8/12...
|
||||
прогрев функции 7/8: попытка 9/12...
|
||||
прогрев функции 7/8: попытка 10/12...
|
||||
прогрев функции 7/8: попытка 11/12...
|
||||
прогрев функции 7/8: попытка 12/12...
|
||||
функция 7/8 — готова
|
||||
прогрев функции 8/8: попытка 1/12...
|
||||
прогрев функции 8/8: попытка 2/12...
|
||||
прогрев функции 8/8: попытка 3/12...
|
||||
прогрев функции 8/8: попытка 4/12...
|
||||
прогрев функции 8/8: попытка 5/12...
|
||||
прогрев функции 8/8: попытка 6/12...
|
||||
прогрев функции 8/8: попытка 7/12...
|
||||
прогрев функции 8/8: попытка 8/12...
|
||||
прогрев функции 8/8: попытка 9/12...
|
||||
прогрев функции 8/8: попытка 10/12...
|
||||
прогрев функции 8/8: попытка 11/12...
|
||||
прогрев функции 8/8: попытка 12/12...
|
||||
функция 8/8 — готова
|
||||
FAIL: Параллельный INVOKE (livetest): 8/8 упали
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=2 FAIL=1
|
||||
========================================
|
||||
[0;31m ✗ test_stress.sh — FAILED (exit 1)[0m
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ Скриптов: PASS=4 FAIL=4 1224s ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
Упавшие скрипты:
|
||||
[0;31m✗ test_isolation.sh[0m
|
||||
[0;31m✗ test_update.sh[0m
|
||||
[0;31m✗ test_invoke.sh[0m
|
||||
[0;31m✗ test_stress.sh[0m
|
||||
@@ -1,563 +0,0 @@
|
||||
LOG: /home/naeel/fission/scripts/../test-results/2026-05-03_19-27-29.log
|
||||
|
||||
>>> Предварительная очистка тест-namespace-ов...
|
||||
pre_cleanup.sh: тест-namespace-ы не найдены, пропускаем.
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ fission-console — полный тест-сьют ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_auth.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_auth.sh run=60c056c8
|
||||
========================================
|
||||
|
||||
>>> POST /auth: пустой токен → 400
|
||||
PASS: POST /auth пустой token → 400 (HTTP 400)
|
||||
|
||||
>>> POST /auth: токен < 6 символов → 401
|
||||
PASS: POST /auth токен 2 символа → 401 (HTTP 401)
|
||||
PASS: POST /auth токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> POST /auth: токен ≥6 символов → 200
|
||||
PASS: POST /auth валидный токен → 200 (HTTP 200)
|
||||
PASS: POST /auth namespace=fission-...
|
||||
PASS: POST /auth ok=true
|
||||
|
||||
>>> POST /auth: невалидный JSON → 400
|
||||
PASS: POST /auth невалидный JSON → 400
|
||||
|
||||
>>> POST /auth: GET метод → 405
|
||||
PASS: GET /auth → 405
|
||||
|
||||
>>> X-Auth-Token: отсутствует → 401
|
||||
PASS: GET /functions без токена → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен < 6 символов → 401
|
||||
PASS: GET /functions токен 2 символа → 401 (HTTP 401)
|
||||
PASS: GET /functions токен 5 символов → 401 (HTTP 401)
|
||||
|
||||
>>> X-Auth-Token: токен ≥6 символов → 200
|
||||
PASS: GET /functions валидный X-Auth-Token → 200 (HTTP 200)
|
||||
|
||||
>>> Authorization: Bearer → 200
|
||||
PASS: GET /functions Authorization: Bearer → 200 (HTTP 200)
|
||||
|
||||
>>> Namespace детерминирован (одинаковый токен → одинаковый NS)
|
||||
PASS: Namespace детерминирован: fission-test-eefba24734b47f9a
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=14 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_auth.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_functions_crud.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_functions_crud.sh run=4389efa7
|
||||
========================================
|
||||
|
||||
>>> CREATE: базовый nodejs → 201
|
||||
PASS: CREATE nodejs → 201 (HTTP 201)
|
||||
PASS: CREATE: name в ответе
|
||||
PASS: CREATE: route непустой
|
||||
PASS: CREATE: package непустой
|
||||
|
||||
>>> CREATE: дубликат → 409
|
||||
PASS: CREATE дубликат → 409 (HTTP 409)
|
||||
|
||||
>>> CREATE: без name → 400
|
||||
PASS: CREATE без name → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без language → 400
|
||||
PASS: CREATE без language → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: без code → 400
|
||||
PASS: CREATE без code → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: невалидный язык → 400
|
||||
PASS: CREATE язык=cobol → 400 (HTTP 400)
|
||||
|
||||
>>> CREATE: валидация имени
|
||||
PASS: CREATE имя с заглавной → 400 (HTTP 400)
|
||||
PASS: CREATE имя с ведущим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с завершающим дефисом → 400 (HTTP 400)
|
||||
PASS: CREATE имя с пробелом → 400 (HTTP 400)
|
||||
PASS: CREATE имя 58 символов → 400 (HTTP 400)
|
||||
PASS: CREATE имя 57 символов → 201 (HTTP 201)
|
||||
|
||||
>>> CREATE: код > 1MB → 400 или 413
|
||||
PASS: CREATE код > 1MB → отклонён (400)
|
||||
|
||||
>>> LIST: GET /functions → 200, массив
|
||||
PASS: LIST GET /functions → 200 (HTTP 200)
|
||||
PASS: LIST: содержит ≥1 функцию (создали выше)
|
||||
|
||||
>>> LIST: новый пользователь → пустой массив
|
||||
PASS: LIST новый пользователь → 200 (HTTP 200)
|
||||
PASS: LIST новый пользователь → []
|
||||
|
||||
>>> GET: /functions/{name} → 200
|
||||
PASS: GET /functions/t-crud-fn-4389efa7 → 200 (HTTP 200)
|
||||
PASS: GET: name совпадает
|
||||
PASS: GET: environment непустой (console-nodejs-env)
|
||||
|
||||
>>> GET: несуществующая функция → 404
|
||||
PASS: GET несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: /functions/{name} → 200
|
||||
PASS: DELETE crud-fn → 200 (HTTP 200)
|
||||
PASS: DELETE: deleted=true
|
||||
|
||||
>>> DELETE: повторное удаление → 404
|
||||
PASS: DELETE повторное → 404 (HTTP 404)
|
||||
|
||||
>>> GET: после DELETE → 404
|
||||
PASS: GET после DELETE → 404 (HTTP 404)
|
||||
|
||||
>>> DELETE: несуществующая функция → 404
|
||||
PASS: DELETE несуществующая → 404 (HTTP 404)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=29 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_functions_crud.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_ttl.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_ttl.sh run=41287101
|
||||
========================================
|
||||
|
||||
>>> CREATE без TTL → expires_at отсутствует
|
||||
PASS: CREATE без TTL: expires_at отсутствует
|
||||
|
||||
>>> CREATE с TTL=15m → expires_at присутствует
|
||||
PASS: CREATE TTL=15m: expires_at=2026-05-03T15:43:08Z
|
||||
PASS: expires_at формат RFC3339
|
||||
|
||||
>>> CREATE с TTL=1h → expires_at присутствует
|
||||
PASS: CREATE TTL=1h: expires_at=2026-05-03T16:28:09Z
|
||||
|
||||
>>> CREATE с TTL=2d → expires_at присутствует
|
||||
PASS: CREATE TTL=2d: expires_at=2026-05-05T15:28:10Z
|
||||
|
||||
>>> CREATE TTL невалидный → 400 (без создания объектов)
|
||||
PASS: TTL='99z' → 400
|
||||
PASS: TTL='abc' → 400
|
||||
PASS: TTL='0d' → 400
|
||||
PASS: TTL='-1h' → 400
|
||||
|
||||
>>> LIST: функция с TTL видна в списке
|
||||
PASS: LIST: видно ≥3 функции (2 с TTL, 1 без)
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
[0;32m ✓ test_ttl.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_isolation.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=e51b5b24
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
FAIL: Alice LIST не содержит t-alice-fn (count=1)
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
FAIL: Bob LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /3a28b708954b/t-alice-fn ≠ /cd66c0880a7a/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=2
|
||||
========================================
|
||||
[0;31m ✗ test_isolation.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_update.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_update.sh run=e557f2e3
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke v1 не стабилизировался
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=t-upd-fn-e557f2e3-pkg-mopxfjxq
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после update не вернул version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-upd-fn-e557f2e3 status=404 raw=404 page not found err=
|
||||
FAIL: Invoke после v3 update не стабилизировался
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=7 FAIL=3
|
||||
========================================
|
||||
[0;31m ✗ test_update.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_invoke.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=b9f17c9f
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-node-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
FAIL: NodeJS invoke не стабилизировался за 60s
|
||||
FAIL: NodeJS второй invoke → status=404
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=2 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=3 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=4 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=5 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=6 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=7 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=8 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=9 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=10 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=11 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=12 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=13 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=14 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
TRY=15 fn=t-py-fn-b9f17c9f status=404 raw=404 page not found err=
|
||||
FAIL: Python invoke не стабилизировался за 75s
|
||||
|
||||
>>> Invoke response fields
|
||||
FAIL: invoke.status = 404
|
||||
PASS: invoke.latency_ms = 1 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=5 FAIL=4
|
||||
========================================
|
||||
[0;31m ✗ test_invoke.sh — FAILED (exit 1)[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_linters.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
=== NODEJS ===
|
||||
[PASS] N01 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N02 | nodejs | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] N03 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) { const x = @@@; };
|
||||
|
||||
[PASS] N04 | nodejs | exp=fail got=False | <code>:1
|
||||
module.exports = function(ctx) {
|
||||
|
||||
|
||||
SyntaxError: Unexpected
|
||||
[PASS] N05 | nodejs | exp=fail got=False | <code>:1
|
||||
const x = {a: 1,, b: 2};
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token ','
|
||||
at wrapSa
|
||||
[PASS] N06 | nodejs | exp=fail got=False | <code>:1
|
||||
function foo( { return 1; }
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected number
|
||||
at
|
||||
|
||||
=== PYTHON ===
|
||||
[PASS] P01 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P02 | python | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] P03 | python | exp=fail got=False | Sorry: IndentationError: expected an indented block after function definition on line 1 (fission-lin
|
||||
[PASS] P04 | python | exp=fail got=False | File "<code>", line 2
|
||||
x = @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] P05 | python | exp=fail got=False | File "<code>", line 1
|
||||
def main(data)
|
||||
^
|
||||
SyntaxError: expected ':'
|
||||
[PASS] P06 | python | exp=fail got=False | File "<code>", line 2
|
||||
def main(
|
||||
^
|
||||
SyntaxError: '(' was never closed
|
||||
|
||||
=== RUBY ===
|
||||
[PASS] R01 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R02 | ruby | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] R03 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected {, expecting ')' (SyntaxError)
|
||||
{ status: 200 }
|
||||
^
|
||||
[PASS] R04 | ruby | exp=fail got=False | ruby: <code>:2: syntax error, unexpected local variable or method, expecting '}' (SyntaxError)
|
||||
{ s
|
||||
[PASS] R05 | ruby | exp=fail got=False | ruby: <code>:3: syntax error, unexpected `end', expecting ')' (SyntaxError)
|
||||
|
||||
=== PHP ===
|
||||
[PASS] H01 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H02 | php | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] H03 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "return" in <code> on line 4
|
||||
Errors parsing <code>
|
||||
[PASS] H04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token ";" in <code> on line 2
|
||||
Errors parsing <code>
|
||||
[PASS] H05 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected token "{", expecting variable in <code> on line 2
|
||||
Errors
|
||||
|
||||
=== GO ===
|
||||
[PASS] G01 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G02 | go | exp=ok got=True | ✅ Синтаксис корректен.
|
||||
[PASS] G03 | go | exp=fail got=False | code.go:2:15: expected ')', found '{' (and 3 more errors)
|
||||
[PASS] G04 | go | exp=fail got=False | code.go:4:1: missing import path (and 1 more errors)
|
||||
[PASS] G05 | go | exp=fail got=False | code.go:3:10: expected operand, found 'ILLEGAL' (and 6 more errors)
|
||||
|
||||
=== ГРАНИЧНЫЕ ===
|
||||
[PASS] E01 | nodejs | exp=fail got=False | Код пустой.
|
||||
[PASS] E02 | python | exp=fail got=False | File "<code>", line 1
|
||||
asd @@@
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
[PASS] E03 | ruby | exp=fail got=False | ruby: <code>:1: syntax error, unexpected ';', expecting ')' (SyntaxError)
|
||||
def foo(;;; end
|
||||
^
|
||||
[PASS] E04 | php | exp=fail got=False | PHP Parse error: syntax error, unexpected end of file in <code> on line 1
|
||||
Errors parsing <code>
|
||||
|
||||
========================
|
||||
PASS=31 FAIL=0
|
||||
========================
|
||||
[0;32m ✓ test_linters.sh — PASSED[0m
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
ЗАПУСК: test_stress.sh
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
========================================
|
||||
test_stress.sh run=d08a7e02 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
прогрев функции 1/8: попытка 1/12...
|
||||
прогрев функции 1/8: попытка 2/12...
|
||||
прогрев функции 1/8: попытка 3/12...
|
||||
прогрев функции 1/8: попытка 4/12...
|
||||
прогрев функции 1/8: попытка 5/12...
|
||||
прогрев функции 1/8: попытка 6/12...
|
||||
прогрев функции 1/8: попытка 7/12...
|
||||
прогрев функции 1/8: попытка 8/12...
|
||||
прогрев функции 1/8: попытка 9/12...
|
||||
прогрев функции 1/8: попытка 10/12...
|
||||
прогрев функции 1/8: попытка 11/12...
|
||||
прогрев функции 1/8: попытка 12/12...
|
||||
функция 1/8 — готова
|
||||
прогрев функции 2/8: попытка 1/12...
|
||||
прогрев функции 2/8: попытка 2/12...
|
||||
прогрев функции 2/8: попытка 3/12...
|
||||
прогрев функции 2/8: попытка 4/12...
|
||||
прогрев функции 2/8: попытка 5/12...
|
||||
прогрев функции 2/8: попытка 6/12...
|
||||
прогрев функции 2/8: попытка 7/12...
|
||||
прогрев функции 2/8: попытка 8/12...
|
||||
прогрев функции 2/8: попытка 9/12...
|
||||
прогрев функции 2/8: попытка 10/12...
|
||||
прогрев функции 2/8: попытка 11/12...
|
||||
прогрев функции 2/8: попытка 12/12...
|
||||
функция 2/8 — готова
|
||||
прогрев функции 3/8: попытка 1/12...
|
||||
прогрев функции 3/8: попытка 2/12...
|
||||
прогрев функции 3/8: попытка 3/12...
|
||||
прогрев функции 3/8: попытка 4/12...
|
||||
прогрев функции 3/8: попытка 5/12...
|
||||
прогрев функции 3/8: попытка 6/12...
|
||||
прогрев функции 3/8: попытка 7/12...
|
||||
прогрев функции 3/8: попытка 8/12...
|
||||
прогрев функции 3/8: попытка 9/12...
|
||||
прогрев функции 3/8: попытка 10/12...
|
||||
прогрев функции 3/8: попытка 11/12...
|
||||
прогрев функции 3/8: попытка 12/12...
|
||||
функция 3/8 — готова
|
||||
прогрев функции 4/8: попытка 1/12...
|
||||
прогрев функции 4/8: попытка 2/12...
|
||||
прогрев функции 4/8: попытка 3/12...
|
||||
прогрев функции 4/8: попытка 4/12...
|
||||
прогрев функции 4/8: попытка 5/12...
|
||||
прогрев функции 4/8: попытка 6/12...
|
||||
прогрев функции 4/8: попытка 7/12...
|
||||
прогрев функции 4/8: попытка 8/12...
|
||||
прогрев функции 4/8: попытка 9/12...
|
||||
прогрев функции 4/8: попытка 10/12...
|
||||
прогрев функции 4/8: попытка 11/12...
|
||||
прогрев функции 4/8: попытка 12/12...
|
||||
функция 4/8 — готова
|
||||
прогрев функции 5/8: попытка 1/12...
|
||||
прогрев функции 5/8: попытка 2/12...
|
||||
прогрев функции 5/8: попытка 3/12...
|
||||
прогрев функции 5/8: попытка 4/12...
|
||||
прогрев функции 5/8: попытка 5/12...
|
||||
прогрев функции 5/8: попытка 6/12...
|
||||
прогрев функции 5/8: попытка 7/12...
|
||||
прогрев функции 5/8: попытка 8/12...
|
||||
прогрев функции 5/8: попытка 9/12...
|
||||
прогрев функции 5/8: попытка 10/12...
|
||||
прогрев функции 5/8: попытка 11/12...
|
||||
прогрев функции 5/8: попытка 12/12...
|
||||
функция 5/8 — готова
|
||||
прогрев функции 6/8: попытка 1/12...
|
||||
прогрев функции 6/8: попытка 2/12...
|
||||
прогрев функции 6/8: попытка 3/12...
|
||||
прогрев функции 6/8: попытка 4/12...
|
||||
прогрев функции 6/8: попытка 5/12...
|
||||
прогрев функции 6/8: попытка 6/12...
|
||||
прогрев функции 6/8: попытка 7/12...
|
||||
прогрев функции 6/8: попытка 8/12...
|
||||
прогрев функции 6/8: попытка 9/12...
|
||||
прогрев функции 6/8: попытка 10/12...
|
||||
прогрев функции 6/8: попытка 11/12...
|
||||
прогрев функции 6/8: попытка 12/12...
|
||||
функция 6/8 — готова
|
||||
прогрев функции 7/8: попытка 1/12...
|
||||
прогрев функции 7/8: попытка 2/12...
|
||||
прогрев функции 7/8: попытка 3/12...
|
||||
прогрев функции 7/8: попытка 4/12...
|
||||
прогрев функции 7/8: попытка 5/12...
|
||||
прогрев функции 7/8: попытка 6/12...
|
||||
прогрев функции 7/8: попытка 7/12...
|
||||
прогрев функции 7/8: попытка 8/12...
|
||||
прогрев функции 7/8: попытка 9/12...
|
||||
прогрев функции 7/8: попытка 10/12...
|
||||
прогрев функции 7/8: попытка 11/12...
|
||||
прогрев функции 7/8: попытка 12/12...
|
||||
функция 7/8 — готова
|
||||
прогрев функции 8/8: попытка 1/12...
|
||||
прогрев функции 8/8: попытка 2/12...
|
||||
прогрев функции 8/8: попытка 3/12...
|
||||
прогрев функции 8/8: попытка 4/12...
|
||||
прогрев функции 8/8: попытка 5/12...
|
||||
прогрев функции 8/8: попытка 6/12...
|
||||
прогрев функции 8/8: попытка 7/12...
|
||||
прогрев функции 8/8: попытка 8/12...
|
||||
прогрев функции 8/8: попытка 9/12...
|
||||
прогрев функции 8/8: попытка 10/12...
|
||||
прогрев функции 8/8: попытка 11/12...
|
||||
прогрев функции 8/8: попытка 12/12...
|
||||
функция 8/8 — готова
|
||||
FAIL: Параллельный INVOKE (livetest): 8/8 упали
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=2 FAIL=1
|
||||
========================================
|
||||
[0;31m ✗ test_stress.sh — FAILED (exit 1)[0m
|
||||
|
||||
╔══════════════════════════════════════╗
|
||||
║ Скриптов: PASS=4 FAIL=4 1224s ║
|
||||
╚══════════════════════════════════════╝
|
||||
|
||||
Упавшие скрипты:
|
||||
[0;31m✗ test_isolation.sh[0m
|
||||
[0;31m✗ test_update.sh[0m
|
||||
[0;31m✗ test_invoke.sh[0m
|
||||
[0;31m✗ test_stress.sh[0m
|
||||
@@ -1,27 +0,0 @@
|
||||
|
||||
========================================
|
||||
test_invoke.sh run=be1c4858
|
||||
========================================
|
||||
|
||||
>>> INVOKE несуществующей функции → 404
|
||||
PASS: INVOKE несуществующей → 404 (HTTP 404)
|
||||
|
||||
>>> NodeJS: create + invoke
|
||||
PASS: NodeJS CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-node-fn-be1c4858 status=200 raw=node-invoke-ok err=
|
||||
PASS: NodeJS invoke → 200, тело содержит node-invoke-ok
|
||||
PASS: NodeJS второй invoke → 200 (warm)
|
||||
|
||||
>>> Python: create + invoke
|
||||
PASS: Python CREATE → 201 (HTTP 201)
|
||||
TRY=1 fn=t-py-fn-be1c4858 status=200 raw=py-invoke-ok err=
|
||||
PASS: Python invoke → 200, тело содержит py-invoke-ok
|
||||
|
||||
>>> Invoke response fields
|
||||
PASS: invoke.status = 200
|
||||
PASS: invoke.latency_ms = 10 ms
|
||||
PASS: invoke.response_raw непустой
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=9 FAIL=0
|
||||
========================================
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
========================================
|
||||
test_isolation.sh run=d680fd2c
|
||||
========================================
|
||||
|
||||
>>> Alice создаёт функцию
|
||||
PASS: Alice CREATE t-alice-fn → 201 (HTTP 201)
|
||||
|
||||
>>> Bob GET t-alice-fn → 404
|
||||
PASS: Bob GET t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob DELETE t-alice-fn → 404
|
||||
PASS: Bob DELETE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Bob PUT t-alice-fn/code → 404
|
||||
PASS: Bob PUT t-alice-fn/code → 404 (HTTP 404)
|
||||
|
||||
>>> Bob INVOKE t-alice-fn → 404
|
||||
PASS: Bob INVOKE t-alice-fn → 404 (HTTP 404)
|
||||
|
||||
>>> Alice GET t-alice-fn → 200 (не повреждена)
|
||||
PASS: Alice GET t-alice-fn → 200 (HTTP 200)
|
||||
|
||||
>>> LIST изоляция: Alice и Bob видят только свои функции
|
||||
PASS: Alice LIST содержит t-alice-fn
|
||||
PASS: Alice LIST не содержит t-bob-fn
|
||||
PASS: Bob LIST содержит t-bob-fn
|
||||
PASS: Bob LIST не содержит t-alice-fn
|
||||
|
||||
>>> Route изоляция: одинаковое имя → разные routes
|
||||
PASS: Route изоляция: /90bae4a0946a/t-alice-fn ≠ /7244c3bb8313/t-bob-fn
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=11 FAIL=0
|
||||
========================================
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
========================================
|
||||
test_stress.sh run=7c90f8d1 users=8 invoke_parallel=8
|
||||
========================================
|
||||
|
||||
>>> [1/3] 8 пользователей создают функции параллельно...
|
||||
PASS: Параллельный CREATE: все 8 пользователей → 201
|
||||
|
||||
>>> [2/3] Изоляция: каждый видит только свою функцию
|
||||
PASS: Изоляция: все 8 пользователей видят только свои функции
|
||||
|
||||
>>> [3/3] 8 параллельных invoke для livetest@test.local
|
||||
Ждём прогрева 8 функций...
|
||||
функция 1/8 — готова
|
||||
функция 2/8 — готова
|
||||
функция 3/8 — готова
|
||||
функция 4/8 — готова
|
||||
функция 5/8 — готова
|
||||
функция 6/8 — готова
|
||||
функция 7/8 — готова
|
||||
функция 8/8 — готова
|
||||
PASS: Параллельный INVOKE (livetest): 8/8 OK
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=3 FAIL=0
|
||||
========================================
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
========================================
|
||||
test_update.sh run=8950afbb
|
||||
========================================
|
||||
|
||||
>>> CREATE: версия v1
|
||||
PASS: CREATE v1 → 201 (HTTP 201)
|
||||
Ждём прогрева...
|
||||
TRY=1 fn=t-upd-fn-8950afbb status=200 raw=version-one err=
|
||||
PASS: Invoke v1 → version-one
|
||||
|
||||
>>> PUT /code: версия v2
|
||||
PASS: PUT /code v2 → 200 (HTTP 200)
|
||||
PASS: PUT /code: updated=true
|
||||
PASS: PUT /code: новый package=t-upd-fn-8950afbb-pkg-mopyyge1
|
||||
Ждём пока executor подхватит новый код...
|
||||
TRY=1 fn=t-upd-fn-8950afbb status=200 raw=version-two err=
|
||||
PASS: Invoke после update → version-two
|
||||
|
||||
>>> PUT /code: пустой код → 400
|
||||
PASS: PUT /code пустой код → 400 (HTTP 400)
|
||||
PASS: PUT /code пробельный код → 400 (HTTP 400)
|
||||
|
||||
>>> PUT /code: несуществующая функция → 404
|
||||
PASS: PUT /code несуществующая → 404 (HTTP 404)
|
||||
|
||||
>>> PUT /code: версия v3 (цепочка)
|
||||
TRY=1 fn=t-upd-fn-8950afbb status=200 raw=version-three err=
|
||||
PASS: Invoke после v3 update → version-three
|
||||
|
||||
========================================
|
||||
ИТОГ: PASS=10 FAIL=0
|
||||
========================================
|
||||
@@ -1,3 +1,12 @@
|
||||
ntazetdinov@nubes.ru
|
||||
|
||||
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWQ3NmM1LTQyNGMtN2MyYy04OWI3LWUzZjA5OTQ5YTk4ZiIsImV4cCI6MTc5MzA3ODQzOSwiaWF0IjoxNzc3NTI2NDM5LCJqdGkiOiI4MTU1ODNkNy0xZTRkLTRiMmMtOGM5ZS0wZDZmMGU2NmJjNTciLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiJXWjAxMTEyIiwiY29tcGFueV9pZCI6IjhlYzcwYWMwLTU0NmQtNDJhNy04Y2ZmLTMzOWM4ZmI1MWEyMyIsImNvbXBhbnlfbmFtZSI6ItCe0J7QniDCq9Cd0KPQkdCV0KHCuyIsImlkcF91c3JfdWlkIjoiMDE5ZDc2YzUtNDI0Yy03YzJjLTg5YjctZTNmMDk5NDlhOThmIiwibG9naW4iOiJudGF6ZXRkaW5vdkBudWJlcy5ydSIsImZpcnN0bmFtZSI6ItCd0LDQuNC70YwiLCJtaWRkbGVuYW1lIjoi0KTQsNGA0LjRgtC-0LLQuNGHIiwibGFzdG5hbWUiOiLQotCw0LfQtdGC0LTQuNC90L7QsiIsImdyb3VwcyI6bnVsbCwicHJlZmVycmVkX3VzZXJuYW1lIjoiIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIiwiZW1haWwiOiJudGF6ZXRkaW5vdkBudWJlcy5ydSJ9.SaZsKAha45-fvKuJkLHUe_09AFsbH5QpzBVdPnjiEDQGIhl1A3ThgnM-oEh_H6CBXXfnOs2QDczzgCD8K8_tYzZKU1Wgk5lj04YW_fbTI89kHTO5wLtrAht9tFEjzBQZ-kmnG8mUK5tgqyNEjsngdQcfqVWRvneF366TiiRXk_76poUPpXWQmqdgCCKb3wq1rRgWcKDZcUC3JvDpOaqYL40zITZlM855drlJhMut4Gkg-EEk7ZGykl6YCsTRWVjFKGQ-7C-BDU2dkmIWhxzBAkvzzM2kBwvtlczCEDiUin7bXE-F_io89oJUYDzY4yPeHzwREwxCp6x9eD1NAIKKuA
|
||||
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWNjMjY4LTZjNmEtNzgxZS04NjEzLTRiZWQ0ZWM3Y2QyMCIsImV4cCI6MTc4OTAxNTg2NywiaWF0IjoxNzczNDYzODY3LCJqdGkiOiJmYjRkMGFiNy1lYTI1LTQ5YmItYmQ3OC0zYjBiNmRiZTdhNzkiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJjb21wYW55X2lkIjoiIiwiZ3JvdXBzIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiIiLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIiLCJlbWFpbCI6InRhemV0ZGlub3ZuQGdtYWlsLmNvbSJ9.xPxWUWA8e_GEUQ8bgpuHOZvoY7Xu2udRtGPrpHZrzP3Z5uhX-NqDvVDDZQRSpuqoAeXjmekTxX0kHFvwEmv-Kd1hnQZJA_nJf3DvSVPYErtu4ePgy4U2N-4uwlMvjtRysxv17SMEPaDP4XAKj6SxDZ8eEtRCnrTJpgxwh-2cz1NRUsH99pIBY0doj7yNkPOQnnrOCQpnzWdX-d1I1ERCwGg10qOa5VpxP3yXoc2PuDMF6gFU_1VxlhCEdL94TTlFQDPdaBmaDk6hDwUq6T7RZk4cRQ8eao10tUWLHucfaMBGtKEutgrbqR8kOmMB6wY3x86TXdZfBS0zr7RO137d-g
|
||||
|
||||
ntazetdinov@nubes.ru
|
||||
|
||||
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWQ3NmM1LTQyNGMtN2MyYy04OWI3LWUzZjA5OTQ5YTk4ZiIsImV4cCI6MTc5MzA3ODQzOSwiaWF0IjoxNzc3NTI2NDM5LCJqdGkiOiI4MTU1ODNkNy0xZTRkLTRiMmMtOGM5ZS0wZDZmMGU2NmJjNTciLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiJXWjAxMTEyIiwiY29tcGFueV9pZCI6IjhlYzcwYWMwLTU0NmQtNDJhNy04Y2ZmLTMzOWM4ZmI1MWEyMyIsImNvbXBhbnlfbmFtZSI6ItCe0J7QniDCq9Cd0KPQkdCV0KHCuyIsImlkcF91c3JfdWlkIjoiMDE5ZDc2YzUtNDI0Yy03YzJjLTg5YjctZTNmMDk5NDlhOThmIiwibG9naW4iOiJudGF6ZXRkaW5vdkBudWJlcy5ydSIsImZpcnN0bmFtZSI6ItCd0LDQuNC70YwiLCJtaWRkbGVuYW1lIjoi0KTQsNGA0LjRgtC-0LLQuNGHIiwibGFzdG5hbWUiOiLQotCw0LfQtdGC0LTQuNC90L7QsiIsImdyb3VwcyI6bnVsbCwicHJlZmVycmVkX3VzZXJuYW1lIjoiIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIiwiZW1haWwiOiJudGF6ZXRkaW5vdkBudWJlcy5ydSJ9.SaZsKAha45-fvKuJkLHUe_09AFsbH5QpzBVdPnjiEDQGIhl1A3ThgnM-oEh_H6CBXXfnOs2QDczzgCD8K8_tYzZKU1Wgk5lj04YW_fbTI89kHTO5wLtrAht9tFEjzBQZ-kmnG8mUK5tgqyNEjsngdQcfqVWRvneF366TiiRXk_76poUPpXWQmqdgCCKb3wq1rRgWcKDZcUC3JvDpOaqYL40zITZlM855drlJhMut4Gkg-EEk7ZGykl6YCsTRWVjFKGQ-7C-BDU2dkmIWhxzBAkvzzM2kBwvtlczCEDiUin7bXE-F_io89oJUYDzY4yPeHzwREwxCp6x9eD1NAIKKuA
|
||||
|
||||
------------------------------
|
||||
=======
|
||||
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWQ3NmM1LTQyNGMtN2MyYy04OWI3LWUzZjA5OTQ5YTk4ZiIsImV4cCI6MTc5MzA3ODQzOSwiaWF0IjoxNzc3NTI2NDM5LCJqdGkiOiI4MTU1ODNkNy0xZTRkLTRiMmMtOGM5ZS0wZDZmMGU2NmJjNTciLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiJXWjAxMTEyIiwiY29tcGFueV9pZCI6IjhlYzcwYWMwLTU0NmQtNDJhNy04Y2ZmLTMzOWM4ZmI1MWEyMyIsImNvbXBhbnlfbmFtZSI6ItCe0J7QniDCq9Cd0KPQkdCV0KHCuyIsImlkcF91c3JfdWlkIjoiMDE5ZDc2YzUtNDI0Yy03YzJjLTg5YjctZTNmMDk5NDlhOThmIiwibG9naW4iOiJudGF6ZXRkaW5vdkBudWJlcy5ydSIsImZpcnN0bmFtZSI6ItCd0LDQuNC70YwiLCJtaWRkbGVuYW1lIjoi0KTQsNGA0LjRgtC-0LLQuNGHIiwibGFzdG5hbWUiOiLQotCw0LfQtdGC0LTQuNC90L7QsiIsImdyb3VwcyI6bnVsbCwicHJlZmVycmVkX3VzZXJuYW1lIjoiIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIiwiZW1haWwiOiJudGF6ZXRkaW5vdkBudWJlcy5ydSJ9.SaZsKAha45-fvKuJkLHUe_09AFsbH5QpzBVdPnjiEDQGIhl1A3ThgnM-oEh_H6CBXXfnOs2QDczzgCD8K8_tYzZKU1Wgk5lj04YW_fbTI89kHTO5wLtrAht9tFEjzBQZ-kmnG8mUK5tgqyNEjsngdQcfqVWRvneF366TiiRXk_76poUPpXWQmqdgCCKb3wq1rRgWcKDZcUC3JvDpOaqYL40zITZlM855drlJhMut4Gkg-EEk7ZGykl6YCsTRWVjFKGQ-7C-BDU2dkmIWhxzBAkvzzM2kBwvtlczCEDiUin7bXE-F_io89oJUYDzY4yPeHzwREwxCp6x9eD1NAIKKuA
|
||||
>>>>>>> d21e1ac (feat: restore cron page, email in navbar, metrics pipeline v1.3.19)
|
||||
|
||||
Reference in New Issue
Block a user