From d21e1ac9a514e75e335fc6087ce45cf6122a0d28 Mon Sep 17 00:00:00 2001 From: Naeel Date: Fri, 1 May 2026 19:53:40 +0300 Subject: [PATCH] feat: restore cron page, email in navbar, metrics pipeline v1.3.19 --- .github/copilot-instructions.md | 57 ++- console/deploy/console.yaml | 2 +- console/internal/api/cron_metrics.go | 203 +++++++++ console/internal/api/server.go | 9 + console/ui/cron.go | 16 + console/ui/cron.html | 430 ++++++++++++++++++ console/ui/embed.go | 2 +- console/ui/index.html | 35 +- doc/progress-2026-04-30-archive-upload.md | 36 ++ .../big-suite/code/python-metrics/croned.py | 73 +++ test_simple_function.sh | 50 -- tests_v2.sh | 34 -- token.txt | 11 +- 13 files changed, 848 insertions(+), 110 deletions(-) create mode 100644 console/internal/api/cron_metrics.go create mode 100644 console/ui/cron.go create mode 100644 console/ui/cron.html create mode 100644 doc/progress-2026-04-30-archive-upload.md create mode 100644 examples/big-suite/code/python-metrics/croned.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 67f3b40..0dde2d4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,18 +1,51 @@ # Правила ## ⛔ ОТВЕЧАТЬ КРАТКО — АБСОЛЮТНОЕ ПРАВИЛО -- Вопрос → короткий ответ → СТОП. Не рассуждать, не исследовать без команды. -- НЕ делать ничего "попутно" без явной просьбы. Сделал — стоп — ждёшь команды. -- Без приветствий и лишних рассуждений. Код — только по запросу. Не бормотать. - -> Подробные правила: [`.github/pravila.md`](pravila.md) +- Вопрос → короткий ответ → СТОП. +- Ничего лишнего. +- Код — только по запросу. 1. Не трогать рабочий код без явного указания. -2. Файлы редактировать локально — `~/remote_dev/` = `~/terra/` на ВМ (sshfs), SCP не нужен. - Перед любым запуском команд проверять, что локальные файлы и содержимое на ВМ синхронизированы; при сомнении ориентироваться на состояние файлов на ВМ. -3. Все команды — **только через SSH**, никогда локально: - ```bash + +2. Файлы редактируются локально: + ~/fission + + После ЛЮБЫХ изменений ОБЯЗАТЕЛЬНО синхронизировать на ВМ командой: + rsync -az \ + -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \ + ~/fission/ \ + naeel@5.172.178.213:~/terra/fission/ + +3. Все команды выполнять ТОЛЬКО через SSH: ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА' - ``` -4. **ЗАПРЕЩЕНО** откатывать код, версии, образы и рабочее состояние без явной команды пользователя. -5. После каждого исправления делать отдельный commit через SSH. + + - не выполнять команды локально + - не открывать интерактивные сессии + - не делать цепочки без необходимости + +4. Перед запуском команд ОБЯЗАТЕЛЬНО убедиться, что синхронизация выполнена. + +5. ЗАПРЕЩЕНО: + - откатывать код + - менять версии + - ломать рабочее состояние + +6. После каждого исправления: + - синхронизация (rsync) + - затем commit через SSH + +## ⛔⛔⛔ ДЕЛАТЬ ТОЛЬКО ЧТО ПРЯМО ПРИКАЗАНО + +**АБСОЛЮТНЫЙ ЗАПРЕТ на додумывание:** +- Не расширять масштаб работы +- Не выполнять "логичные следующие шаги" +- Не инициировать дополнительные операции +- Не делать ничего кроме того что сказано + +**Пример (2026-05-01):** +- Приказано: "собери" +- Сделано: ✓ собрал образы v1.3.17 и v0.1.2 +- СТОП — жду команды дальше +- **ЗАПРЕЩЕНО:** обновлять манифесты, заливать образы, применять на кластер, запускать тесты + +**Исключение:** только если приказ явно включает цепочку ("собери И залей И тесты") diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml index 24a8b1e..14d0acd 100644 --- a/console/deploy/console.yaml +++ b/console/deploy/console.yaml @@ -52,7 +52,7 @@ spec: serviceAccountName: fission-console containers: - name: console - image: naeel/fission-console:v1.3.12 + image: naeel/fission-console:v1.3.19 ports: - containerPort: 8090 env: diff --git a/console/internal/api/cron_metrics.go b/console/internal/api/cron_metrics.go new file mode 100644 index 0000000..5869b9b --- /dev/null +++ b/console/internal/api/cron_metrics.go @@ -0,0 +1,203 @@ +package api + +import ( + "encoding/json" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" +) + +type CronMetric struct { + Timestamp string `json:"timestamp"` + Source string `json:"source"` + Hostname string `json:"hostname,omitempty"` + Status string `json:"status,omitempty"` + MemoryTotalMB float64 `json:"memory_total_mb,omitempty"` + MemoryFreeMB float64 `json:"memory_free_mb,omitempty"` + MemoryAvailMB float64 `json:"memory_available_mb,omitempty"` + MemoryUsedMB float64 `json:"memory_used_mb,omitempty"` + MemoryPercent float64 `json:"memory_percent,omitempty"` + CpuLoad1m float64 `json:"cpu_load_1m,omitempty"` + CpuLoad5m float64 `json:"cpu_load_5m,omitempty"` + CpuLoad15m float64 `json:"cpu_load_15m,omitempty"` + DiskTotalGB float64 `json:"disk_total_gb,omitempty"` + DiskFreeGB float64 `json:"disk_free_gb,omitempty"` + DiskUsedGB float64 `json:"disk_used_gb,omitempty"` + UptimeSec float64 `json:"uptime_sec,omitempty"` + Notes string `json:"notes,omitempty"` +} + +var ( + cronMetricMu sync.RWMutex + cronMetricHistory []CronMetric + cronMetricMaxKeep = 240 +) + +func (s *Server) handleCronMetrics(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + cronMetricMu.RLock() + history := append([]CronMetric(nil), cronMetricHistory...) + cronMetricMu.RUnlock() + latest := CronMetric{} + if len(history) > 0 { + latest = history[len(history)-1] + } + writeAnyJSON(w, http.StatusOK, map[string]any{ + "count": len(history), + "latest": latest, + "history": history, + }) + case http.MethodPost: + if !allowCronIngest(r) { + writeJSONError(w, http.StatusUnauthorized, "invalid cron token") + return + } + var payload map[string]any + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + writeJSONError(w, http.StatusBadRequest, "invalid json payload") + return + } + sample := normalizeCronMetric(payload) + cronMetricMu.Lock() + cronMetricHistory = append(cronMetricHistory, sample) + if len(cronMetricHistory) > cronMetricMaxKeep { + cronMetricHistory = append([]CronMetric(nil), cronMetricHistory[len(cronMetricHistory)-cronMetricMaxKeep:]...) + } + count := len(cronMetricHistory) + cronMetricMu.Unlock() + writeAnyJSON(w, http.StatusOK, map[string]any{"ok": true, "stored": count, "latest": sample}) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func allowCronIngest(r *http.Request) bool { + expected := strings.TrimSpace(os.Getenv("CRON_TOKEN")) + if expected == "" { + return true + } + return strings.TrimSpace(r.Header.Get("X-Cron-Token")) == expected +} + +func normalizeCronMetric(payload map[string]any) CronMetric { + now := time.Now().UTC().Format(time.RFC3339) + metric := CronMetric{ + Timestamp: now, + Source: textValue(payload, "source", "function", "name", "host", "hostname", "instance"), + Hostname: textValue(payload, "hostname", "host", "node", "instance"), + Status: textValue(payload, "status"), + Notes: textValue(payload, "notes", "note", "message"), + } + if metric.Source == "" { + metric.Source = "cron" + } + + metric.MemoryTotalMB = numberValue(payload, "memory_total_mb", "total_memory_mb", "mem_total_mb", "mem_total") + metric.MemoryFreeMB = numberValue(payload, "memory_free_mb", "free_memory_mb", "mem_free_mb", "free_mb") + metric.MemoryAvailMB = numberValue(payload, "memory_available_mb", "available_memory_mb", "mem_available_mb", "avail_memory_mb") + metric.MemoryUsedMB = numberValue(payload, "memory_used_mb", "used_memory_mb", "mem_used_mb", "used_mb") + metric.MemoryPercent = numberValue(payload, "memory_percent", "mem_percent", "memory_usage_percent") + if metric.MemoryUsedMB == 0 && metric.MemoryTotalMB > 0 && metric.MemoryFreeMB > 0 { + metric.MemoryUsedMB = metric.MemoryTotalMB - metric.MemoryFreeMB + } + if metric.MemoryAvailMB == 0 && metric.MemoryFreeMB > 0 { + metric.MemoryAvailMB = metric.MemoryFreeMB + } + if metric.MemoryPercent == 0 && metric.MemoryTotalMB > 0 && metric.MemoryUsedMB > 0 { + metric.MemoryPercent = (metric.MemoryUsedMB / metric.MemoryTotalMB) * 100 + } + + metric.CpuLoad1m = numberValue(payload, "cpu_load_1m", "loadavg_1m", "load_1m", "load1") + metric.CpuLoad5m = numberValue(payload, "cpu_load_5m", "loadavg_5m", "load_5m", "load5") + metric.CpuLoad15m = numberValue(payload, "cpu_load_15m", "loadavg_15m", "load_15m", "load15") + + metric.DiskTotalGB = numberValue(payload, "disk_total_gb", "total_disk_gb", "disk_total_mb", "disk_total") + metric.DiskFreeGB = numberValue(payload, "disk_free_gb", "free_disk_gb", "disk_free_mb", "disk_free") + metric.DiskUsedGB = numberValue(payload, "disk_used_gb", "used_disk_gb", "disk_used_mb", "disk_used") + if metric.DiskUsedGB == 0 && metric.DiskTotalGB > 0 && metric.DiskFreeGB > 0 { + metric.DiskUsedGB = metric.DiskTotalGB - metric.DiskFreeGB + } + + if uptime := numberValue(payload, "uptime_sec", "uptime", "uptime_seconds"); uptime > 0 { + metric.UptimeSec = uptime + } + if ts := textValue(payload, "timestamp", "ts", "collected_at"); ts != "" { + metric.Timestamp = ts + } + return metric +} + +func textValue(payload map[string]any, keys ...string) string { + for _, key := range keys { + if value, ok := payload[key]; ok { + text := strings.TrimSpace(toString(value)) + if text != "" { + return text + } + } + } + return "" +} + +func numberValue(payload map[string]any, keys ...string) float64 { + for _, key := range keys { + if value, ok := payload[key]; ok { + if number, ok := toFloat64(value); ok { + return number + } + } + } + return 0 +} + +func toString(value any) string { + switch v := value.(type) { + case string: + return v + case []byte: + return string(v) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case int: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case json.Number: + return v.String() + default: + return "" + } +} + +func toFloat64(value any) (float64, bool) { + switch v := value.(type) { + case float64: + return v, true + case float32: + return float64(v), true + case int: + return float64(v), true + case int64: + return float64(v), true + case json.Number: + f, err := v.Float64() + return f, err == nil + case string: + if strings.TrimSpace(v) == "" { + return 0, false + } + n, err := strconv.ParseFloat(strings.TrimSpace(v), 64) + if err == nil { + return n, true + } + return 0, false + default: + return 0, false + } +} diff --git a/console/internal/api/server.go b/console/internal/api/server.go index 8da9770..16c2a46 100644 --- a/console/internal/api/server.go +++ b/console/internal/api/server.go @@ -127,6 +127,15 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) { // Auth: не требует токена — сам проверяет и возвращает namespace mux.HandleFunc("/console/api/auth", s.handleAuth) + // Cron: публичные маршруты для метрик + cronHandler := ui.CronHandler() + mux.Handle("/cron", cronHandler) + mux.Handle("/cron/", cronHandler) + mux.Handle("/console/cron", cronHandler) + mux.Handle("/console/cron/", cronHandler) + mux.HandleFunc("/cron/api/metrics", s.handleCronMetrics) + mux.HandleFunc("/console/cron/api/metrics", s.handleCronMetrics) + // Приватные: требуют X-Auth-Token / X-Test-Sub auth := s.authMiddleware diff --git a/console/ui/cron.go b/console/ui/cron.go new file mode 100644 index 0000000..3764e24 --- /dev/null +++ b/console/ui/cron.go @@ -0,0 +1,16 @@ +package ui + +import ( + _ "embed" + "net/http" +) + +//go:embed cron.html +var cronPage string + +func CronHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write([]byte(cronPage)) + }) +} diff --git a/console/ui/cron.html b/console/ui/cron.html new file mode 100644 index 0000000..d7ac743 --- /dev/null +++ b/console/ui/cron.html @@ -0,0 +1,430 @@ + + + + + + NUBES Cron + + + + + +
+
+
+
+

