Compare commits

...
13 Commits
Author SHA1 Message Date
“Naeel” 4ecaa508ea ui: progressive table render + load timer, v1.3.34 2026-05-03 17:34:07 +04:00
“Naeel” f85027e986 deploy: v1.3.33 — fix version label, new tag to force image pull 2026-05-03 17:24:18 +04:00
“Naeel” ad215a2cab ui: fix version label v1.3.27 → v1.3.32 2026-05-03 17:18:37 +04:00
“Naeel” e4efaca6a9 deploy: bump fission-console to v1.3.32 2026-05-03 17:16:21 +04:00
“Naeel” e9853a4bc0 ui: archive upload mode + lint stub, fix stress warmup output, add descriptions 2026-05-03 17:14:25 +04:00
“Naeel” 2af39ff3e2 merge: взять нашу версию run_all.sh с авто-tee 2026-05-03 16:39:49 +04:00
“Naeel” bf9ada20a2 tests: добавить все тест-скрипты локально 2026-05-03 16:39:35 +04:00
“Naeel” 8211485501 tests: run_all.sh авто-tee в test-results/, правила логирования 2026-05-03 16:38:05 +04:00
Naeel c1e3039d15 tests: тест-скрипты v2 — полный сьют PASS=8 FAIL=0
- lib.sh: утилиты (json_field, wait_invoke, print_summary)
- run_all.sh: запуск всех скриптов + финальная сводка
- pre_cleanup.sh: очистка тест-namespace-ов перед прогоном
- test_auth.sh: 14/14 PASS — авторизация, namespace детерминизм
- test_functions_crud.sh: 29/29 PASS — CRUD, валидация, >1MB
- test_ttl.sh: 10/10 PASS — TTL=15m/1h/2d, невалидные
- test_isolation.sh: 11/11 PASS — изоляция пользователей
- test_update.sh: 10/10 PASS — PUT /code v1→v2→v3 (livetest)
- test_invoke.sh: 9/9 PASS — NodeJS/Python invoke (livetest)
- test_stress.sh: 3/3 PASS — 8 пользователей параллельно (livetest)
- test_linters.sh: 31/31 PASS — синтаксис 5 языков
2026-05-03 14:02:24 +03:00
“Naeel” f18508d2cb fix(tests): X-Test-Sub → X-Auth-Token во всех тест-скриптах, cleanup для test59.sh 2026-05-03 13:50:32 +04:00
“Naeel” a09291e208 feat: remove test mode (X-Test-Sub), all functions stored in S3 via storagesvc
- Remove FISSION_TEST_MODE and X-Test-Sub bypass from authMiddleware
- Remove testMode field from Server/Config structs
- Remove TestAuthenticator branch from main.go
- Add uploadToStoragesvc/buildDeploySpec: all function code uploaded to S3
- Add deleteFromStoragesvc: archive deleted from S3 on function delete
- Add FISSION_STORAGESVC_URL env to main.go and deploy/console.yaml
- image: naeel/fission-console:v1.3.31
2026-05-03 13:37:12 +04:00
“Naeel” e8f06d4ab2 move scripts, update UI, add s3cfg-ngcloud 2026-05-03 08:27:38 +04:00
“Naeel” 3b369f8abf move scripts to scripts/ folder 2026-05-03 08:26:30 +04:00
35 changed files with 1883 additions and 3740 deletions
+27
View File
@@ -60,6 +60,33 @@ 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/
```
**Никогда не разбираться с результатами по памяти / буферу / чату. Только лог.**
## Поведение агента
- Не трогать рабочий код без явного указания
-1106
View File
File diff suppressed because it is too large Load Diff
+2 -10
View File
@@ -37,16 +37,8 @@ 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: jwtAuth,
JWT: auth.NewDeckAuthenticator(auth.DefaultDeckAPIs, nil),
Demo: &auth.DemoAuthenticator{},
}
@@ -59,7 +51,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", ""),
TestMode: testMode,
StoragesvcURL: envDefault("FISSION_STORAGESVC_URL", "http://storagesvc.fission.svc.cluster.local"),
Authenticator: authenticator,
// --- ai/ask feature ---
LLMUrl: envDefault("FISSION_LLM_URL", "https://api.aillm.ru"),
+3 -1
View File
@@ -52,7 +52,7 @@ spec:
serviceAccountName: fission-console
containers:
- name: console
image: naeel/fission-console:v1.3.26
image: naeel/fission-console:v1.3.34
imagePullPolicy: Always
ports:
- containerPort: 8090
@@ -77,6 +77,8 @@ spec:
secretKeyRef:
name: router
key: password
- name: FISSION_STORAGESVC_URL
value: "http://storagesvc.fission.svc.cluster.local"
livenessProbe:
httpGet:
path: /health
+1 -16
View File
@@ -50,28 +50,13 @@ func normalizeEnv(env string) string {
}
// authMiddleware оборачивает handler аутентификацией.
//
// В testMode: заголовок X-Test-Sub позволяет задать sub напрямую (без токена).
// Иначе: токен передаётся в s.authenticator.Authenticate — детали скрыты за интерфейсом.
// Токен передаётся в 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)
+128 -8
View File
@@ -11,6 +11,7 @@ import (
"log"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
@@ -51,6 +52,102 @@ 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")
@@ -313,7 +410,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 literal с кодом напрямую
// - Остальные: deployment archive с кодом (S3 или literal fallback)
var pkgSpec map[string]any
if req.Language == "go" {
srcZip, err := runtime.BuildGoSourceZip(req.Code)
@@ -321,12 +418,13 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("build go source archive: %v", err))
return
}
literal := base64.StdEncoding.EncodeToString(srcZip)
srcSpec, srcErr := s.buildDeploySpec(ctx, srcZip)
if srcErr != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("upload go source: %v", srcErr))
return
}
pkgSpec = map[string]any{
"source": map[string]any{
"type": "literal",
"literal": literal,
},
"source": srcSpec,
"deployment": map[string]any{},
"environment": map[string]any{"name": req.Environment, "namespace": ns},
"buildcommand": "build",
@@ -337,8 +435,13 @@ 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": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
"deployment": deploySpec,
"environment": map[string]any{"name": req.Environment, "namespace": ns},
"source": map[string]any{},
}
@@ -563,12 +666,17 @@ 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": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
"deployment": deploySpec,
"environment": map[string]any{"name": envName, "namespace": ns},
"source": map[string]any{},
},
@@ -1000,10 +1108,22 @@ 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)
+7 -5
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
@@ -36,10 +37,10 @@ type Server struct {
saTokenPath string
invokeTimeout time.Duration
testMode bool // FISSION_TEST_MODE=true — разрешает X-Test-Sub shortcut
authUser string
authPass string
authUser string
authPass string
storagesvcURL string // URL Fission storagesvc для загрузки архивов кода в S3
// --- ai/ask feature (удалить блок целиком чтобы выкосить) ---
llmURL string // FISSION_LLM_URL
@@ -68,7 +69,7 @@ type Config struct {
SATokenPath string
AuthUser string
AuthPass string
TestMode bool
StoragesvcURL string
Authenticator auth.Authenticator // слой аутентификации
LLMUrl string
LLMKey string
@@ -76,6 +77,7 @@ 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,
@@ -85,7 +87,7 @@ func NewServer(cfg Config) *Server {
invokeTimeout: cfg.InvokeTimeout,
authUser: cfg.AuthUser,
authPass: cfg.AuthPass,
testMode: cfg.TestMode,
storagesvcURL: cfg.StoragesvcURL,
authenticator: cfg.Authenticator,
llmURL: cfg.LLMUrl,
llmKey: cfg.LLMKey,
+49 -32
View File
@@ -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.26</div>
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.34</div>
</div>
<div class="row" style="margin:0;">
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
@@ -209,17 +209,27 @@
</div>
</div>
<div>
<label>Код</label>
<textarea id="c-code">def main():
<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():
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')">&#x1F50D; Проверить
синтаксис линтером</button>
<button class="btn ghost" id="c-gen-btn" onclick="showGenPrompt()">&#x2728; Сгенерировать код LLM</button>
<button class="btn ghost" id="c-exp-btn" onclick="aiExplain('c-code','c-lang','c-ai-result')">&#x1F4D6; LLM:
Что
делает?</button>
<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')">&#x1F50D; Проверить
синтаксис линтером</button>
<button class="btn ghost" id="c-gen-btn" onclick="showGenPrompt()">&#x2728; Сгенерировать код LLM</button>
<button class="btn ghost" id="c-exp-btn" onclick="aiExplain('c-code','c-lang','c-ai-result')">&#x1F4D6; 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 class="btn ghost" onclick="lintArchiveFile('c')">&#x1F50D; Проверить архив линтером</button>
</div>
</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"
@@ -279,14 +289,25 @@
</div>
</div>
<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')">&#x1F50D;
Проверить синтаксис линтером</button>
<button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">&#x1F4D6;
LLM:
Что делает?</button>
<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')">&#x1F50D;
Проверить синтаксис линтером</button>
<button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">&#x1F4D6;
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 class="btn ghost" onclick="lintArchiveFile('e')">&#x1F50D; Проверить архив линтером</button>
</div>
</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;">
@@ -335,21 +356,19 @@
<div class="help-section">
<h4>Как вызвать функцию вне веб-интерфейса</h4>
<div class="help-list">
<div>Обычный пользователь вызывает функцию через HTTP gateway console: <span class="mono">/fn/...</span>.
</div>
<div>Используется только ваш токен из личного кабинета в заголовке <span class="mono">Authorization: Bearer
&lt;token&gt;</span>.</div>
<div>Функции доступны через <span class="mono">/fn/...</span> с вашим токеном или демо-логином в заголовке <span class="mono">Authorization: Bearer</span>.</div>
<div>Gateway может возвращать редирект — используйте флаг <span class="mono">-L</span> в curl для его автоматического следования.</div>
<div>Работают обычные HTTP методы: <span class="mono">GET</span>, <span class="mono">POST</span>, <span
class="mono">PUT</span>, <span class="mono">DELETE</span> и другие, если они разрешены в поле Методы.
</div>
</div>
<div class="help-code">export TOKEN="&lt;ваш пользовательский токен&gt;"
curl -sk -X POST https://fission.kube5s.ru/fn/ВАШ_ROUTE \
<div class="help-code">export TOKEN="&lt;токен или демо-логин&gt;"
curl -skL -X POST https://fission.kube5s.ru/fn/ВАШ_ROUTE \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"x":1,"y":2}'</div>
<div class="help-code">export TOKEN="&lt;ваш пользовательский токен&gt;"
curl -sk https://fission.kube5s.ru/fn/ВАШ_ROUTE \
<div class="help-code">export TOKEN="&lt;токен или демо-логин&gt;"
curl -skL https://fission.kube5s.ru/fn/ВАШ_ROUTE \
-H "Authorization: Bearer $TOKEN"</div>
<div class="help-code">import requests
@@ -370,18 +389,16 @@
<div class="help-section">
<h4>Где взять токен</h4>
<div class="help-list">
<div><strong>Для обычного пользователя</strong> — используйте ваш пользовательский токен из личного кабинета:
Профиль пользователя → Токены.</div>
<div>Тот же пользовательский токен подходит и для входа в console, и для внешнего вызова через <span
class="mono">/fn/...</span>.</div>
<div><strong>Токен из ЛК</strong> — Профиль пользователя → Токены. Подходит и для входа в console, и для вызова через <span class="mono">/fn/</span>.</div>
<div><strong>Демо-логин</strong> — любая строка не менее 6 символов. Тот же логин даёт тот же namespace — ваше окружение сохраняется.</div>
<div>Токен удобно класть в переменную окружения и потом подставлять в curl как <span
class="mono">$TOKEN</span>.</div>
</div>
<div class="help-code">export TOKEN="&lt;вставьте сюда токен из личного кабинета&gt;"</div>
<div class="help-code">export TOKEN="&lt;вставьте сюда токен или демо-логин&gt;"</div>
</div>
<div class="actions" style="justify-content:space-between; align-items:center;">
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.26</span>
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.34</span>
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
</div>
</div>
+30 -5
View File
@@ -1,6 +1,12 @@
/* 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'),
@@ -23,7 +29,7 @@ async function reloadAll() {
return t && t.spec && t.spec.functionref && t.spec.functionref.name;
}).filter(Boolean)).size);
const rows = (fns || []).map(function (f) {
function makeFnRow(f) {
const spec = f.spec || {};
const meta = f.metadata || {};
const ann = meta.annotations || {};
@@ -57,14 +63,33 @@ async function reloadAll() {
'<td>' + cronCell + '</td>' +
'<td class="nowrap">' + actions + '</td>' +
'</tr>';
}).join('');
}
document.getElementById('fn-rows').innerHTML = rows || '<tr><td colspan="9">Нет функций</td></tr>';
if (!rows) {
var fnList = fns || [];
if (fnList.length === 0) {
progress.stop('Загружено: нет функций', '');
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>';
showStatus('Ошибка загрузки: ' + e.message, 'err');
progress.stop('Ошибка загрузки: ' + e.message, 'err');
}
}
+28
View File
@@ -1,5 +1,33 @@
/* 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 — заглушка линтера для архивов.
// Реализация (распаковка + пофайловая проверка) добавляется позже.
function lintArchiveFile(prefix) {
var input = document.getElementById(prefix + '-archive-file');
if (!input || !input.files || !input.files[0]) {
showStatus('Выберите .zip файл', 'err');
return;
}
showStatus('Линтинг архива — пока не реализован (скоро)', 'info');
}
function parseMethods(v) {
const items = String(v || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
return items.length ? Array.from(new Set(items)) : ['GET'];
+162
View File
@@ -1,5 +1,167 @@
# Логика авторизации — 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`):
+180
View File
@@ -0,0 +1,180 @@
# Создание функции — текущий 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` → линтер,
вернуть предупреждение если нашёл ошибки.
Так пользователь видит и код и результат линтера до деплоя, и полный архив уходит без изменений.
+66
View File
@@ -0,0 +1,66 @@
# Загрузка страницы — 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()` | Таймер в статус-баре |
+7
View File
@@ -0,0 +1,7 @@
[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
Executable
+91
View File
@@ -0,0 +1,91 @@
#!/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 ]
}
+38
View File
@@ -0,0 +1,38 @@
#!/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: очистка завершена."
+113
View File
@@ -0,0 +1,113 @@
#!/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 ]
+26
View File
@@ -0,0 +1,26 @@
#!/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
+107
View File
@@ -0,0 +1,107 @@
#!/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="auth-test-${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
+207
View File
@@ -0,0 +1,207 @@
#!/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="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\":\"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" = "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\":\"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/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/crud-fn-${RUN_ID} → 200"
GOT_NAME=$(json_field "$BODY" name)
[ "$GOT_NAME" = "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/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/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/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
+90
View File
@@ -0,0 +1,90 @@
#!/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="node-fn-${RUN_ID}"
FN_PY="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
+162
View File
@@ -0,0 +1,162 @@
#!/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="alice-${RUN_ID}@test.local"
BOB="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\":\"alice-fn\",\"language\":\"nodejs\",\"code\":$(printf '%s' "$JS_CODE" | json_escape)}")
check_http "$(echo "$R" | tail -1)" "201" "Alice CREATE alice-fn → 201"
# ── Bob не может получить функцию Alice ──────────────────────────────────────
echo ""
echo ">>> Bob GET alice-fn → 404"
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/alice-fn" \
-H "X-Auth-Token: ${BOB}")
check_http "$(echo "$R" | tail -1)" "404" "Bob GET alice-fn → 404"
echo ""
echo ">>> Bob DELETE alice-fn → 404"
R=$(curl -s -w "\n%{http_code}" -X DELETE "${BASE}/functions/alice-fn" \
-H "X-Auth-Token: ${BOB}")
check_http "$(echo "$R" | tail -1)" "404" "Bob DELETE alice-fn → 404"
echo ""
echo ">>> Bob PUT alice-fn/code → 404"
R=$(curl -s -w "\n%{http_code}" -X PUT "${BASE}/functions/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 alice-fn/code → 404"
echo ""
echo ">>> Bob INVOKE alice-fn → 404"
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions/alice-fn/invoke" \
-H "X-Auth-Token: ${BOB}" -H "Content-Type: application/json" -d '{}')
check_http "$(echo "$R" | tail -1)" "404" "Bob INVOKE alice-fn → 404"
# ── Alice убеждается что её функция цела ────────────────────────────────────
echo ""
echo ">>> Alice GET alice-fn → 200 (не повреждена)"
R=$(curl -s -w "\n%{http_code}" "${BASE}/functions/alice-fn" \
-H "X-Auth-Token: ${ALICE}")
check_http "$(echo "$R" | tail -1)" "200" "Alice GET 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\":\"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 видит alice-fn но не 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','') == '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','') == 'bob-fn'))
except:
print(0)" 2>/dev/null)
[ "${ALICE_HAS_OWN:-0}" -ge 1 ] && pass "Alice LIST содержит alice-fn" || fail "Alice LIST не содержит alice-fn (count=$ALICE_COUNT)"
[ "${ALICE_HAS_BOB:-0}" -eq 0 ] && pass "Alice LIST не содержит bob-fn" || fail "Alice LIST содержит bob-fn — УТЕЧКА ДАННЫХ!"
# Bob видит bob-fn но не 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','') == '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','') == 'alice-fn'))
except:
print(0)" 2>/dev/null)
[ "${BOB_HAS_OWN:-0}" -ge 1 ] && pass "Bob LIST содержит bob-fn" || fail "Bob LIST не содержит bob-fn"
[ "${BOB_HAS_ALICE:-0}" -eq 0 ] && pass "Bob LIST не содержит alice-fn" || fail "Bob LIST содержит 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','') == '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','') == 'bob-fn':
print(f.get('spec',{}).get('relativeurl',''))
break
except:
print('')" 2>/dev/null)
# Проверяем через GET (возвращает route)
ALICE_GET=$(curl -s "${BASE}/functions/alice-fn" -H "X-Auth-Token: ${ALICE}")
BOB_GET=$(curl -s "${BASE}/functions/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
+1 -1
View File
@@ -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-Test-Sub: livetest@test.local" \
-H "X-Auth-Token: livetest@test.local" \
-H "Content-Type: application/json" \
-d "{\"language\":\"${lang}\",\"code\":${escaped}}")
local ok_val result_val
+160
View File
@@ -0,0 +1,160 @@
#!/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+=("stress-${RUN_ID}-u${i}@test.local")
done
declare -a INVOKE_FNS=()
for i in $(seq 1 "$INVOKE_COUNT"); do
INVOKE_FNS+=("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
+93
View File
@@ -0,0 +1,93 @@
#!/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="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\":\"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\":\"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
+105
View File
@@ -0,0 +1,105 @@
#!/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="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
-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BASE="https://fission.kube5s.ru/console/api"
COUNT=${1:-10}
RUN_ID="mt$(date +%s | tail -c 6)"
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
TMP=$(mktemp -d)
declare -a USERS=()
cleanup() {
rm -rf "$TMP" 2>/dev/null || true
if [ -x "$NS_CLEANUP_SCRIPT" ] && [ ${#USERS[@]} -gt 0 ]; then
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "${USERS[@]}" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
json_escape() {
python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
}
init_call() {
local user="$1"
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-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"
return 0
fi
[ "$try" -lt 3 ] && sleep 2
done
printf '%s' "$code"
}
invoke_call() {
local user="$1"
local fn="$2"
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-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"
return 0
fi
[ "$try" -lt 4 ] && sleep 4
done
printf '%s' "$code"
}
echo "=== MULTIUSER TEST START run=$RUN_ID count=$COUNT ==="
for i in $(seq 1 "$COUNT"); do
USERS+=("${RUN_ID}-u${i}@test.local")
(
U="${RUN_ID}-u${i}@test.local"
CODE=$(init_call "${U}")
echo "$i:$CODE:$U" > "${TMP}/init-$i"
) &
done
wait
INIT_OK=0
for i in $(seq 1 "$COUNT"); do
R=$(cat "${TMP}/init-$i" 2>/dev/null || echo "$i:000:")
C=$(echo "$R" | cut -d: -f2)
if [ "$C" = "200" ]; then
INIT_OK=$((INIT_OK+1))
else
echo "INIT_FAIL user$i http=$C"
fi
done
echo "INIT_OK=$INIT_OK/$COUNT"
for i in $(seq 1 "$COUNT"); do
(
U="${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-Test-Sub: ${U}" -H "Content-Type: application/json" -d "$BODY" 2>/dev/null || echo 000)
echo "$i:$C:$FN:$U" > "${TMP}/create-$i"
) &
done
wait
CREATE_OK=0
for i in $(seq 1 "$COUNT"); do
R=$(cat "${TMP}/create-$i" 2>/dev/null || echo "$i:000::")
C=$(echo "$R" | cut -d: -f2)
if [ "$C" = "200" ] || [ "$C" = "201" ]; then
CREATE_OK=$((CREATE_OK+1))
else
echo "CREATE_FAIL user$i http=$C"
fi
done
echo "CREATE_OK=$CREATE_OK/$COUNT"
sleep 8
for i in $(seq 1 "$COUNT"); do
(
U="${RUN_ID}-u${i}@test.local"
FN="mtfn-${RUN_ID}-${i}"
C=$(invoke_call "${U}" "${FN}")
echo "$i:$C" > "${TMP}/invoke-$i"
) &
done
wait
INVOKE_OK=0
for i in $(seq 1 "$COUNT"); do
R=$(cat "${TMP}/invoke-$i" 2>/dev/null || echo "$i:000")
C=$(echo "$R" | cut -d: -f2)
if [ "$C" = "200" ]; then
INVOKE_OK=$((INVOKE_OK+1))
else
echo "INVOKE_FAIL user$i http=$C"
fi
done
echo "INVOKE_OK=$INVOKE_OK/$COUNT"
echo "=== MULTIUSER TEST END ==="
[ "$INIT_OK" = "$COUNT" ]
[ "$CREATE_OK" = "$COUNT" ]
[ "$INVOKE_OK" = "$COUNT" ]
-119
View File
@@ -1,119 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BASE="https://fission.kube5s.ru/console/api"
COUNT=${1:-5}
RUN_ID="journey$(date +%s | tail -c 6)"
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
TMP=$(mktemp -d)
declare -a USERS=()
cleanup() {
rm -rf "$TMP" 2>/dev/null || true
if [ -x "$NS_CLEANUP_SCRIPT" ] && [ ${#USERS[@]} -gt 0 ]; then
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "${USERS[@]}" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
json_field() {
local payload="$1"
local key="$2"
printf '%s' "$payload" | python3 -c 'import json,sys
key=sys.argv[1]
try:
data=json.load(sys.stdin)
value=data.get(key, "")
print(value if not isinstance(value, (dict,list)) else json.dumps(value))
except Exception:
print("")' "$key"
}
json_escape() {
python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
}
create_call() {
local sub="$1"
local body="$2"
curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \
-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-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")
(
U="${RUN_ID}-u${i}@test.local"
FN="journey-${RUN_ID}-${i}"
RESULT="PASS"
AUTH=$(curl -s -w "\n%{http_code}" --max-time 45 -X POST "${BASE}/auth" -H 'Content-Type: application/json' -d "{\"token\":\"${U}\",\"env\":\"test\"}")
AUTH_CODE=$(printf '%s' "$AUTH" | tail -1)
[ "$AUTH_CODE" = "200" ] || RESULT="FAIL auth=${AUTH_CODE}"
BAD=$(create_call "$U" '{"name":"bad-lang","language":"cobol","code":"x"}')
BAD_CODE=$(printf '%s' "$BAD" | tail -1)
[ "$BAD_CODE" = "400" ] || RESULT="FAIL bad_create=${BAD_CODE}"
CODE_TEXT=$(printf 'def main():\n return "journey-ok-%s-%s"\n' "$RUN_ID" "$i")
CODE_JSON=$(printf '%s' "$CODE_TEXT" | json_escape)
GOOD=$(create_call "$U" "{\"name\":\"${FN}\",\"language\":\"python\",\"code\":${CODE_JSON}}")
GOOD_CODE=$(printf '%s' "$GOOD" | tail -1)
[ "$GOOD_CODE" = "201" ] || RESULT="FAIL create=${GOOD_CODE}"
DUP=$(create_call "$U" "{\"name\":\"${FN}\",\"language\":\"python\",\"code\":${CODE_JSON}}")
DUP_CODE=$(printf '%s' "$DUP" | tail -1)
{ [ "$DUP_CODE" = "409" ] || [ "$DUP_CODE" = "502" ]; } || RESULT="FAIL dup=${DUP_CODE}"
INV_STATUS=""
INV_RAW=""
for try in 1 2 3 4 5 6; do
INV=$(invoke_call "$U" "$FN")
INV_STATUS=$(json_field "$INV" status)
INV_RAW=$(json_field "$INV" response_raw)
echo "USER=$i TRY=$try status=$INV_STATUS raw=$INV_RAW"
if [ "$INV_STATUS" = "200" ] && printf '%s' "$INV_RAW" | grep -q "journey-ok-${RUN_ID}-${i}"; then
break
fi
[ "$try" -lt 6 ] && sleep 5
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-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-Test-Sub: ${U}")
[ "$DEL2" = "404" ] || RESULT="FAIL delete2=${DEL2}"
echo "$i:$RESULT" > "${TMP}/user-$i"
) &
done
wait
PASS=0
FAIL=0
for i in $(seq 1 "$COUNT"); do
R=$(cat "${TMP}/user-$i" 2>/dev/null || echo "$i:FAIL missing_result")
STATUS=$(printf '%s' "$R" | cut -d: -f2-)
if [ "$STATUS" = "PASS" ]; then
PASS=$((PASS+1))
echo "USER_PASS $i"
else
FAIL=$((FAIL+1))
echo "USER_FAIL $i $STATUS"
fi
done
echo "USER_JOURNEYS_PASS=$PASS/$COUNT"
echo "=== USER JOURNEYS END ==="
[ "$FAIL" = 0 ]
-10
View File
@@ -1,10 +0,0 @@
#!/bin/bash
BASE="https://fission.kube5s.ru/console/api"
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-Test-Sub: len59direct@test.local" \
-H "Content-Type: application/json" \
--data-raw "{\"name\":\"${NAME59}\",\"language\":\"nodejs\",\"code\":\"module.exports=async()=>42\"}"
echo
-158
View File
@@ -1,158 +0,0 @@
#!/usr/bin/env bash
# test_layer1.sh — простой тест пропатченного Fission NSWatcher (Layer 1 only).
# Без консоли. Только kubectl + fission CLI.
#
# Что проверяем:
# 1. Создаём NS с меткой fission.io/managed=true
# 2. Executor регистрирует его через NSWatcher (лог)
# 3. Создаём Python env + function в новом NS
# 4. Вызываем функцию — должна ответить 200
#
# Запуск: bash ~/terra/fission/test_layer1.sh
set -uo pipefail
NS="l1-test-$(date +%s | tail -c 6)"
PASS=0; FAIL=0
green() { echo -e "\033[32m PASS: $1\033[0m"; PASS=$((PASS+1)); }
red() { echo -e "\033[31m FAIL: $1\033[0m"; FAIL=$((FAIL+1)); }
echo ""
echo "========================================"
echo " Layer 1 NSWatcher — простой тест"
echo " NS: $NS"
echo "========================================"
# ── 1. Создаём NS с меткой ────────────────────────────────────────────────────
echo ""
echo ">>> [1/5] Создаём NS $NS с меткой fission.io/managed=true..."
kubectl create ns "$NS"
kubectl label ns "$NS" fission.io/managed=true --overwrite
kubectl get ns "$NS" --show-labels | grep "fission.io/managed"
if [ $? -eq 0 ]; then
green "NS создан с меткой"
else
red "Метка не применилась"
exit 1
fi
# ── 2. Ждём регистрации в executor ────────────────────────────────────────────
echo ""
echo ">>> [2/5] Ждём регистрации NS в executor (до 15 сек)..."
DEADLINE=$((SECONDS + 15))
REGISTERED=false
while [ $SECONDS -lt $DEADLINE ]; do
if kubectl logs -n fission deployment/executor 2>/dev/null | grep "registered namespace" | grep -q "$NS"; then
REGISTERED=true
break
fi
sleep 2
done
if [ "$REGISTERED" = "true" ]; then
green "Executor зарегистрировал NS $NS"
else
red "Executor НЕ зарегистрировал NS за 15 сек"
echo " Лог executor (последние строки про namespace):"
kubectl logs -n fission deployment/executor 2>/dev/null | grep "namespace" | tail -5
fi
# ── 3. Создаём environment в новом NS ─────────────────────────────────────────
echo ""
echo ">>> [3/5] Создаём Python environment в NS $NS..."
fission env create \
--name py-env \
--namespace "$NS" \
--image ghcr.io/fission/python-env:latest \
--poolsize 1 2>&1
if kubectl get environment py-env -n "$NS" &>/dev/null; then
green "Environment py-env создан в $NS"
else
red "Environment не создался"
fi
# ── 4. Создаём функцию ────────────────────────────────────────────────────────
echo ""
echo ">>> [4/5] Создаём функцию hello..."
cat > /tmp/hello.py << 'EOF'
def main():
return "hello from layer1"
EOF
fission fn create \
--name hello \
--namespace "$NS" \
--env py-env \
--code /tmp/hello.py 2>&1
fission httptrigger create \
--name hello-route \
--namespace "$NS" \
--function hello \
--url "/${NS}/hello" \
--method GET 2>&1
if kubectl get httptrigger hello-route -n "$NS" &>/dev/null; then
green "Function + HTTPTrigger созданы"
else
red "HTTPTrigger не создался"
fi
# ── 5. Вызываем функцию через внутренний router с JWT ────────────────────────
echo ""
echo ">>> [5/5] Вызываем функцию через internal router + JWT (cold start до 180 сек)..."
ROUTER_INT="http://router.fission.svc.cluster.local"
DEADLINE=$((SECONDS + 180))
FN_OK=false
while [ $SECONDS -lt $DEADLINE ]; do
RESULT=$(kubectl run fn-probe-$$ --rm -i --restart=Never \
--image=curlimages/curl:8.7.1 \
--namespace=fission \
-- sh -c "
TOKEN=\$(curl -sf --max-time 15 -X POST ${ROUTER_INT}/auth/login \
-H 'Content-Type: application/json' \
-d '{\"username\":\"admin\",\"password\":\"7XG1lSg0EFqPLE4pf3He\"}' \
| grep -o '\"accesstoken\":\"[^\"]*' | cut -d'\"' -f4)
if [ -z \"\$TOKEN\" ]; then echo 'LOGIN_FAIL|||000'; exit 0; fi
curl -s -w '|||%{http_code}' --max-time 90 \
-H \"Authorization: Bearer \$TOKEN\" \
${ROUTER_INT}/${NS}/hello
" 2>/dev/null | grep "|||" || echo "|||000")
CODE=$(echo "$RESULT" | grep -o '|||[0-9]*' | tr -d '|||')
BODY=$(echo "$RESULT" | sed 's/|||[0-9]*$//')
echo " HTTP $CODE$(echo "$BODY" | head -c 80)"
[ "$CODE" = "200" ] && FN_OK=true && break
sleep 5
done
if [ "$FN_OK" = "true" ]; then
green "Функция ответила 200: $BODY"
else
red "Функция не ответила 200 (timeout)"
echo " Логи router (last 3):"
kubectl logs -n fission deployment/router 2>/dev/null | tail -3
fi
# ── Cleanup ───────────────────────────────────────────────────────────────────
echo ""
echo ">>> Cleanup..."
fission httptrigger delete --name hello-route --namespace "$NS" 2>/dev/null || true
fission fn delete --name hello --namespace "$NS" 2>/dev/null || true
fission env delete --name py-env --namespace "$NS" 2>/dev/null || true
kubectl delete ns "$NS" --wait=false 2>/dev/null || true
echo " Готово."
# ── Итог ─────────────────────────────────────────────────────────────────────
echo ""
echo "========================================"
echo " ИТОГ: PASS=$PASS FAIL=$FAIL"
echo "========================================"
[ $FAIL -eq 0 ] && exit 0 || exit 1
-214
View File
@@ -1,214 +0,0 @@
#!/usr/bin/env bash
# test_multitenant_ns.sh — стресс-тест Layer 1 (патченный Fission NSWatcher).
#
# Что проверяем:
# 1. N пользователей создаются параллельно (каждый → свой NS через консоль API)
# 2. Executor регистрирует каждый NS через NSWatcher (лог: registered namespace)
# 3. В каждом NS создаётся функция через консоль API
# 4. Каждая функция вызывается и отвечает правильно
# 5. Параллельные вызовы всех функций одновременно — нет деградации
#
# Запуск: bash test_multitenant_ns.sh [COUNT]
# COUNT — кол-во параллельных пользователей (default: 10)
set -uo pipefail
if [ "${KUBECTL_DIRECT:-0}" = "1" ]; then
EXECUTOR_LOGS_CMD() {
kubectl logs -n fission deployment/executor 2>/dev/null
}
else
SSH="ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213"
EXECUTOR_LOGS_CMD() {
$SSH "kubectl logs -n fission deployment/executor 2>/dev/null"
}
fi
BASE="https://fission.kube5s.ru/console/api"
COUNT=${1:-10}
RUN_ID="mt$(date +%s | tail -c 6)"
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
PASS=0; FAIL=0
declare -a CREATED_USERS=()
cleanup_namespaces() {
rm -rf "${TMPDIR_RESULTS:-}" 2>/dev/null || true
if [ -x "$NS_CLEANUP_SCRIPT" ] && [ "${#CREATED_USERS[@]}" -gt 0 ]; then
echo ""
echo ">>> Cleanup test namespaces..."
"$NS_CLEANUP_SCRIPT" "${CREATED_USERS[@]}" || true
fi
}
trap cleanup_namespaces EXIT
green() { echo -e "\033[32m PASS: $1\033[0m"; PASS=$((PASS+1)); }
red() { echo -e "\033[31m FAIL: $1\033[0m"; FAIL=$((FAIL+1)); }
echo ""
echo "========================================"
echo " Layer 1 NSWatcher — стресс-тест"
echo " Пользователей: $COUNT run: $RUN_ID"
echo "========================================"
# Каждый "пользователь" — уникальный X-Test-Sub → уникальный NS
# Консоль вызывает SetupFissionNamespace → NS получает fission.io/managed=true
# Executor NSWatcher подхватывает → регистрирует без рестарта
# ── Шаг 1: создаём NS для всех пользователей параллельно ─────────────────────
echo ""
echo ">>> [1/4] Создаём NS для $COUNT пользователей параллельно..."
TMPDIR_RESULTS=$(mktemp -d)
for i in $(seq 1 $COUNT); do
USER="${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-Test-Sub: ${USER}" 2>/dev/null || echo 000)
echo "$i:$CODE:$USER" > "${TMPDIR_RESULTS}/init-${i}"
) &
done
wait
NS_OK=0; NS_FAIL=0
declare -A USERS
for i in $(seq 1 $COUNT); do
R=$(cat "${TMPDIR_RESULTS}/init-${i}" 2>/dev/null || echo "$i:000:")
CODE=$(echo "$R" | cut -d: -f2)
USER=$(echo "$R" | cut -d: -f3)
USERS[$i]="$USER"
if [ "$CODE" = "200" ]; then
NS_OK=$((NS_OK+1))
else
NS_FAIL=$((NS_FAIL+1))
echo " user${i}: HTTP $CODE"
fi
done
if [ $NS_FAIL -eq 0 ]; then
green "Все $COUNT NS созданы через консоль API (HTTP 200)"
else
red "$NS_FAIL / $COUNT NS не создались"
fi
# ── Шаг 2: проверяем что executor зарегистрировал NS ─────────────────────────
echo ""
echo ">>> [2/4] Проверяем регистрацию NS в executor..."
# Ждём немного — informer почти мгновенный, но логи могут задержаться
sleep 5
EXEC_LOGS=$(EXECUTOR_LOGS_CMD | grep 'registered namespace' 2>/dev/null || echo "")
REGISTERED=0
for i in $(seq 1 $COUNT); do
USER="${USERS[$i]:-}"
# NS имя формируется консолью как hash от email
# Ищем любые новые registered namespace в логах после старта теста
REGISTERED=$(EXECUTOR_LOGS_CMD | grep 'registered namespace' | wc -l 2>/dev/null || echo 0)
done
echo " Всего registered namespace событий в executor: $REGISTERED"
if [ "$REGISTERED" -ge "$COUNT" ]; then
green "Executor зарегистрировал минимум $COUNT NS"
else
# Не обязательно FAIL — NS могли быть уже зарегистрированы ранее
echo " INFO: зарегистрировано $REGISTERED (некоторые NS могли существовать до теста)"
PASS=$((PASS+1))
fi
# ── Шаг 3: создаём Python функцию для каждого пользователя ───────────────────
echo ""
echo ">>> [3/4] Создаём функции для каждого пользователя параллельно..."
# Простой Python код — не требует компиляции, деплоится мгновенно
PYTHON_CODE='def main():
return "ok-'"${RUN_ID}"'"'
json_escape() {
python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
}
for i in $(seq 1 $COUNT); do
USER="${USERS[$i]:-${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-Test-Sub: ${USER}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${FN_NAME}\",\"language\":\"python\",\"code\":$(printf '%s' \"${PYTHON_CODE}\" | json_escape)}" \
2>/dev/null)
CODE=$(echo "$CREATE" | tail -1)
echo "$i:$CODE" > "${TMPDIR_RESULTS}/create-${i}"
) &
done
wait
CREATE_OK=0; CREATE_FAIL=0
for i in $(seq 1 $COUNT); do
R=$(cat "${TMPDIR_RESULTS}/create-${i}" 2>/dev/null || echo "$i:000")
CODE=$(echo "$R" | cut -d: -f2)
if [ "$CODE" = "200" ] || [ "$CODE" = "201" ]; then
CREATE_OK=$((CREATE_OK+1))
else
CREATE_FAIL=$((CREATE_FAIL+1))
echo " user${i}: create HTTP $CODE"
fi
done
if [ $CREATE_FAIL -eq 0 ]; then
green "Все $COUNT функции созданы"
else
red "$CREATE_FAIL / $COUNT функций не создались"
fi
# ── Шаг 4: вызываем все функции параллельно ──────────────────────────────────
echo ""
echo ">>> [4/4] Параллельные вызовы всех $COUNT функций..."
# Ждём прогрева (первый вызов — cold start)
sleep 5
for i in $(seq 1 $COUNT); do
USER="${USERS[$i]:-${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-Test-Sub: ${USER}" \
-H "Content-Type: application/json" \
-d '{}' 2>/dev/null || echo 000)
echo "$i:$CODE" > "${TMPDIR_RESULTS}/invoke-${i}"
) &
done
wait
INVOKE_OK=0; INVOKE_FAIL=0
for i in $(seq 1 $COUNT); do
R=$(cat "${TMPDIR_RESULTS}/invoke-${i}" 2>/dev/null || echo "$i:000")
CODE=$(echo "$R" | cut -d: -f2)
if [ "$CODE" = "200" ]; then
INVOKE_OK=$((INVOKE_OK+1))
else
INVOKE_FAIL=$((INVOKE_FAIL+1))
echo " user${i}: invoke HTTP $CODE"
fi
done
if [ $INVOKE_FAIL -eq 0 ]; then
green "Все $COUNT функции отвечают HTTP 200 параллельно"
else
red "$INVOKE_FAIL / $COUNT функций не ответили (возможно cold start — повторить тест)"
fi
# ── Итог ─────────────────────────────────────────────────────────────────────
echo ""
echo "========================================"
echo " ИТОГ: PASS=$PASS FAIL=$FAIL"
echo "========================================"
[ $FAIL -eq 0 ] && exit 0 || exit 1
-812
View File
@@ -1,812 +0,0 @@
#!/usr/bin/env bash
# test_simple_function.sh — интеграционные тесты fission_simple_function
# Запуск: bash test_simple_function.sh
# Требования: terraform + kubectl в PATH, kubeconfig в ~/.kube/config
set -uo pipefail
PASS=0; FAIL=0; SKIP=0
NAMESPACE="default"
TESTDIR="/tmp/sless-sf-tests"
KUBECONFIG_PATH="/home/naeel/.kube/config"
PROVIDER_NS="default"
# уникальный префикс — не пересекаемся с prod
RUN_ID="sf$(date +%s | tail -c 6)"
TF_PROVIDER='
terraform {
required_providers {
fission = { source = "nail/fission" }
}
}
provider "fission" {
kubeconfig_path = "/home/naeel/.kube/config"
namespace = "default"
}
'
green() { echo -e "\033[32m$*\033[0m"; }
red() { echo -e "\033[31m$*\033[0m"; }
yellow(){ echo -e "\033[33m$*\033[0m"; }
pass() { PASS=$((PASS+1)); green " PASS: $1"; }
fail() { FAIL=$((FAIL+1)); red " FAIL: $1"; }
skip() { SKIP=$((SKIP+1)); yellow " SKIP: $1"; }
# ── helpers ──────────────────────────────────────────────────────────────────
kube_exists() {
local kind=$1 name=$2
kubectl get "${kind}" -n "${NAMESPACE}" "${name}" &>/dev/null
}
kube_gone() {
local kind=$1 name=$2
! kubectl get "${kind}" -n "${NAMESPACE}" "${name}" &>/dev/null
}
tf_apply() {
local dir=$1
terraform -chdir="${dir}" apply -auto-approve -no-color 2>&1
}
tf_destroy() {
local dir=$1
terraform -chdir="${dir}" destroy -auto-approve -no-color 2>&1
}
tf_plan_exitcode() {
# 0=no-changes, 1=error, 2=changes
local dir=$1
terraform -chdir="${dir}" plan -detailed-exitcode -no-color 2>&1
echo $?
}
wait_package_ready() {
local name=$1 retries=30
for ((i=0; i<retries; i++)); do
local status
status=$(kubectl get packages.fission.io -n "${NAMESPACE}" "${name}" \
-o jsonpath='{.status.buildstatus}' 2>/dev/null || echo "")
# для literal packages buildstatus пустой — это нормально
[[ "$status" == "succeeded" || "$status" == "" ]] && return 0
sleep 2
done
return 1
}
# ── setup / teardown ─────────────────────────────────────────────────────────
setup_workdir() {
local name=$1
local dir="${TESTDIR}/${name}"
rm -rf "${dir}"
mkdir -p "${dir}"
echo "${dir}"
}
write_provider() {
local dir=$1
cat > "${dir}/main.tf" <<EOF
${TF_PROVIDER}
EOF
}
# ── code fixtures ─────────────────────────────────────────────────────────────
write_js_v1() {
local dir=$1
mkdir -p "${dir}"
cat > "${dir}/main.js" <<'EOF'
exports.handler = async (ctx) => ({
body: JSON.stringify({ msg: "hello-v1", ts: Date.now() }),
headers: { "Content-Type": "application/json" },
});
EOF
}
write_js_v2() {
local dir=$1
mkdir -p "${dir}"
cat > "${dir}/main.js" <<'EOF'
exports.handler = async (ctx) => ({
body: JSON.stringify({ msg: "hello-v2-updated", ts: Date.now() }),
headers: { "Content-Type": "application/json" },
});
EOF
}
write_python() {
local dir=$1
mkdir -p "${dir}"
cat > "${dir}/main.py" <<'EOF'
def main():
return {"status": 200, "body": '{"msg":"hello-python"}'}
EOF
}
write_ruby() {
local dir=$1
mkdir -p "${dir}"
cat > "${dir}/main.rb" <<'EOF'
def main(ctx)
{ body: "hello-ruby", status_code: 200 }
end
EOF
}
write_php() {
local dir=$1
mkdir -p "${dir}"
cat > "${dir}/main.php" <<'EOF'
<?php
function main($context) {
return ["body" => "hello-php"];
}
EOF
}
write_perl() {
local dir=$1
mkdir -p "${dir}"
cat > "${dir}/main.pl" <<'EOF'
sub main {
my ($context) = @_;
return { body => "hello-perl" };
}
1;
EOF
}
EOF
cat > "${dir}/go.mod" <<'EOF'
module fn
go 1.21
EOF
}
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "================================================================"
echo " fission_simple_function — интеграционные тесты"
echo " Run ID: ${RUN_ID}"
echo "================================================================"
mkdir -p "${TESTDIR}"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T01: nodejs — create с HTTPTrigger, verify k8s, destroy"
# ════════════════════════════════════════════════════════════════════════
T01="${RUN_ID}-node-basic"
D=$(setup_workdir "t01")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T01}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T01}"
methods = ["GET"]
}
EOF
OUT=$(tf_apply "${D}")
if echo "${OUT}" | grep -q "Apply complete"; then
pass "T01: terraform apply OK"
else
fail "T01: terraform apply FAILED"
echo "${OUT}" | tail -20
fi
kube_exists "functions.fission.io" "${T01}" \
&& pass "T01: Function создана в k8s" || fail "T01: Function не найдена в k8s"
kube_exists "packages.fission.io" "${T01}-pkg" \
&& pass "T01: Package создан в k8s" || fail "T01: Package не найден в k8s"
kube_exists "httptriggers.fission.io" "${T01}-trigger" \
&& pass "T01: HTTPTrigger создан в k8s" || fail "T01: HTTPTrigger не найден в k8s"
kube_exists "environments.fission.io" "simple-nodejs-env" \
&& pass "T01: Environment simple-nodejs-env создан" || fail "T01: Environment не найден"
OUT=$(tf_destroy "${D}")
echo "${OUT}" | grep -q "Destroy complete" \
&& pass "T01: terraform destroy OK" || fail "T01: terraform destroy FAILED"
kube_gone "functions.fission.io" "${T01}" && pass "T01: Function удалена после destroy" || fail "T01: Function осталась после destroy"
kube_gone "packages.fission.io" "${T01}-pkg" && pass "T01: Package удалён после destroy" || fail "T01: Package остался после destroy"
kube_gone "httptriggers.fission.io" "${T01}-trigger" && pass "T01: Trigger удалён после destroy" || fail "T01: Trigger остался после destroy"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T02: python — create БЕЗ url (нет HTTPTrigger)"
# ════════════════════════════════════════════════════════════════════════
T02="${RUN_ID}-py-notrigger"
D=$(setup_workdir "t02")
write_provider "${D}"
write_python "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T02}"
runtime = "python"
code_dir = "\${path.module}/code"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" \
&& pass "T02: apply OK" || fail "T02: apply FAILED"
kube_exists "functions.fission.io" "${T02}" && pass "T02: Function создана" || fail "T02: Function не найдена"
kube_exists "packages.fission.io" "${T02}-pkg" && pass "T02: Package создан" || fail "T02: Package не найден"
kube_gone "httptriggers.fission.io" "${T02}-trigger" \
&& pass "T02: HTTPTrigger НЕ создан (url не задан)" || fail "T02: HTTPTrigger создан — не должен был"
kube_exists "environments.fission.io" "simple-python-env" \
&& pass "T02: Environment simple-python-env создан" || fail "T02: Environment не найден"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T02: destroy OK" || fail "T02: destroy FAILED"
kube_gone "functions.fission.io" "${T02}" && pass "T02: Function удалена" || fail "T02: Function осталась"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T04: ruby — create с URL"
# ════════════════════════════════════════════════════════════════════════
T04="${RUN_ID}-ruby"
D=$(setup_workdir "t04")
write_provider "${D}"
write_ruby "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T04}"
runtime = "ruby"
code_dir = "\${path.module}/code"
url = "/test/${T04}"
methods = ["GET"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" \
&& pass "T04: ruby apply OK" || fail "T04: ruby apply FAILED"
kube_exists "environments.fission.io" "simple-ruby-env" \
&& pass "T04: simple-ruby-env создан" || fail "T04: simple-ruby-env не найден"
kube_exists "httptriggers.fission.io" "${T04}-trigger" \
&& pass "T04: Trigger создан" || fail "T04: Trigger не найден"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T04: destroy OK" || fail "T04: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T05: php — create с URL"
# ════════════════════════════════════════════════════════════════════════
T05="${RUN_ID}-php"
D=$(setup_workdir "t05")
write_provider "${D}"
write_php "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T05}"
runtime = "php"
code_dir = "\${path.module}/code"
url = "/test/${T05}"
methods = ["GET"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" \
&& pass "T05: php apply OK" || fail "T05: php apply FAILED"
kube_exists "environments.fission.io" "simple-php-env" \
&& pass "T05: simple-php-env создан" || fail "T05: simple-php-env не найден"
kube_exists "functions.fission.io" "${T05}" \
&& pass "T05: Function создана" || fail "T05: Function не найдена"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T05: destroy OK" || fail "T05: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T06: perl — create с URL"
# ════════════════════════════════════════════════════════════════════════
T06="${RUN_ID}-perl"
D=$(setup_workdir "t06")
write_provider "${D}"
write_perl "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T06}"
runtime = "perl"
code_dir = "\${path.module}/code"
url = "/test/${T06}"
methods = ["GET"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" \
&& pass "T06: perl apply OK" || fail "T06: perl apply FAILED"
kube_exists "environments.fission.io" "simple-perl-env" \
&& pass "T06: simple-perl-env создан" || fail "T06: simple-perl-env не найден"
kube_exists "functions.fission.io" "${T06}" \
&& pass "T06: Function создана" || fail "T06: Function не найдена"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T06: destroy OK" || fail "T06: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T07: code_hash drift detection — update пересчитывает хеш и обновляет Package"
# ════════════════════════════════════════════════════════════════════════
T07="${RUN_ID}-drift"
D=$(setup_workdir "t07")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T07}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T07: apply v1 OK" || fail "T07: apply v1 FAILED"
HASH1=$(terraform -chdir="${D}" show -json 2>/dev/null \
| python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['code_hash'])" 2>/dev/null || echo "?")
# Обновляем код (v2)
write_js_v2 "${D}/code"
OUT=$(tf_apply "${D}")
echo "${OUT}" | grep -q "Apply complete" && pass "T07: apply v2 OK" || fail "T07: apply v2 FAILED"
HASH2=$(terraform -chdir="${D}" show -json 2>/dev/null \
| python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['code_hash'])" 2>/dev/null || echo "?")
[ "${HASH1}" != "${HASH2}" ] && [ "${HASH1}" != "?" ] && [ "${HASH2}" != "?" ] \
&& pass "T07: code_hash изменился (${HASH1:0:8}… → ${HASH2:0:8}…)" \
|| fail "T07: code_hash не изменился или не читается (h1=${HASH1:0:8}, h2=${HASH2:0:8})"
echo "${OUT}" | grep -qE "fission_simple_function.fn.*updated|updated.*fission_simple_function.fn" \
&& pass "T07: Package был обновлён (update in apply output)" \
|| { echo "${OUT}" | grep -q "1 updated" && pass "T07: 1 ресурс обновлён" || fail "T07: Обновление не обнаружено в output"; }
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T07: destroy OK" || fail "T07: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T08: добавление url (update — создание Trigger)"
# ════════════════════════════════════════════════════════════════════════
T08="${RUN_ID}-addurl"
D=$(setup_workdir "t08")
write_provider "${D}"
write_js_v1 "${D}/code"
# Apply без url
cat > "${D}/main.tf" <<EOF
${TF_PROVIDER}
resource "fission_simple_function" "fn" {
name = "${T08}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T08: apply (no url) OK" || fail "T08: apply (no url) FAILED"
kube_gone "httptriggers.fission.io" "${T08}-trigger" \
&& pass "T08: Trigger не создан (нет url)" || fail "T08: Trigger создан без url"
# Добавляем url
cat > "${D}/main.tf" <<EOF
${TF_PROVIDER}
resource "fission_simple_function" "fn" {
name = "${T08}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T08}"
methods = ["GET", "POST"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T08: apply (with url) OK" || fail "T08: apply (with url) FAILED"
kube_exists "httptriggers.fission.io" "${T08}-trigger" \
&& pass "T08: Trigger создан после добавления url" || fail "T08: Trigger не создан после update"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T08: destroy OK" || fail "T08: destroy FAILED"
kube_gone "httptriggers.fission.io" "${T08}-trigger" \
&& pass "T08: Trigger удалён при destroy" || fail "T08: Trigger остался"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T09: удаление url (update — удаление Trigger)"
# ════════════════════════════════════════════════════════════════════════
T09="${RUN_ID}-rmurl"
D=$(setup_workdir "t09")
write_provider "${D}"
write_js_v1 "${D}/code"
# Apply с url
cat > "${D}/main.tf" <<EOF
${TF_PROVIDER}
resource "fission_simple_function" "fn" {
name = "${T09}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T09}"
methods = ["GET"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T09: apply (with url) OK" || fail "T09: apply (with url) FAILED"
kube_exists "httptriggers.fission.io" "${T09}-trigger" \
&& pass "T09: Trigger создан" || fail "T09: Trigger не создан"
# Убираем url
cat > "${D}/main.tf" <<EOF
${TF_PROVIDER}
resource "fission_simple_function" "fn" {
name = "${T09}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T09: apply (url removed) OK" || fail "T09: apply (url removed) FAILED"
kube_gone "httptriggers.fission.io" "${T09}-trigger" \
&& pass "T09: Trigger удалён после убирания url" || fail "T09: Trigger остался после убирания url"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T09: destroy OK" || fail "T09: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T10: изменение url (update Trigger)"
# ════════════════════════════════════════════════════════════════════════
T10="${RUN_ID}-churl"
D=$(setup_workdir "t10")
write_provider "${D}"
write_js_v1 "${D}/code"
cat > "${D}/main.tf" <<EOF
${TF_PROVIDER}
resource "fission_simple_function" "fn" {
name = "${T10}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T10}/v1"
methods = ["GET"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T10: apply v1 url OK" || fail "T10: apply v1 url FAILED"
URL_BEFORE=$(kubectl get httptriggers.fission.io -n "${NAMESPACE}" "${T10}-trigger" \
-o jsonpath='{.spec.relativeurl}' 2>/dev/null || echo "?")
cat > "${D}/main.tf" <<EOF
${TF_PROVIDER}
resource "fission_simple_function" "fn" {
name = "${T10}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T10}/v2"
methods = ["GET", "PUT"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T10: apply v2 url OK" || fail "T10: apply v2 url FAILED"
URL_AFTER=$(kubectl get httptriggers.fission.io -n "${NAMESPACE}" "${T10}-trigger" \
-o jsonpath='{.spec.relativeurl}' 2>/dev/null || echo "?")
[ "${URL_BEFORE}" = "/test/${T10}/v1" ] && [ "${URL_AFTER}" = "/test/${T10}/v2" ] \
&& pass "T10: URL обновлён (/v1 → /v2)" \
|| fail "T10: URL не обновлён (before='${URL_BEFORE}', after='${URL_AFTER}')"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T10: destroy OK" || fail "T10: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T11: plan без изменений → 0 changes"
# ════════════════════════════════════════════════════════════════════════
T11="${RUN_ID}-nochange"
D=$(setup_workdir "t11")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T11}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T11}"
methods = ["GET"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T11: initial apply OK" || fail "T11: initial apply FAILED"
PLAN_OUT=$(terraform -chdir="${D}" plan -no-color 2>&1)
echo "${PLAN_OUT}" | grep -qE "No changes|0 to add, 0 to change, 0 to destroy" \
&& pass "T11: повторный plan показывает 0 changes" \
|| fail "T11: повторный plan показывает изменения (не idempotent)"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T11: destroy OK" || fail "T11: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T12: auto-env idempotency — два ресурса делят один environment"
# ════════════════════════════════════════════════════════════════════════
T12A="${RUN_ID}-shared-a"
T12B="${RUN_ID}-shared-b"
D=$(setup_workdir "t12")
write_provider "${D}"
write_js_v1 "${D}/code-a"
write_js_v1 "${D}/code-b"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn_a" {
name = "${T12A}"
runtime = "nodejs"
code_dir = "\${path.module}/code-a"
}
resource "fission_simple_function" "fn_b" {
name = "${T12B}"
runtime = "nodejs"
code_dir = "\${path.module}/code-b"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T12: apply двух функций OK" || fail "T12: apply двух функций FAILED"
kube_exists "functions.fission.io" "${T12A}" && pass "T12: fn_a создана" || fail "T12: fn_a не создана"
kube_exists "functions.fission.io" "${T12B}" && pass "T12: fn_b создана" || fail "T12: fn_b не создана"
# Environment должен быть один (один simple-nodejs-env)
ENV_COUNT=$(kubectl get environments.fission.io -n "${NAMESPACE}" -o name 2>/dev/null \
| grep -c "simple-nodejs-env" || echo "0")
[ "${ENV_COUNT}" -eq 1 ] \
&& pass "T12: ровно 1 экземпляр simple-nodejs-env (shared)" \
|| fail "T12: ENV_COUNT=${ENV_COUNT} (ожидалось 1)"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T12: destroy OK" || fail "T12: destroy FAILED"
kube_gone "functions.fission.io" "${T12A}" && pass "T12: fn_a удалена" || fail "T12: fn_a осталась"
kube_gone "functions.fission.io" "${T12B}" && pass "T12: fn_b удалена" || fail "T12: fn_b осталась"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T13: невалидный runtime → apply завершается ошибкой"
# ════════════════════════════════════════════════════════════════════════
T13="${RUN_ID}-badruntime"
D=$(setup_workdir "t13")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T13}"
runtime = "cobol"
code_dir = "\${path.module}/code"
}
EOF
OUT=$(tf_apply "${D}" 2>&1 || true)
echo "${OUT}" | grep -qiE "Error|error|cobol|неподдерживаемый|unsupported" \
&& pass "T13: apply с runtime=cobol выдал ошибку" \
|| fail "T13: apply не выдал ошибку для runtime=cobol"
kube_gone "functions.fission.io" "${T13}" \
&& pass "T13: Function не создана" || fail "T13: Function создана для плохого runtime"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T14: terraform import (namespace/name)"
# ════════════════════════════════════════════════════════════════════════
T14="${RUN_ID}-import"
D=$(setup_workdir "t14")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T14}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T14}"
methods = ["GET"]
}
EOF
# Создаём через apply
tf_apply "${D}" | grep -q "Apply complete" && pass "T14: initial apply OK" || fail "T14: initial apply FAILED"
# Удаляем state, импортируем обратно
terraform -chdir="${D}" state rm fission_simple_function.fn &>/dev/null || true
IMP_OUT=$(terraform -chdir="${D}" import -no-color \
fission_simple_function.fn "default/${T14}" 2>&1)
echo "${IMP_OUT}" | grep -qiE "Import successful|import" \
&& pass "T14: import выполнен" || fail "T14: import не прошёл: $(echo "${IMP_OUT}" | tail -5)"
# Plan после import должен показать изменения (code_dir/hash не в k8s state)
# — это нормально, просто убеждаемся что plan не падает с ошибкой
PLAN_OUT=$(terraform -chdir="${D}" plan -no-color 2>&1)
echo $? | grep -qE "^[012]$" && pass "T14: plan после import не падает" || fail "T14: plan после import упал"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T14: destroy OK" || fail "T14: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T15: nodejs — методы по умолчанию [GET]"
# ════════════════════════════════════════════════════════════════════════
T15="${RUN_ID}-defmethods"
D=$(setup_workdir "t15")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T15}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T15}"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T15: apply (no methods) OK" || fail "T15: apply FAILED"
METHODS=$(kubectl get httptriggers.fission.io -n "${NAMESPACE}" "${T15}-trigger" \
-o jsonpath='{.spec.methods[*]}' 2>/dev/null || echo "?")
echo "${METHODS}" | grep -q "GET" \
&& pass "T15: default method=GET в k8s: '${METHODS}'" \
|| fail "T15: методы не GET: '${METHODS}'"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T15: destroy OK" || fail "T15: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T16: Package.spec для nodejs — deployment (не source)"
# ════════════════════════════════════════════════════════════════════════
T16="${RUN_ID}-nodepkg"
D=$(setup_workdir "t16")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T16}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T16: apply OK" || fail "T16: apply FAILED"
SRC=$(kubectl get packages.fission.io -n "${NAMESPACE}" "${T16}-pkg" \
-o jsonpath='{.spec.source.type}' 2>/dev/null || echo "")
DEP=$(kubectl get packages.fission.io -n "${NAMESPACE}" "${T16}-pkg" \
-o jsonpath='{.spec.deployment.type}' 2>/dev/null || echo "")
[ -z "${SRC}" ] && [ "${DEP}" = "literal" ] \
&& pass "T16: nodejs Package использует deployment.literal (не source)" \
|| fail "T16: pkg spec — source.type='${SRC}', deployment.type='${DEP}'"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T16: destroy OK" || fail "T16: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T17: namespace вычисляется из провайдера"
# ════════════════════════════════════════════════════════════════════════
T17="${RUN_ID}-ns"
D=$(setup_workdir "t17")
write_js_v1 "${D}/code"
# Провайдер без явного namespace в ресурсе
cat > "${D}/main.tf" <<EOF
${TF_PROVIDER}
resource "fission_simple_function" "fn" {
name = "${T17}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T17: apply OK" || fail "T17: apply FAILED"
NS=$(terraform -chdir="${D}" show -json 2>/dev/null \
| python3 -c "import sys,json; r=json.load(sys.stdin)['values']['root_module']['resources']; print(r[0]['values']['namespace'])" 2>/dev/null || echo "?")
[ "${NS}" = "default" ] \
&& pass "T17: namespace=default из провайдера (state: ${NS})" \
|| fail "T17: namespace='${NS}' (ожидалось 'default')"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T17: destroy OK" || fail "T17: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T18: computed поля environment_name/package_name/trigger_name в state"
# ════════════════════════════════════════════════════════════════════════
T18="${RUN_ID}-computed"
D=$(setup_workdir "t18")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T18}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T18}"
methods = ["GET"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T18: apply OK" || fail "T18: apply FAILED"
SHOW=$(terraform -chdir="${D}" show -json 2>/dev/null \
| python3 -c "
import sys, json
r = json.load(sys.stdin)['values']['root_module']['resources'][0]['values']
print(r.get('environment_name','?'))
print(r.get('package_name','?'))
print(r.get('trigger_name','?'))
print(r.get('code_hash','?'))
" 2>/dev/null || echo "?
?
?
?")
ENV_NAME=$(echo "${SHOW}" | sed -n '1p')
PKG_NAME=$(echo "${SHOW}" | sed -n '2p')
TRG_NAME=$(echo "${SHOW}" | sed -n '3p')
HASH_VAL=$(echo "${SHOW}" | sed -n '4p')
[ "${ENV_NAME}" = "simple-nodejs-env" ] && pass "T18: environment_name='${ENV_NAME}'" || fail "T18: environment_name='${ENV_NAME}'"
[ "${PKG_NAME}" = "${T18}-pkg" ] && pass "T18: package_name='${PKG_NAME}'" || fail "T18: package_name='${PKG_NAME}'"
[ "${TRG_NAME}" = "${T18}-trigger" ] && pass "T18: trigger_name='${TRG_NAME}'" || fail "T18: trigger_name='${TRG_NAME}'"
[[ "${HASH_VAL}" =~ ^[0-9a-f]{64}$ ]] && pass "T18: code_hash выглядит как sha256" || fail "T18: code_hash='${HASH_VAL}'"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T18: destroy OK" || fail "T18: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T19: повторный apply одного ресурса — idempotency (no drift)"
# ════════════════════════════════════════════════════════════════════════
T19="${RUN_ID}-idem"
D=$(setup_workdir "t19")
write_provider "${D}"
write_js_v1 "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T19}"
runtime = "nodejs"
code_dir = "\${path.module}/code"
url = "/test/${T19}"
methods = ["GET", "POST"]
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T19: первый apply OK" || fail "T19: первый apply FAILED"
OUT2=$(tf_apply "${D}")
echo "${OUT2}" | grep -qE "Apply complete.*0 added, 0 changed, 0 destroyed" \
&& pass "T19: второй apply — 0 changes (idempotent)" \
|| fail "T19: второй apply не idempotent: $(echo "${OUT2}" | grep -E 'added|changed|destroyed' | tail -2)"
tf_destroy "${D}" | grep -q "Destroy complete" && pass "T19: destroy OK" || fail "T19: destroy FAILED"
# ════════════════════════════════════════════════════════════════════════
echo ""
echo ">>> T20: destroy без trigger (url не задан) — нет паники"
# ════════════════════════════════════════════════════════════════════════
T20="${RUN_ID}-notrig-del"
D=$(setup_workdir "t20")
write_provider "${D}"
write_python "${D}/code"
cat >> "${D}/main.tf" <<EOF
resource "fission_simple_function" "fn" {
name = "${T20}"
runtime = "python"
code_dir = "\${path.module}/code"
}
EOF
tf_apply "${D}" | grep -q "Apply complete" && pass "T20: apply OK" || fail "T20: apply FAILED"
OUT=$(tf_destroy "${D}")
echo "${OUT}" | grep -q "Destroy complete" \
&& pass "T20: destroy OK (нет паники без trigger)" \
|| fail "T20: destroy FAILED"
echo "${OUT}" | grep -qi "panic\|nil pointer" \
&& fail "T20: PANIC в destroy!" || pass "T20: нет паники при destroy"
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "================================================================"
if [ "${FAIL}" -eq 0 ]; then
green " ИТОГ: PASS=${PASS} FAIL=${FAIL} SKIP=${SKIP} — ВСЕ ТЕСТЫ ПРОШЛИ"
else
red " ИТОГ: PASS=${PASS} FAIL=${FAIL} SKIP=${SKIP} — ЕСТЬ ПРОВАЛЫ"
fi
echo "================================================================"
echo ""
# Очистка temp dirs
rm -rf "${TESTDIR}"
[ "${FAIL}" -eq 0 ]
-326
View File
@@ -1,326 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
BASE="https://fission.kube5s.ru/console/api"
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
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"
pass() { PASS=$((PASS+1)); echo " PASS: $1"; }
fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; }
json_quote() {
python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
}
json_field() {
local payload="$1"
local key="$2"
printf '%s' "$payload" | python3 -c 'import json,sys
key=sys.argv[1]
try:
data=json.load(sys.stdin)
value=data.get(key, "")
print(value if not isinstance(value, (dict,list)) else json.dumps(value))
except Exception:
print("")' "$key"
}
cleanup_namespaces() {
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
echo ""
echo ">>> Cleanup UI test namespaces..."
KUBECTL_DIRECT=1 "$NS_CLEANUP_SCRIPT" "$LOGIN_SUB" "$LANG_SUB" "$NODE_SUB" "$STACK_SUB" "$NEG_SUB" || true
fi
}
trap cleanup_namespaces EXIT
auth_call() {
local token="$1"
local env_name="${2:-test}"
curl -s -w "\n%{http_code}" --max-time 45 -X POST "${BASE}/auth" \
-H 'Content-Type: application/json' \
-d "{\"token\":\"${token}\",\"env\":\"${env_name}\"}"
}
auth_get() {
local path="$1"
local token="$2"
local env_name="${3:-test}"
curl -s -w "\n%{http_code}" --max-time 45 "${BASE}${path}" \
-H "X-Auth-Token: ${token}" \
-H "X-Auth-Env: ${env_name}"
}
create_call() {
local sub="$1"
local body="$2"
curl -s -w "\n%{http_code}" --max-time 60 -X POST "${BASE}/functions" \
-H "X-Test-Sub: ${sub}" -H 'Content-Type: application/json' -d "$body"
}
invoke_call() {
local sub="$1"
local name="$2"
local body="${3:-{}}"
local out_file err_file http_code rc resp err_text
out_file=$(mktemp)
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-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"
if [ "$rc" -ne 0 ]; then
printf '{"error":%s,"status":"","response_raw":"","http_status":"%s"}\n' \
"$(json_quote "curl rc=${rc} http=${http_code}: ${err_text}")" "$http_code"
return 0
fi
if printf '%s' "$resp" | python3 -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1; then
printf '%s' "$resp"
return 0
fi
printf '{"error":%s,"status":"","response_raw":%s,"http_status":"%s"}\n' \
"$(json_quote "non-json invoke response http=${http_code}")" "$(json_quote "$resp")" "$http_code"
}
wait_invoke_ok() {
local sub="$1"
local name="$2"
local expected="$3"
local tries="$4"
local delay="$5"
local try status raw err latency http_status
for try in $(seq 1 "$tries"); do
resp=$(invoke_call "$sub" "$name")
status=$(json_field "$resp" status)
raw=$(json_field "$resp" response_raw)
err=$(json_field "$resp" error)
latency=$(json_field "$resp" latency_ms)
http_status=$(json_field "$resp" http_status)
echo " TRY=${try} fn=${name} status=${status} http=${http_status} latency_ms=${latency} raw=${raw} err=${err}"
if [ "$status" = "200" ] && printf '%s' "$raw" | grep -q "$expected"; then
return 0
fi
if [ "$try" -lt "$tries" ]; then
sleep "$delay"
fi
done
return 1
}
wait_init_ready() {
local token="$1"
local env_name="${2:-test}"
local tries="$3"
local delay="$4"
local try debug_raw debug_code debug_body status_raw status_code status_body ready done dbg_ready exec_ready exec_pods router_ready router_pods
for try in $(seq 1 "$tries"); do
debug_raw=$(auth_get "/ns/debug" "$token" "$env_name")
debug_code=$(printf '%s' "$debug_raw" | tail -1)
debug_body=$(printf '%s' "$debug_raw" | sed '$d')
status_raw=$(auth_get "/ns/status" "$token" "$env_name")
status_code=$(printf '%s' "$status_raw" | tail -1)
status_body=$(printf '%s' "$status_raw" | sed '$d')
ready=$(json_field "$status_body" ready)
done=$(printf '%s' "$status_body" | python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
print(sum(1 for s in data.get("stages", []) if s.get("done")))
except Exception:
print(0)')
dbg_ready=$(printf '%s' "$debug_body" | python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
print(str(data.get("controlPlane", {}).get("ready", "")))
except Exception:
print("")')
exec_ready=$(printf '%s' "$debug_body" | python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
print(str(data.get("controlPlane", {}).get("executor", {}).get("ready", "")))
except Exception:
print("")')
exec_pods=$(printf '%s' "$debug_body" | python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
print(data.get("controlPlane", {}).get("executor", {}).get("podCount", ""))
except Exception:
print("")')
router_ready=$(printf '%s' "$debug_body" | python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
print(str(data.get("controlPlane", {}).get("router", {}).get("ready", "")))
except Exception:
print("")')
router_pods=$(printf '%s' "$debug_body" | python3 -c 'import json,sys
try:
data=json.load(sys.stdin)
print(data.get("controlPlane", {}).get("router", {}).get("podCount", ""))
except Exception:
print("")')
echo " INIT_TRY=${try} status_http=${status_code} debug_http=${debug_code} ready=${ready} done=${done}/3 cp_ready=${dbg_ready} executor_ready=${exec_ready} executor_pods=${exec_pods} router_ready=${router_ready} router_pods=${router_pods}"
if [ "$status_code" = "200" ] && [ "$debug_code" = "200" ] && [ "$ready" = "True" -o "$ready" = "true" ]; then
return 0
fi
if [ "$try" -lt "$tries" ]; then
sleep "$delay"
fi
done
return 1
}
echo ""
echo "========================================"
echo " UI-like user flow tests"
echo " Run ID: ${RUN_ID}"
echo "========================================"
echo ""
echo ">>> T01: login с невалидным токеном"
R=$(auth_call "not-an-email")
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "401" ] && pass "login invalid token -> 401" || fail "login invalid token -> got $CODE"
echo ""
echo ">>> T02: login c валидным test token"
R=$(auth_call "$LOGIN_SUB")
CODE=$(printf '%s' "$R" | tail -1)
BODY=$(printf '%s' "$R" | sed '$d')
NS=$(json_field "$BODY" namespace)
[ "$CODE" = "200" ] && pass "login valid token -> 200" || fail "login valid token -> got $CODE"
printf '%s' "$NS" | grep -q '^fission-' && pass "login returns namespace" || fail "login namespace malformed: ${NS}"
echo ""
echo ">>> T02b: первый вход ждёт init overlay до 3/3"
if wait_init_ready "$LOGIN_SUB" "test" 12 5; then
pass "init overlay path -> namespace ready 3/3"
else
fail "init overlay path did not reach 3/3"
fi
echo ""
echo ">>> T03: ошибки create, которые типичны для UI"
R=$(create_call "$NEG_SUB" '{"name":"","language":"python","code":"def main():\n return 1"}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "400" ] && pass "empty name -> 400" || fail "empty name -> got $CODE"
R=$(create_call "$NEG_SUB" '{"name":"empty-code","language":"python","code":" "}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "400" ] && pass "empty code -> 400" || fail "empty code -> got $CODE"
R=$(create_call "$NEG_SUB" '{"name":"bad ttl","language":"python","code":"def main():\n return 1","ttl":"99z"}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "400" ] && pass "invalid ttl -> 400" || fail "invalid ttl -> got $CODE"
R=$(create_call "$NEG_SUB" '{"name":"bad name fn","language":"python","code":"def main():\n return 1"}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" != "201" ] && [ "$CODE" != "200" ] && pass "invalid name with spaces rejected" || fail "invalid name unexpectedly created"
R=$(create_call "$NEG_SUB" '{"name":"bad-lang","language":"cobol","code":"x"}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "400" ] && pass "invalid language -> 400" || fail "invalid language -> got $CODE"
echo ""
echo ">>> T04: duplicate create и повторный delete"
R=$(create_call "$NEG_SUB" '{"name":"dup-ui","language":"python","entrypoint":"main.main","code":"def main():\n return \"dup-ui\""}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "201" ] && pass "first create -> 201" || fail "first create -> got $CODE"
R=$(create_call "$NEG_SUB" '{"name":"dup-ui","language":"python","entrypoint":"main.main","code":"def main():\n return \"dup-ui-2\""}')
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-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-Test-Sub: ${NEG_SUB}")
[ "$DC2" = "404" ] && pass "second delete -> 404" || fail "second delete -> got $DC2"
echo ""
echo ">>> T05: Python create/invoke как из UI"
R=$(create_call "$LANG_SUB" '{"name":"py-ui","language":"python","entrypoint":"main.main","code":"def main():\n return \"py-ui-ok\""}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "201" ] && pass "python create -> 201" || fail "python create -> got $CODE"
if wait_invoke_ok "$LANG_SUB" "py-ui" "py-ui-ok" 8 5; then
pass "python invoke -> 200"
else
fail "python invoke did not stabilize"
fi
PY_PAR1=$(invoke_call "$LANG_SUB" "py-ui")
PY_PAR2=$(invoke_call "$LANG_SUB" "py-ui")
[ "$(json_field "$PY_PAR1" status)" = "200" ] && [ "$(json_field "$PY_PAR2" status)" = "200" ] && pass "python double invoke -> both 200" || fail "python double invoke did not return two 200s"
echo ""
echo ">>> T06: NodeJS create/invoke и edit/update"
R=$(create_call "$NODE_SUB" '{"name":"node-ui","language":"nodejs","entrypoint":"main","code":"module.exports = async function() { return { status: 200, body: \"node-ui-ok\" }; }"}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "201" ] && pass "nodejs create -> 201" || fail "nodejs create -> got $CODE"
if wait_invoke_ok "$NODE_SUB" "node-ui" "node-ui-ok" 16 5; then
pass "nodejs invoke -> 200"
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-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"
if wait_invoke_ok "$NODE_SUB" "node-ui" "node-ui-updated" 16 5; then
pass "nodejs invoke after edit -> updated body"
else
fail "nodejs invoke after edit did not return updated body"
fi
echo ""
echo ">>> T07: PHP и Ruby в одном namespace"
PHP_CODE=$(python3 -c 'import json; print(json.dumps("<?php\nfunction handler($context) {\n $context[\"response\"]->getBody()->write(\"php-ui-ok\");\n}"))')
RUBY_CODE=$(python3 -c 'import json; print(json.dumps("def handler\n \"ruby-ui-ok\"\nend"))')
R=$(create_call "$STACK_SUB" "{\"name\":\"php-ui\",\"language\":\"php\",\"entrypoint\":\"main.php::handler\",\"code\":${PHP_CODE}}")
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "201" ] && pass "php create -> 201" || fail "php create -> got $CODE"
if wait_invoke_ok "$STACK_SUB" "php-ui" "php-ui-ok" 24 10; then
pass "php invoke -> 200"
else
fail "php invoke did not stabilize"
fi
R=$(create_call "$STACK_SUB" "{\"name\":\"ruby-ui\",\"language\":\"ruby\",\"entrypoint\":\"handler\",\"code\":${RUBY_CODE}}")
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "201" ] && pass "ruby create -> 201" || fail "ruby create -> got $CODE"
if wait_invoke_ok "$STACK_SUB" "ruby-ui" "ruby-ui-ok" 24 10; then
pass "ruby invoke -> 200"
else
fail "ruby invoke did not stabilize"
fi
echo ""
echo ">>> T08: плохой entrypoint как пользовательский промах"
R=$(create_call "$NEG_SUB" '{"name":"node-bad-entry","language":"nodejs","entrypoint":"missing","code":"module.exports = async function() { return { status: 200, body: \"bad\" }; }"}')
CODE=$(printf '%s' "$R" | tail -1)
[ "$CODE" = "201" ] && pass "bad entrypoint function created" || fail "bad entrypoint create -> got $CODE"
RESP=$(invoke_call "$NEG_SUB" "node-bad-entry")
STATUS=$(json_field "$RESP" status)
ERR=$(json_field "$RESP" error)
[ "$STATUS" != "200" ] && pass "bad entrypoint invoke fails as expected" || fail "bad entrypoint unexpectedly invoked"
printf '%s' "$ERR" | grep -Eiq 'timeout|specialization|error|not found' && pass "bad entrypoint error is visible" || fail "bad entrypoint error text weak: ${ERR}"
echo ""
echo "========================================"
echo " RESULT: PASS=${PASS} FAIL=${FAIL}"
echo "========================================"
[ "$FAIL" -eq 0 ]
-783
View File
@@ -1,783 +0,0 @@
#!/usr/bin/env bash
set -uo pipefail
BASE="https://fission.kube5s.ru/console/api"
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
NS_CLEANUP_SCRIPT="${ROOT_DIR}/cleanup_test_namespaces.sh"
PASS=0; FAIL=0
RUN_ID=$(date +%s%N 2>/dev/null || date +%s)$(od -An -N2 -tu2 /dev/urandom 2>/dev/null | tr -d ' ' || echo "$$")
RUN_ID=$(echo "$RUN_ID" | sha256sum 2>/dev/null | head -c 8 || echo "${RUN_ID}" | cut -c1-8)
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")
local uid
for uid in $(seq 1 10); do
subs+=("u${uid}-${RUN_ID}@test.local")
done
for uid in $(seq 1 3); do
subs+=("fullcycle-u${uid}-${RUN_ID}@test.local")
done
if [ -x "$NS_CLEANUP_SCRIPT" ]; then
echo ""
echo ">>> Cleanup test namespaces..."
"$NS_CLEANUP_SCRIPT" "${subs[@]}" || true
fi
}
trap cleanup_namespaces EXIT
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 HTTP $got, want $want"; }
echo ""
echo "========================================"
echo " fission-console v0.6.8 — ТЕСТ v2"
echo "========================================"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК A: Фиксы v0.6.8 — регресс"
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions/nonexistent-xyz-abc/invoke \
-H "X-Test-Sub: 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-Test-Sub: 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-Test-Sub: 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-Test-Sub: 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)
[ "$ROUTE_A" != "$ROUTE_B" ] && pass "Route isolation: маршруты различаются ($ROUTE_A vs $ROUTE_B)" || fail "Route collision: маршруты одинаковы: $ROUTE_A"
sleep 10
# Invoke после изоляции
INV_A=$(curl -s -X POST ${BASE}/functions/${ISO_NAME}/invoke \
-H "X-Test-Sub: routetest-alice@test.local" -H "Content-Type: application/json" -d '{}')
INV_B=$(curl -s -X POST ${BASE}/functions/${ISO_NAME}/invoke \
-H "X-Test-Sub: 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"
[ "$BODY_B" = "bob-iso" ] && pass "Route isolation invoke — bob получает bob" || fail "Route isolation — bob получил: $BODY_B"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК B: Валидация входных данных"
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
-H "X-Test-Sub: 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-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-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-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-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)
( [ "$CODE" = "400" ] || [ "$CODE" = "422" ] || [ "$CODE" = "502" ] ) && pass "Имя с пробелами → не 201 (got $CODE)" || fail "Имя с пробелами → got $CODE"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК C: Дублирующее создание"
curl -s -X POST ${BASE}/functions \
-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-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"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК D: UPDATE кода и повторный invoke"
curl -s -X POST ${BASE}/functions \
-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-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-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"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК E: GET функции"
curl -s -X POST ${BASE}/functions \
-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-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-Test-Sub: get-user@test.local")
CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "GET несуществующей → 404"
# ──────────────────────────────────────────────
echo ""
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-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-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-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-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"
[ "$COUNT_U2" -ge 1 ] && [ "$COUNT_U2" -lt "$COUNT_U1" ] 2>/dev/null && pass "LIST изоляция: u2 видит $COUNT_U2 (меньше чем u1)" || fail "LIST u2 — $COUNT_U2 (u1=$COUNT_U1)"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК G: Namespace изоляция DELETE"
# Carol пытается удалить функцию Alice
curl -s -X POST ${BASE}/functions \
-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-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-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-Test-Sub: alice-del@test.local")
CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "Alice повторный DELETE → 404"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК H: Python — разные форматы кода"
# Формат 1: def main() без аргумента — Fission Python env вызывает main() без аргументов
curl -s -X POST ${BASE}/functions \
-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-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)"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК I: NodeJS — несколько export-паттернов"
# module.exports = async function
curl -s -X POST ${BASE}/functions \
-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-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-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-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
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК J: TTL — создание и проверка"
curl -s -X POST ${BASE}/functions \
-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-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-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"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК K: Invoke — тело запроса передаётся в функцию"
# ctx.request.body может быть undefined — тестируем через ctx.request.url (всегда доступен)
curl -s -X POST ${BASE}/functions \
-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-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)
[ "$STATUS" = "200" ] && pass "NodeJS ctx.request.url доступен — invoke 200" || fail "Echo url — status=$STATUS resp: $RESP"
echo "$RESP" | grep -q "got:" && pass "NodeJS ctx.request.url содержит данные" || fail "ctx.request.url — ответ: $RESP"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК L: Stress — 10 параллельных invoke"
curl -s -X POST ${BASE}/functions \
-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-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 &
wait
# Run them
STRESS_OK=0
PIDS=()
TMPDIR_S=$(mktemp -d)
for i in $(seq 1 10); do
( curl -s -X POST ${BASE}/functions/stress-fn/invoke \
-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
wait
STRESS_OK=$(ls ${TMPDIR_S}/ok_* 2>/dev/null | wc -l)
rm -rf "${TMPDIR_S}"
[ "$STRESS_OK" -ge 9 ] && pass "Stress 10 parallel: $STRESS_OK/10 OK" || fail "Stress 10 parallel: $STRESS_OK/10 OK"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК M: LIST пустого namespace"
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"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК N: DELETE полная цепочка (fn + pkg + trigger)"
curl -s -X POST ${BASE}/functions \
-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-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-Test-Sub: chain-user@test.local")
CODE=$(echo "$R" | tail -1); check_http "$CODE" "200" "DELETE chain-fn → 200"
# Ждём propagation удаления в k8s
sleep 3
# k8s: проверяем через kubectl (или через API если kubectl недоступен)
FN_RAW=$(kubectl get functions.fission.io chain-fn -n ${NS_CHAIN} 2>&1)
if echo "$FN_RAW" | grep -q "not found"; then
pass "k8s: Function удалена после DELETE"
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-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"
fi
TRG_EXISTS=$(kubectl get httptriggers.fission.io -n ${NS_CHAIN} 2>&1 | grep -c "chain-fn" || true)
# После удаления GET должен вернуть 404
R=$(curl -s -w "\n%{http_code}" ${BASE}/functions/chain-fn \
-H "X-Test-Sub: chain-user@test.local")
CODE=$(echo "$R" | tail -1); check_http "$CODE" "404" "GET после DELETE → 404"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК O: Auth — без заголовка"
R=$(curl -s -w "\n%{http_code}" ${BASE}/functions)
CODE=$(echo "$R" | tail -1)
( [ "$CODE" = "401" ] || [ "$CODE" = "403" ] ) && pass "Без X-Test-Sub → 401/403 (got $CODE)" || fail "Без X-Test-Sub → got $CODE"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК P: /console/api/auth"
# POST /auth без токена → 400 bad request
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/auth \
-H "Content-Type: application/json" -d '{"token":""}')
CODE=$(echo "$R" | tail -1); BODY=$(echo "$R" | head -1)
check_http "$CODE" "400" "POST /auth без токена → 400"
echo "$BODY" | grep -q "token\|required\|invalid" && pass "/auth — тело содержит ошибку токена" || fail "/auth — тело: $BODY"
# POST /auth с невалидным токеном → 401
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/auth \
-H "Content-Type: application/json" -d '{"token":"not.a.real.jwt.token"}')
CODE=$(echo "$R" | tail -1)
check_http "$CODE" "401" "POST /auth с невалидным токеном → 401"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК Q: Дубликат → 409, невалидное имя → 400"
# Создаём функцию, потом снова с тем же именем → 409
curl -s -X POST ${BASE}/functions \
-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-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-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-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-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-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)"
# Имя 64 символа → 400
LONGNAME=$(python3 -c "print('a'*64)")
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
-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"
# Имя 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-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 символа)"
# Имя 57 символов → принято (route = 63 символа, ровно лимит Fission)
NAME57=$(python3 -c "print('a'*57)")
# Первый прогрев namespace
curl -s -X POST ${BASE}/functions \
-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-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)"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК R: Лимит размера кода 1MB"
# Код > 1MB → 400
BIG_TMP=$(mktemp)
python3 -c "
import json, sys
code = 'x' * (1024*1024 + 1)
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-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)
# 400 = наш код, 413 = nginx body limit, 000 = connection reset nginx — всё означает "отклонён"
( [ "$CODE" = "400" ] || [ "$CODE" = "413" ] || [ "$CODE" = "000" ] ) \
&& pass "Код > 1MB → отклонён (HTTP $CODE)" \
|| fail "Код > 1MB → got HTTP $CODE (ожидался 400/413)"
# Код ровно 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-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"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК S: TTL expire + reaper очистка"
# Создаём функцию с TTL=1m
R=$(curl -s -X POST ${BASE}/functions \
-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-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"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК T: Параллельное создание одного namespace (race condition)"
# 5 одновременных запросов от одного пользователя (первый раз)
NS_USER="race-${RUN_ID}@test.local"
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-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-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"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК U: 10 разных пользователей создают функции параллельно"
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-Test-Sub: ${U}" -H "Content-Type: application/json" \
-d "{\"name\":\"fn-${uid}\",\"language\":\"nodejs\",\"code\":\"${CODE}\"}" > /dev/null
) &
done
wait
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-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"
# ──────────────────────────────────────────────
echo ""
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-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)
OTHER_COUNT=$(echo "$LIST" | python3 -c "import sys,json; fns=json.load(sys.stdin); print(sum(1 for f in fns if f['metadata']['name'].startswith('fn-') and f['metadata']['name']!='fn-${uid}'))" 2>/dev/null)
if [ "${MY_COUNT:-0}" -ge 1 ] && [ "${OTHER_COUNT:-0}" -eq 0 ]; then
ISOLATION_OK=$((ISOLATION_OK+1))
fi
done
[ "$ISOLATION_OK" -ge 8 ] && pass "Изоляция 10 юзеров: ${ISOLATION_OK}/10 видят только своё" || fail "Изоляция 10 юзеров: только ${ISOLATION_OK}/10"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК W: 5 юзеров одновременно вызывают свои функции"
for uid in $(seq 1 5); do
(
U="u${uid}-${RUN_ID}@test.local"
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}" \
"https://fission.kube5s.ru/console/proxy/${NS12}/fn-${uid}")
echo "U${uid}:${INVOKE_CODE}"
else
echo "U${uid}:NO_ROUTE"
fi
) &
done
INVOKE_RESULTS=$(wait; sleep 2)
# Просто проверяем что invoke не упал совсем (ждём фоновые процессы)
sleep 3
pass "5 юзеров параллельный invoke — завершён без паники"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК X: 3 юзера — полный цикл CREATE→LIST→INVOKE→DELETE"
FULL_CYCLE_OK=0
for uid in $(seq 1 3); do
U="fullcycle-u${uid}-${RUN_ID}@test.local"
FN="fc-fn-${uid}"
CODE="module.exports=async()=>({body:'cycle${uid}'})"
# CREATE
CR=$(curl -s -o /dev/null -w "%{http_code}" -X POST ${BASE}/functions \
-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-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-Test-Sub: ${U}")
# LIST после DELETE — функции нет
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
FULL_CYCLE_OK=$((FULL_CYCLE_OK+1))
else
echo " [U${uid}] CR=$CR LC=$LC DC=$DC LC2=$LC2"
fi
done
[ "$FULL_CYCLE_OK" -eq 3 ] && pass "3 юзера полный цикл CREATE→LIST→DELETE: ${FULL_CYCLE_OK}/3" || fail "3 юзера полный цикл: только ${FULL_CYCLE_OK}/3"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК Y: Один юзер создаёт 10 функций, проверяем LIST и DELETE all"
MFUNC_USER="mfunc-${RUN_ID}@test.local"
for i in $(seq 1 10); do
curl -s -X POST ${BASE}/functions \
-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-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-Test-Sub: ${MFUNC_USER}" > /dev/null &
done
wait
sleep 3
MFUNC_AFTER=$(curl -s ${BASE}/functions \
-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:-?} функций"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК Z: Cross-user attack — юзер пытается DELETE/UPDATE/GET чужую функцию"
VICTIM_USER="victim-${RUN_ID}@test.local"
ATTACKER_USER="attacker-${RUN_ID}@test.local"
# Жертва создаёт функцию
curl -s -X POST ${BASE}/functions \
-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-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-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-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-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: функция жертвы исчезла!"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК AA: PHP — создание и вызов"
# Оба блока (AA/AB) используют ОДИН namespace, чтобы избежать повторных executor rollouts.
# Первая функция (PHP) вызывает addNSToFission → executor rollout (~60s).
# Ruby создаётся в том же namespace → rollout не нужен → пул стартует сразу.
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-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"
# Ждём пока poolmgr поднимет pod для нового env (retry до 200s).
# После executor rollout (~60s) + startup pod (~30s) = ~90s до первого invoke.
STATUS=""; RAW=""
for _try in $(seq 1 20); do
sleep 10
INV=$(curl -s -X POST ${BASE}/functions/php-fn/invoke \
-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
done
[ "$STATUS" = "200" ] && pass "PHP: invoke → 200" || fail "PHP: invoke → got $STATUS (body: $RAW)"
echo "$RAW" | grep -q "php-ok" && pass "PHP: тело содержит php-ok" || fail "PHP: тело не содержит php-ok (got: $RAW)"
# ──────────────────────────────────────────────
echo ""
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-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"
# Ждём пока poolmgr поднимет pod (retry до 200s).
# Namespace уже известен executor после PHP-блока → rollout не нужен → пул стартует быстро.
STATUS=""; RAW=""
for _try in $(seq 1 20); do
sleep 10
INV=$(curl -s -X POST ${BASE}/functions/ruby-fn/invoke \
-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
done
[ "$STATUS" = "200" ] && pass "Ruby: invoke → 200" || fail "Ruby: invoke → got $STATUS (body: $RAW)"
echo "$RAW" | grep -q "ruby-ok" && pass "Ruby: тело содержит ruby-ok" || fail "Ruby: тело не содержит ruby-ok (got: $RAW)"
# ──────────────────────────────────────────────
echo ""
echo ">>> БЛОК AD: Невалидный язык → 400"
R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \
-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"
# ──────────────────────────────────────────────
echo ""
# ──────────────────────────────────────────────
echo ""
echo "========================================"
printf "Итого: PASS=%d FAIL=%d\n" $PASS $FAIL
echo "========================================"