Снимки состояния по cron

+

+ Python-функция по расписанию отправляет сюда snapshot со свободной памятью, CPU load, диском и uptime. + Страница показывает последний снимок и историю замеров в том же стиле, что и основной dashboard: карточки + таблица. +

+ Примечание: метрики читаются из /proc внутри pod-а и отражают состояние + ноды кластера, на которой запущена функция — не ресурсы самого pod-а и не агрегированные показатели кластера. +

+
+
+ Current time + --:--:-- +
+
+
+
+ 0 points +
+
+ +
+
+ +
+
+
+

История

+
Ожидание первых данных…
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
ВремяИсточникХостСвоб. МБДост. МБИсп. МБПамять %ЦП 1мЦП 5мЦП 15мДиск своб.АптаймСтатус
Данных пока нет
+
+ +
+ + +
+ + + + diff --git a/console/ui/embed.go b/console/ui/embed.go index 20ef7f6..69baf15 100644 --- a/console/ui/embed.go +++ b/console/ui/embed.go @@ -5,7 +5,7 @@ import ( "net/http" ) -//go:embed index.html +//go:embed index.html cron.html var content embed.FS func Handler() http.Handler { diff --git a/console/ui/index.html b/console/ui/index.html index 6333ecf..35a314f 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -406,7 +406,7 @@ @@ -1246,6 +1245,7 @@ } localStorage.setItem('auth_token', token); localStorage.setItem('auth_env', env); + setUserAvatar(token); hideLoginOverlay(); // Проверяем статус NS — если не ready, показываем init overlay try { @@ -1265,10 +1265,37 @@ } } + function jwtEmail(token) { + try { + var payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); + return payload.email || payload.sub || null; + } catch (_) { return null; } + } + + function setUserAvatar(token) { + var el = document.getElementById('user-avatar'); + if (!el) return; + var email = token ? jwtEmail(token) : null; + if (email) { + el.textContent = email; + el.title = email; + el.style.fontSize = '0.7rem'; + el.style.minWidth = '60px'; + el.style.padding = '0 8px'; + } else { + el.textContent = 'N'; + el.title = ''; + el.style.fontSize = ''; + el.style.minWidth = ''; + el.style.padding = ''; + } + } + function doLogout() { localStorage.removeItem('auth_token'); localStorage.removeItem('auth_env'); try { document.getElementById('l-token').value = ''; } catch (_) { } + setUserAvatar(null); showLoginOverlay(); } @@ -1480,10 +1507,12 @@ } function checkAuth() { - if (!localStorage.getItem('auth_token')) { + var storedToken = localStorage.getItem('auth_token'); + if (!storedToken) { showLoginOverlay(); return; } + setUserAvatar(storedToken); hideLoginOverlay(); reloadAll(); } diff --git a/doc/progress-2026-04-30-archive-upload.md b/doc/progress-2026-04-30-archive-upload.md new file mode 100644 index 0000000..5731655 --- /dev/null +++ b/doc/progress-2026-04-30-archive-upload.md @@ -0,0 +1,36 @@ +# План интеграции загрузки функций из архивов в UI Fission Console + +## Цель +Добавить в web-интерфейс Fission Console возможность загружать функцию в виде архива (zip/tar), аналогично `fission function create --deploy ...`. + +## Основные шаги +1. UI: добавить компонент для выбора и загрузки архива (drag&drop или file input). +2. Backend: реализовать обработку архива, валидацию структуры, запуск линтеров и (опционально) AI-проверку. +3. Интеграция с Fission API: отправка архива через REST (multipart/form-data) для создания/обновления функции. +4. UI: отображение статуса загрузки, ошибок, логов билда. + +## Валидация архива +- Проверка наличия entrypoint-файла (main.py/handler.js и т.д.) +- Проверка структуры (отсутствие мусора, секретов) +- Линтеры (flake8/pylint/go vet и т.д.) +- (Опционально) AI-проверка через облачный LLM + +## Безопасность +- Ограничить размер архива +- Фильтрация запрещённых файлов +- Логирование действий + +## Синхронизация с Terraform/CLI +- Архивы, деплоенные через UI, CLI, Terraform — одинакового формата +- UI всегда отображает актуальный список функций из Fission API + +## Возможные сложности +- Ошибки структуры архива (нет entrypoint) +- Долгая сборка/деплой больших архивов +- Drift при одновременном управлении через Terraform и UI + +## Следующие шаги +- Реализовать UI-компонент для загрузки архива +- Прототип backend-валидации +- Интеграция с Fission API +- Документировать пайплайн и типовые ошибки diff --git a/examples/big-suite/code/python-metrics/croned.py b/examples/big-suite/code/python-metrics/croned.py new file mode 100644 index 0000000..2091d58 --- /dev/null +++ b/examples/big-suite/code/python-metrics/croned.py @@ -0,0 +1,73 @@ +import os +import urllib.request +import urllib.error +import json + +COLLECTOR_URL = "http://metrics-collector.fission.svc.cluster.local:8091/metrics" + +def main(): + payload = { + "source": "fission-cron-metrics", + "hostname": os.uname().nodename, + "status": "ok", + } + + # CPU load + try: + load = os.getloadavg() + payload["cpu_load_1m"] = load[0] + payload["cpu_load_5m"] = load[1] + payload["cpu_load_15m"] = load[2] + except Exception: + pass + + # Memory + try: + with open("/proc/meminfo") as f: + meminfo = {} + for line in f: + k, v = line.split(":") + meminfo[k.strip()] = int(v.split()[0]) + total = meminfo.get("MemTotal", 0) / 1024 + free = meminfo.get("MemFree", 0) / 1024 + avail = meminfo.get("MemAvailable", 0) / 1024 + used = total - free + payload["memory_total_mb"] = round(total, 1) + payload["memory_free_mb"] = round(free, 1) + payload["memory_available_mb"] = round(avail, 1) + payload["memory_used_mb"] = round(used, 1) + payload["memory_percent"] = round(used / total * 100, 1) if total else 0 + except Exception: + pass + + # Disk + try: + st = os.statvfs("/") + total_gb = st.f_blocks * st.f_frsize / 1024**3 + free_gb = st.f_bfree * st.f_frsize / 1024**3 + used_gb = total_gb - free_gb + payload["disk_total_gb"] = round(total_gb, 1) + payload["disk_free_gb"] = round(free_gb, 1) + payload["disk_used_gb"] = round(used_gb, 1) + except Exception: + pass + + # Uptime + try: + with open("/proc/uptime") as f: + payload["uptime_sec"] = float(f.read().split()[0]) + except Exception: + pass + + data = json.dumps(payload).encode() + req = urllib.request.Request( + COLLECTOR_URL, + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=5) as resp: + return {"ok": True, "status": resp.status} + except urllib.error.URLError as e: + return {"ok": False, "error": str(e)} diff --git a/test_simple_function.sh b/test_simple_function.sh index d8cef5c..8e51ec4 100755 --- a/test_simple_function.sh +++ b/test_simple_function.sh @@ -158,21 +158,6 @@ sub main { EOF } -write_go() { - local dir=$1 - mkdir -p "${dir}" - cat > "${dir}/handler.go" <<'EOF' -package main - -import ( - "encoding/json" - "net/http" -) - -func Handler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"msg": "hello-go"}) -} EOF cat > "${dir}/go.mod" <<'EOF' module fn @@ -260,41 +245,6 @@ kube_exists "environments.fission.io" "simple-python-env" \ 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 ">>> T03: go — source pipeline, create с URL" -# ════════════════════════════════════════════════════════════════════════ -T03="${RUN_ID}-go-basic" -D=$(setup_workdir "t03") -write_provider "${D}" -write_go "${D}/code" -cat >> "${D}/main.tf" </dev/null || echo "") -[ "${PKG_SOURCE}" = "literal" ] \ - && pass "T03: Package.spec.source.type=literal (source deploy)" \ - || fail "T03: Package.spec.source.type='${PKG_SOURCE}' (ожидалось literal)" - -tf_destroy "${D}" | grep -q "Destroy complete" && pass "T03: destroy OK" || fail "T03: destroy FAILED" -kube_gone "functions.fission.io" "${T03}" && pass "T03: Function удалена" || fail "T03: Function осталась" - # ════════════════════════════════════════════════════════════════════════ echo "" echo ">>> T04: ruby — create с URL" diff --git a/tests_v2.sh b/tests_v2.sh index 0b3f597..c3db912 100644 --- a/tests_v2.sh +++ b/tests_v2.sh @@ -10,7 +10,6 @@ RUN_ID=$(echo "$RUN_ID" | sha256sum 2>/dev/null | head -c 8 || echo "${RUN_ID}" cleanup_namespaces() { local subs=() - subs+=("${GO_NS}") 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") @@ -38,12 +37,6 @@ cleanup_namespaces() { } trap cleanup_namespaces EXIT -# GO_NS фиксированный — namespace создаётся один раз, Fission не рестартует при повторных прогонах. -# Первый прогон занимает ~5 мин (rolling restart Fission), последующие — мгновенно. -GO_NS="go-livetest@test.local" -# Триггерим ensureUserNamespace заранее — Fission начнёт регистрировать NS пока идут остальные тесты. -curl -s -o /dev/null -X GET ${BASE}/functions -H "X-Test-Sub: ${GO_NS}" || true - 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"; } @@ -783,33 +776,6 @@ check_http "$CODE" "400" "Невалидный язык cobol → 400" # ────────────────────────────────────────────── echo "" -echo ">>> БЛОК AE: Go — создание и вызов" - -# Go: namespace GO_NS уже прогрет в начале теста (curl в самом верху). -# При первом прогоне Fission делает rolling restart (~5 мин) — retry до 600s покрывает это. -# При последующих прогонах NS уже зарегистрирован → invoke занимает ~10-15s. -GO_CODE=$(python3 -c "import json; print(json.dumps('package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc Handler(w http.ResponseWriter, r *http.Request) {\n\tfmt.Fprintf(w, \"go-ok\")\n}'))") -# Удаляем старую функцию если осталась от прошлого прогона (игнорируем ошибку) -curl -s -o /dev/null -X DELETE ${BASE}/functions/go-fn -H "X-Test-Sub: ${GO_NS}" || true -R=$(curl -s -w "\n%{http_code}" -X POST ${BASE}/functions \ - -H "X-Test-Sub: ${GO_NS}" -H "Content-Type: application/json" \ - -d "{\"name\":\"go-fn\",\"language\":\"go\",\"code\":${GO_CODE},\"ttl\":\"30m\"}") -CODE=$(echo "$R" | tail -1) -check_http "$CODE" "201" "Go: create → 201" - -# Retry до 600s (60 итераций × 10s): первый прогон — ждём Fission rolling restart + build + pool pod. -STATUS=""; RAW="" -for _try in $(seq 1 60); do - sleep 10 - INV=$(curl -s -X POST ${BASE}/functions/go-fn/invoke \ - -H "X-Test-Sub: ${GO_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 "Go: invoke → 200" || fail "Go: invoke → got $STATUS (body: $RAW)" -echo "$RAW" | grep -q "go-ok" && pass "Go: тело содержит go-ok" || fail "Go: тело не содержит go-ok (got: $RAW)" - # ────────────────────────────────────────────── echo "" echo "========================================" diff --git a/token.txt b/token.txt index 659d224..f2ff862 100644 --- a/token.txt +++ b/token.txt @@ -1,10 +1,3 @@ -api_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWNjMjY4LTZjNmEtNzgxZS04NjEzLTRiZWQ0ZWM3Y2QyMCIsImV4cCI6MTc4OTAxNTg2NywiaWF0IjoxNzczNDYzODY3LCJqdGkiOiJmYjRkMGFiNy1lYT> -# s3_uid = "332cdb0d-34bf-43bf-864d-4adcc3b556fc" -s3_uid = "332cdb0d-34bf-43bf-864d-4adcc3b556fb" -# realm = "k8s-5-sandbox-nubes-ru" -realm = "k8s-3-sandbox-nubes-ru" -#realm = "naeel-test-3" +ntazetdinov@nubes.ru - - -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWNjMjY4LTZjNmEtNzgxZS04NjEzLTRiZWQ0ZWM3Y2QyMCIsImV4cCI6MTc4OTAxNTg2Nywia \ No newline at end of file +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWQ3NmM1LTQyNGMtN2MyYy04OWI3LWUzZjA5OTQ5YTk4ZiIsImV4cCI6MTc5MzA3ODQzOSwiaWF0IjoxNzc3NTI2NDM5LCJqdGkiOiI4MTU1ODNkNy0xZTRkLTRiMmMtOGM5ZS0wZDZmMGU2NmJjNTciLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiJXWjAxMTEyIiwiY29tcGFueV9pZCI6IjhlYzcwYWMwLTU0NmQtNDJhNy04Y2ZmLTMzOWM4ZmI1MWEyMyIsImNvbXBhbnlfbmFtZSI6ItCe0J7QniDCq9Cd0KPQkdCV0KHCuyIsImlkcF91c3JfdWlkIjoiMDE5ZDc2YzUtNDI0Yy03YzJjLTg5YjctZTNmMDk5NDlhOThmIiwibG9naW4iOiJudGF6ZXRkaW5vdkBudWJlcy5ydSIsImZpcnN0bmFtZSI6ItCd0LDQuNC70YwiLCJtaWRkbGVuYW1lIjoi0KTQsNGA0LjRgtC-0LLQuNGHIiwibGFzdG5hbWUiOiLQotCw0LfQtdGC0LTQuNC90L7QsiIsImdyb3VwcyI6bnVsbCwicHJlZmVycmVkX3VzZXJuYW1lIjoiIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIiwiZW1haWwiOiJudGF6ZXRkaW5vdkBudWJlcy5ydSJ9.SaZsKAha45-fvKuJkLHUe_09AFsbH5QpzBVdPnjiEDQGIhl1A3ThgnM-oEh_H6CBXXfnOs2QDczzgCD8K8_tYzZKU1Wgk5lj04YW_fbTI89kHTO5wLtrAht9tFEjzBQZ-kmnG8mUK5tgqyNEjsngdQcfqVWRvneF366TiiRXk_76poUPpXWQmqdgCCKb3wq1rRgWcKDZcUC3JvDpOaqYL40zITZlM855drlJhMut4Gkg-EEk7ZGykl6YCsTRWVjFKGQ-7C-BDU2dkmIWhxzBAkvzzM2kBwvtlczCEDiUin7bXE-F_io89oJUYDzY4yPeHzwREwxCp6x9eD1NAIKKuA \ No newline at end of file