Compare commits

..
Author SHA1 Message Date
“Naeel” 865d33a0e1 feat: storage benchmark — local-path PV, vcd-disk-ext4 PV, S3 (10 iter each) 2026-05-19 07:28:45 +04:00
“Naeel” 1d81ca34e1 doc: план и разъяснения по устранению 401 'key is of invalid type' (root cause, порядок исправления, volume-mount secret, отказ от fallback на SA token) 2026-05-18 19:51:59 +04:00
“Naeel” 8374254020 chore: gitignore scripts/test_layer1.sh 2026-05-15 14:16:30 +04:00
“Naeel” ae913f7ad6 chore: gitignore token.txt and test-results/ 2026-05-15 14:16:03 +04:00
“Naeel” 68296d8fe0 fix: update paths from ~/IoT and ~/fission to ~/fission-console in .github docs 2026-05-15 14:14:45 +04:00
“Naeel” 949b0b52d6 fix(console): v1.3.92 — EnsureEnvironment обновляет образ если устарел
Вместо костыля (kubectl patch вручную) — системный фикс:
EnsureEnvironment теперь проверяет spec.runtime.image у существующего env
и обновляет его если отличается от LangEnvMap[lang].Image.

Это автоматически мигрирует все namespace-ы при следующем создании функции.
2026-05-13 10:55:47 +04:00
“Naeel” 6a8c3a27e2 fix(console): v1.3.91 — deps сохраняются в аннотации и восстанавливаются при edit
- handleCreateFunction: deps → аннотация fission-console/deps
- handleUpdateFunctionCode: deps → аннотация fission-console/deps (или удаление если пусто)
- GET /functions/:name: возвращает deps из аннотации
- openEdit: заполняет e-deps.value = fn.deps (вместо пустого поля)
2026-05-13 10:43:59 +04:00
“Naeel” 75532d8d8e fix(console): v1.3.90 — deps textarea compact, linter внешние импорты, python env v1.1
- deps textarea: rows=3, resize:none, overflow-y:auto
- aiCheck (Python): после линтера проверяет внешние импорты
  если deps поле пустое — предупреждение с именами модулей
- Python env: v1.0 → v1.1 (содержит boto3+requests+psycopg2-binary)
2026-05-13 10:37:14 +04:00
“Naeel” 10b2b4a8e0 feat(console): поле зависимостей для функций из кода (deps → zip)
- UI: textarea 'Зависимости' в формах создания и редактирования
  - плейсхолдер меняется по языку (requirements.txt / package.json / Gemfile / composer.json)
- Python: если deps заполнен → zip(main.py + requirements.txt), иначе raw bytes
- PHP: если deps → zip(main.php + composer.json)
- Ruby: если deps → zip(handler.rb + Gemfile)
- Node.js: пока без изменений (сложная структура package.json)
- runtime: BuildPythonZip, BuildScriptZipWithDeps, buildZipTwo
- model: Deps string в CreateFunctionRequest и UpdateCodeRequest
- v1.3.89
2026-05-13 10:22:45 +04:00
“Naeel” 17f1c5d46f feat: стартовая точка для weather pipeline без Terraform (ветка feat/weather-pipeline-no-tf) 2026-05-13 07:44:38 +04:00
38 changed files with 875 additions and 248 deletions
+4 -4
View File
@@ -28,16 +28,16 @@
1. Не трогать рабочий код без явного указания. 1. Не трогать рабочий код без явного указания.
2. Файлы редактируются локально: 2. Файлы редактируются локально:
~/IoT ~/fission-console
После ЛЮБЫХ изменений ОБЯЗАТЕЛЬНО синхронизировать на ВМ командой: После ЛЮБЫХ изменений ОБЯЗАТЕЛЬНО синхронизировать на ВМ командой:
rsync -az \ rsync -az \
-e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \ -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
~/IoT/ \ ~/fission-console/ \
naeel@5.172.178.213:~/terra/IoT/ naeel@5.172.178.213:~/terra/fission/
3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/IoT 3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/fission-console
4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ: 4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ:
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА' ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
+6 -6
View File
@@ -13,14 +13,14 @@
## Файловая система (актуально) ## Файловая система (актуально)
1. Все файлы редактируются локально: `~/IoT` 1. Все файлы редактируются локально: `~/fission-console`
2. После любых изменений — обязательно rsync на ВМ: 2. После любых изменений — обязательно rsync на ВМ:
rsync -az \ rsync -az \
-e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \ -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
~/IoT/ \ ~/fission-console/ \
naeel@5.172.178.213:~/terra/IoT/ naeel@5.172.178.213:~/terra/fission/
3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/IoT 3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/fission-console
4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ 4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ
5. Перед запуском любой команды на ВМ обязательно убедиться, что синхронизация (rsync) выполнена 5. Перед запуском любой команды на ВМ обязательно убедиться, что синхронизация (rsync) выполнена
6. SCP, sshfs, remote_dev и маунты больше НЕ используются 6. SCP, sshfs, remote_dev и маунты больше НЕ используются
@@ -50,7 +50,7 @@ ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=
## Git ## Git
⛔⛔⛔ АБСОЛЮТНОЕ ПРАВИЛО: ⛔⛔⛔ АБСОЛЮТНОЕ ПРАВИЛО:
- Git — ТОЛЬКО ЛОКАЛЬНО в `~/fission`. НИКОГДА через SSH на VM. - Git — ТОЛЬКО ЛОКАЛЬНО в `~/fission-console`. НИКОГДА через SSH на VM.
- Разрешены ТОЛЬКО две операции: `git commit` и `git push`. - Разрешены ТОЛЬКО две операции: `git commit` и `git push`.
- ЗАПРЕЩЕНО: git pull, git fetch, git rebase, git merge, git reset, git stash, git checkout — что угодно кроме commit и push. - ЗАПРЕЩЕНО: git pull, git fetch, git rebase, git merge, git reset, git stash, git checkout — что угодно кроме commit и push.
- Если push отклонён — СТОП, доложить пользователю. Не лезть в pull/merge/rebase самостоятельно. - Если push отклонён — СТОП, доложить пользователю. Не лезть в pull/merge/rebase самостоятельно.
@@ -86,7 +86,7 @@ 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 \ 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}" "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" \ rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no" \
naeel@5.172.178.213:~/terra/IoT/test-results/ ~/IoT/test-results/ naeel@5.172.178.213:~/terra/fission/test-results/ ~/fission-console/test-results/
``` ```
**Никогда не разбираться с результатами по памяти / буферу / чату. Только лог.** **Никогда не разбираться с результатами по памяти / буферу / чату. Только лог.**
+7
View File
@@ -16,3 +16,10 @@ examples/*/dist/
terraform-provider-fission terraform-provider-fission
terraform-provider-fission_* terraform-provider-fission_*
console/fission-console console/fission-console
# Secrets / tokens
token.txt
# Test logs
test-results/
scripts/test_layer1.sh
+117
View File
@@ -0,0 +1,117 @@
# Fix: 401 "key is of invalid type" при invoke функций
## Статус: ГОТОВО К ПРАВКЕ (не исправлено)
## Root Cause (установлен 2026-05-18)
Secret `router` в k8s namespace `fission` был ротирован.
Router pod перезапустился — подхватил новый пароль.
Console pod НЕ перезапускался (5d8h) — имеет старый пароль в env var.
| | |
|---|---|
| `FISSION_AUTH_PASSWORD` в console pod (env) | `ToAjBTs0Sb8mLC1J9NN3` (СТАРЫЙ) |
| `password` в k8s secret `router` (текущий) | `slxGj3G3FAD7l5ms2tJ9` |
### Цепочка отказа
1. `getRouterToken()` → `POST /auth/login` со старым паролем → **401 Unauthorized**
2. Fallback: `return s.readSAToken()` → Kubernetes SA token, `alg: RS256`
3. Console отправляет `Authorization: Bearer <RS256-SA-token>` в router
4. Router keyfunc: `return []byte(JWT_SIGNING_KEY), nil` — возвращает `[]byte` для **любого** алгоритма
5. `jwt.Parse` получает RS256 JWT но ключ `[]byte` (ожидается `*rsa.PublicKey`) → **"key is of invalid type"**
### Подтверждено
```bash
# Старый пароль → 401:
kubectl exec -n fission fission-console-874c786c6-j6fvd -- wget -qO- \
--post-data='{"username":"admin","password":"ToAjBTs0Sb8mLC1J9NN3"}' \
--header='Content-Type: application/json' \
http://router.fission.svc.cluster.local/auth/login
# → HTTP/1.1 401 Unauthorized
# Новый пароль → OK:
# password=slxGj3G3FAD7l5ms2tJ9 → {"accesstoken":"eyJhbGciOiJIUzI1NiI..."}
```
---
## Что нужно исправить
### Файл 1: `console/internal/api/server.go` — функция `getRouterToken()`
**Проблема A:** читает пароль из env var один раз при старте → устаревает при ротации secret.
**Проблема B:** при ошибке login молча возвращает SA token вместо ошибки.
**Нужно:**
- Читать username/password из **файла** при каждом вызове `getRouterToken()`, а не из `os.Getenv` при старте.
Путь файла: `/etc/fission-router-secret/username` и `/etc/fission-router-secret/password`
- При ошибке login — возвращать `("", error)`, не `readSAToken()`.
Caller (`handleInvokeFunction`) должен вернуть 503 с понятным сообщением.
### Файл 2: `console/deploy/console.yaml`
**Нужно:** заменить `env` → `secretKeyRef` на volume mount.
```yaml
# Убрать из env:
- name: FISSION_AUTH_USERNAME
valueFrom:
secretKeyRef:
name: router
key: username
- name: FISSION_AUTH_PASSWORD
valueFrom:
secretKeyRef:
name: router
key: password
# Добавить volume:
volumes:
- name: router-secret
secret:
secretName: router
items:
- key: username
path: username
- key: password
path: password
# Добавить volumeMount:
volumeMounts:
- name: router-secret
mountPath: /etc/fission-router-secret
readOnly: true
```
Kubernetes обновляет смонтированные secret-файлы автоматически в течение ~60с после изменения secret.
---
## Порядок работы
1. Прочитать текущий код `getRouterToken()` в `console/internal/api/server.go`
2. Найти где инициализируются `s.authUser` / `s.authPass` (вероятно в `NewServer()` или аналоге)
3. Убрать сохранение в struct, читать из файла на каждый вызов `getRouterToken()`
4. Изменить возврат при ошибке login: `return "", fmt.Errorf("router login failed: %w", err)` вместо `readSAToken()`
5. Найти всех callers `getRouterToken()` — обработать ошибку (вернуть 503)
6. Обновить `console/deploy/console.yaml` (volume mount вместо env)
7. Увеличить тег образа (согласно правилам: vX.Y.Z → vX.Y.Z+1)
8. rsync → build → push → apply
## Текущий тег образа
Проверить: `grep "naeel/fission-console" console/deploy/console.yaml`
Последний известный: `v1.3.92`
## SSH / rsync
```bash
# rsync:
rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
~/fission-console/ naeel@5.172.178.213:~/terra/fission/
# SSH:
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
```
+1 -1
View File
@@ -58,7 +58,7 @@ spec:
serviceAccountName: fission-console serviceAccountName: fission-console
containers: containers:
- name: console - name: console
image: naeel/fission-console:v1.3.88 image: naeel/fission-console:v1.3.92
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8090 - containerPort: 8090
+23 -6
View File
@@ -35,17 +35,26 @@ import (
// buildDeployArchive упаковывает исходный код в байты для deployment Package. // buildDeployArchive упаковывает исходный код в байты для deployment Package.
// Для nodejs — ESM-обёртка (package.json + main.js). // Для nodejs — ESM-обёртка (package.json + main.js).
// Для php/ruby — zip с одним файлом скрипта. // Для php/ruby — zip с одним файлом скрипта.
// Для остальных (python) — raw bytes кода. // Для остальных (python) — raw bytes кода (или zip если есть deps).
func buildDeployArchive(lang, code string) ([]byte, error) { // deps — содержимое файла зависимостей (requirements.txt, Gemfile, composer.json).
// Если deps пустой — поведение как раньше.
func buildDeployArchive(lang, code, deps string) ([]byte, error) {
switch lang { switch lang {
case "nodejs": case "nodejs":
// TODO: поддержка package.json с deps для nodejs — пока игнорируем deps
return runtime.BuildJSDeployZip(code) return runtime.BuildJSDeployZip(code)
case "php": case "php":
if deps != "" {
return runtime.BuildScriptZipWithDeps(code, "main.php", deps, "composer.json")
}
return runtime.BuildScriptZip(code, "main.php") return runtime.BuildScriptZip(code, "main.php")
case "ruby": case "ruby":
if deps != "" {
return runtime.BuildScriptZipWithDeps(code, "handler.rb", deps, "Gemfile")
}
return runtime.BuildScriptZip(code, "handler.rb") return runtime.BuildScriptZip(code, "handler.rb")
default: default: // python
return []byte(code), nil return runtime.BuildPythonZip(code, deps)
} }
} }
@@ -173,7 +182,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
"buildcommand": "build", "buildcommand": "build",
} }
} else { } else {
deployBytes, archiveErr := buildDeployArchive(req.Language, req.Code) deployBytes, archiveErr := buildDeployArchive(req.Language, req.Code, req.Deps)
if archiveErr != nil { if archiveErr != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr)) writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr))
return return
@@ -205,6 +214,9 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
now := time.Now().UTC() now := time.Now().UTC()
fnAnnotations[functionCreatedAtAnnotation] = now.Format(time.RFC3339) fnAnnotations[functionCreatedAtAnnotation] = now.Format(time.RFC3339)
fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339) fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339)
if req.Deps != "" {
fnAnnotations["fission-console/deps"] = req.Deps
}
if req.TTL != "" { if req.TTL != "" {
expiresAt, ttlErr := parseTTL(req.TTL) expiresAt, ttlErr := parseTTL(req.TTL)
if ttlErr != nil { if ttlErr != nil {
@@ -329,7 +341,7 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
// Определяем язык из аннотации — нужен для правильной упаковки // Определяем язык из аннотации — нужен для правильной упаковки
lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language") lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language")
deployBytes, archiveErr := buildDeployArchive(lang, req.Code) deployBytes, archiveErr := buildDeployArchive(lang, req.Code, req.Deps)
if archiveErr != nil { if archiveErr != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", lang, archiveErr)) writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", lang, archiveErr))
return return
@@ -393,6 +405,11 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
} }
fnAnnotations[functionCreatedAtAnnotation] = createdAt.UTC().Format(time.RFC3339) fnAnnotations[functionCreatedAtAnnotation] = createdAt.UTC().Format(time.RFC3339)
fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339) fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339)
if req.Deps != "" {
fnAnnotations["fission-console/deps"] = req.Deps
} else {
delete(fnAnnotations, "fission-console/deps")
}
fn.SetAnnotations(fnAnnotations) fn.SetAnnotations(fnAnnotations)
if err := unstructured.SetNestedField(fn.Object, map[string]any{ if err := unstructured.SetNestedField(fn.Object, map[string]any{
"name": newPkgName, "name": newPkgName,
+5
View File
@@ -83,6 +83,7 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
// Читаем source-type аннотацию (code / archive) // Читаем source-type аннотацию (code / archive)
sourceType := "code" sourceType := "code"
archiveFilename := "" archiveFilename := ""
deps := ""
if ann := fn.GetAnnotations(); ann != nil { if ann := fn.GetAnnotations(); ann != nil {
if v := ann[fissionSourceTypeAnnotation]; v != "" { if v := ann[fissionSourceTypeAnnotation]; v != "" {
sourceType = v sourceType = v
@@ -90,6 +91,9 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
if v := ann["fission-console/archive-filename"]; v != "" { if v := ann["fission-console/archive-filename"]; v != "" {
archiveFilename = v archiveFilename = v
} }
if v := ann["fission-console/deps"]; v != "" {
deps = v
}
} }
writeAnyJSON(w, http.StatusOK, map[string]any{ writeAnyJSON(w, http.StatusOK, map[string]any{
@@ -102,6 +106,7 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
"created_at": functionTimestampResponse(fn)["created_at"], "created_at": functionTimestampResponse(fn)["created_at"],
"updated_at": functionTimestampResponse(fn)["updated_at"], "updated_at": functionTimestampResponse(fn)["updated_at"],
"code": code, "code": code,
"deps": deps,
"source_type": sourceType, "source_type": sourceType,
"archive_filename": archiveFilename, "archive_filename": archiveFilename,
"route": route, "route": route,
+13 -2
View File
@@ -28,9 +28,20 @@ func EnsureEnvironment(ctx context.Context, dyn dynamic.Interface, ns, lang stri
envName := "console-" + lang + "-env" envName := "console-" + lang + "-env"
// Проверяем существование — Get быстрее чем Create+IsAlreadyExists // Проверяем существование — Get быстрее чем Create+IsAlreadyExists
_, getErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Get(ctx, envName, metav1.GetOptions{}) existing, getErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Get(ctx, envName, metav1.GetOptions{})
if getErr == nil { if getErr == nil {
return envName, nil // уже существует — быстрый путь // Уже существует — проверяем образ. Если устарел — обновляем.
currentImage, _, _ := unstructured.NestedString(existing.Object, "spec", "runtime", "image")
if currentImage != langDef.Image {
if patchErr := unstructured.SetNestedField(existing.Object, langDef.Image, "spec", "runtime", "image"); patchErr == nil {
if _, updateErr := dyn.Resource(EnvironmentGVR).Namespace(ns).Update(ctx, existing, metav1.UpdateOptions{}); updateErr != nil {
log.Printf("ensureEnvironment: update image %s/%s: %v", ns, envName, updateErr)
} else {
log.Printf("ensureEnvironment: updated image %s/%s: %s → %s", ns, envName, currentImage, langDef.Image)
}
}
}
return envName, nil
} }
if !apierrors.IsNotFound(getErr) { if !apierrors.IsNotFound(getErr) {
return "", fmt.Errorf("check environment %q: %w", envName, getErr) return "", fmt.Errorf("check environment %q: %w", envName, getErr)
+5 -1
View File
@@ -5,11 +5,14 @@ package model
// CreateFunctionRequest — тело POST /console/api/functions. // CreateFunctionRequest — тело POST /console/api/functions.
// TTL пустой → функция живёт вечно; "1d", "24h" — протухнет через указанное время. // TTL пустой → функция живёт вечно; "1d", "24h" — протухнет через указанное время.
// Deps — содержимое файла зависимостей: requirements.txt (python), package.json deps (nodejs),
// Gemfile (ruby), composer.json (php). Если задан — код упаковывается в zip вместе с deps-файлом.
type CreateFunctionRequest struct { type CreateFunctionRequest struct {
Name string `json:"name"` Name string `json:"name"`
Language string `json:"language"` Language string `json:"language"`
Environment string `json:"environment"` Environment string `json:"environment"`
Code string `json:"code"` Code string `json:"code"`
Deps string `json:"deps"` // содержимое файла зависимостей (опционально)
Entrypoint string `json:"entrypoint"` Entrypoint string `json:"entrypoint"`
Route string `json:"route"` Route string `json:"route"`
Methods []string `json:"methods"` Methods []string `json:"methods"`
@@ -51,6 +54,7 @@ type CreateKWTriggerRequest struct {
// UpdateCodeRequest — тело PUT /console/api/functions/:name/code. // UpdateCodeRequest — тело PUT /console/api/functions/:name/code.
type UpdateCodeRequest struct { type UpdateCodeRequest struct {
Code string `json:"code"` Code string `json:"code"`
Deps string `json:"deps"` // содержимое файла зависимостей (опционально)
Timeout int64 `json:"timeout"` Timeout int64 `json:"timeout"`
} }
@@ -66,7 +70,7 @@ type LangEnvDef struct {
// LangEnvMap сопоставляет идентификатор языка (string) с описанием среды выполнения. // LangEnvMap сопоставляет идентификатор языка (string) с описанием среды выполнения.
// Ключ используется в createFunctionRequest.Language и как суффикс имени Environment. // Ключ используется в createFunctionRequest.Language и как суффикс имени Environment.
var LangEnvMap = map[string]LangEnvDef{ var LangEnvMap = map[string]LangEnvDef{
"python": {Image: "naeel/fission-python-env:v1.0"}, "python": {Image: "naeel/fission-python-env:v1.1"},
"nodejs": {Image: "ghcr.io/fission/node-env"}, "nodejs": {Image: "ghcr.io/fission/node-env"},
"go": {Image: "ghcr.io/fission/go-env", BuilderImage: "naeel/go-builder-fast:v1"}, "go": {Image: "ghcr.io/fission/go-env", BuilderImage: "naeel/go-builder-fast:v1"},
"php": {Image: "ghcr.io/fission/php-env"}, "php": {Image: "ghcr.io/fission/php-env"},
+25
View File
@@ -25,3 +25,28 @@ func buildZip(fileName string, content []byte) ([]byte, error) {
} }
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// buildZipTwo создаёт zip-архив с двумя файлами.
// Используется когда пользователь указал файл зависимостей (requirements.txt и т.д.).
func buildZipTwo(file1, file2 string, content1, content2 []byte) ([]byte, error) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for _, f := range []struct {
name string
content []byte
}{{file1, content1}, {file2, content2}} {
fw, err := zw.Create(f.name)
if err != nil {
return nil, err
}
if _, err := fw.Write(f.content); err != nil {
return nil, err
}
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
+17
View File
@@ -11,3 +11,20 @@ package runtime
func BuildScriptZip(code, fileName string) ([]byte, error) { func BuildScriptZip(code, fileName string) ([]byte, error) {
return buildZip(fileName, []byte(code)) return buildZip(fileName, []byte(code))
} }
// BuildScriptZipWithDeps создаёт zip с кодом и файлом зависимостей.
// fileName — имя файла кода (handler.rb, main.php и т.д.)
// depsName — имя файла зависимостей (Gemfile, composer.json и т.д.)
func BuildScriptZipWithDeps(code, fileName, deps, depsName string) ([]byte, error) {
return buildZipTwo(fileName, depsName, []byte(code), []byte(deps))
}
// BuildPythonZip создаёт zip с main.py (и опционально requirements.txt).
// Если deps пустой — возвращает raw bytes кода (текущее поведение Python).
// Если deps задан — zip с main.py + requirements.txt для pip install.
func BuildPythonZip(code, deps string) ([]byte, error) {
if deps == "" {
return []byte(code), nil
}
return buildZipTwo("main.py", "requirements.txt", []byte(code), []byte(deps))
}
+14 -2
View File
@@ -103,7 +103,7 @@
<div class="nubes">NUBES</div> <div class="nubes">NUBES</div>
<div class="product">FISSION CONSOLE</div> <div class="product">FISSION CONSOLE</div>
</div> </div>
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.88</div> <div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.92</div>
</div> </div>
<div class="row" style="margin:0;"> <div class="row" style="margin:0;">
<button class="btn ghost" onclick="reloadAll()">Refresh</button> <button class="btn ghost" onclick="reloadAll()">Refresh</button>
@@ -306,6 +306,12 @@
<button class="btn ghost" id="cc-gen-btn" onclick="showGenPrompt('cc')">&#x2728; Сгенерировать код LLM</button> <button class="btn ghost" id="cc-gen-btn" onclick="showGenPrompt('cc')">&#x2728; Сгенерировать код LLM</button>
<button class="btn ghost" id="cc-exp-btn" onclick="aiExplain('cc-code','cc-lang','cc-ai-result')">&#x1F4D6; LLM: Что делает?</button> <button class="btn ghost" id="cc-exp-btn" onclick="aiExplain('cc-code','cc-lang','cc-ai-result')">&#x1F4D6; LLM: Что делает?</button>
</div> </div>
<div style="margin-top:10px;">
<label id="cc-deps-label" style="font-size:12px; color:var(--text-secondary); margin-bottom:4px; display:block;">Зависимости (requirements.txt)</label>
<textarea id="cc-deps" rows="3" placeholder="boto3
requests>=2.28
psycopg2-binary" style="width:100%; box-sizing:border-box; font-family:monospace; font-size:12px; background:#1a1a2e; border:1px dashed #3a3a5c; border-radius:4px; color:#cdd6f4; padding:6px 8px; resize:none; overflow-y:auto;"></textarea>
</div>
</div> </div>
<div id="cc-gen-prompt" style="display:none; margin-top:8px; gap:6px; align-items:center;"> <div id="cc-gen-prompt" style="display:none; margin-top:8px; gap:6px; align-items:center;">
<input id="cc-gen-desc" type="text" <input id="cc-gen-desc" type="text"
@@ -445,6 +451,12 @@
<button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">&#x1F4D6; <button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">&#x1F4D6;
LLM: Что делает?</button> LLM: Что делает?</button>
</div> </div>
<div style="margin-top:10px;">
<label id="e-deps-label" style="font-size:12px; color:var(--text-secondary); margin-bottom:4px; display:block;">Зависимости (requirements.txt)</label>
<textarea id="e-deps" rows="3" placeholder="boto3
requests>=2.28
psycopg2-binary" style="width:100%; box-sizing:border-box; font-family:monospace; font-size:12px; background:#1a1a2e; border:1px dashed #3a3a5c; border-radius:4px; color:#cdd6f4; padding:6px 8px; resize:none; overflow-y:auto;"></textarea>
</div>
</div> </div>
<div id="e-archive-area" style="display:none;"> <div id="e-archive-area" style="display:none;">
<div id="e-archive-current" style="margin-bottom:10px; padding:8px 12px; background:var(--bg-alt); border-radius:6px; font-size:13px; color:var(--text-secondary);"> <div id="e-archive-current" style="margin-bottom:10px; padding:8px 12px; background:var(--bg-alt); border-radius:6px; font-size:13px; color:var(--text-secondary);">
@@ -614,7 +626,7 @@
</div> </div>
<div class="actions" style="justify-content:space-between; align-items:center;"> <div class="actions" style="justify-content:space-between; align-items:center;">
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.88</span> <span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.92</span>
<button class="btn ghost" onclick="closeHelp()">Закрыть</button> <button class="btn ghost" onclick="closeHelp()">Закрыть</button>
</div> </div>
</div> </div>
+57 -1
View File
@@ -1,5 +1,43 @@
/* ai.js — AI/LLM функции: проверка, генерация, объяснение, ассистент */ /* ai.js — AI/LLM функции: проверка, генерация, объяснение, ассистент */
// PYTHON_STDLIB — стандартная библиотека Python (не требуют pip install).
var PYTHON_STDLIB = new Set([
'os','sys','json','time','re','math','io','collections','itertools','functools',
'datetime','pathlib','urllib','http','threading','asyncio','logging','random',
'string','struct','hashlib','hmac','base64','uuid','copy','abc','typing',
'dataclasses','contextlib','enum','warnings','traceback','inspect','importlib',
'subprocess','socket','ssl','email','html','xml','csv','sqlite3','unittest',
'gc','weakref','array','queue','heapq','bisect','decimal','fractions',
'statistics','textwrap','difflib','shutil','glob','fnmatch','tempfile',
'zipfile','tarfile','gzip','bz2','lzma','pickle','shelve','codecs',
'unicodedata','ast','dis','types','builtins','operator','numbers','cmath',
'pprint','reprlib','platform','signal','mmap','ctypes','multiprocessing',
'concurrent','select','selectors','errno','atexit','sched','calendar',
'locale','gettext','argparse','getopt','shlex','configparser','tokenize',
'runpy','pkgutil','site','sysconfig','distutils','zipimport','abc','io',
'__future__','_thread','threading','contextvars','netrc','plistlib',
'html','xml','http','urllib','email','mailbox','mimetypes','encodings',
'codecs','unicodedata','readline','rlcompleter','curses','idlelib','tkinter',
]);
// detectExternalPythonImports — возвращает список модулей, которых нет в stdlib.
function detectExternalPythonImports(code) {
var external = [];
var seen = {};
var patterns = [/^import\s+([\w]+)/gm, /^from\s+([\w]+)/gm];
for (var pi = 0; pi < patterns.length; pi++) {
var m;
while ((m = patterns[pi].exec(code)) !== null) {
var mod = m[1];
if (!PYTHON_STDLIB.has(mod) && !seen[mod]) {
seen[mod] = true;
external.push(mod);
}
}
}
return external;
}
function llmGeneratedWarning(lang) { function llmGeneratedWarning(lang) {
var text = 'Сделано LLM. Не доверяй, проверяй!'; var text = 'Сделано LLM. Не доверяй, проверяй!';
switch (lang) { switch (lang) {
@@ -45,7 +83,25 @@ async function aiCheck(codeId, langId, resultId) {
var data = await requestJSON(API_BASE + '/ai/check', 'POST', { language: lang, code: code }); var data = await requestJSON(API_BASE + '/ai/check', 'POST', { language: lang, code: code });
resEl.style.background = data.ok ? '#1a3a1a' : '#3a1a1a'; resEl.style.background = data.ok ? '#1a3a1a' : '#3a1a1a';
resEl.style.color = data.ok ? '#8f8' : '#f88'; resEl.style.color = data.ok ? '#8f8' : '#f88';
resEl.textContent = data.result || '(пустой ответ)'; var resultText = data.result || '(пустой ответ)';
// Для Python: проверяем внешние импорты vs поле зависимостей
if (lang === 'python') {
var depsId = codeId.replace('-code', '-deps'); // cc-code→cc-deps, e-code→e-deps
var depsEl = document.getElementById(depsId);
var depsVal = depsEl ? depsEl.value.trim() : '';
var external = detectExternalPythonImports(code);
if (external.length > 0 && !depsVal) {
resultText += '\n\n⚠️ Внешние библиотеки: ' + external.join(', ') +
'\nДобавьте их в поле "Зависимости (requirements.txt)" или убедитесь что они уже есть в Python-окружении.';
if (data.ok) {
resEl.style.background = '#3a2a00';
resEl.style.color = '#ffa';
}
}
}
resEl.textContent = resultText;
} catch (e) { } catch (e) {
resEl.style.background = '#3a2a00'; resEl.style.background = '#3a2a00';
resEl.style.color = '#ffa'; resEl.style.color = '#ffa';
+20 -1
View File
@@ -7,6 +7,23 @@ function onLangChangeCode() {
document.getElementById('cc-entry').value = t.entrypoint; document.getElementById('cc-entry').value = t.entrypoint;
document.getElementById('cc-code').value = t.code; document.getElementById('cc-code').value = t.code;
} }
// Обновляем лейбл поля зависимостей под язык
var depsLabels = {
python: 'Зависимости (requirements.txt)',
nodejs: 'Зависимости (package.json dependencies)',
php: 'Зависимости (composer.json)',
ruby: 'Зависимости (Gemfile)',
};
var depsPlaceholders = {
python: 'boto3\nrequests>=2.28\npsycopg2-binary',
nodejs: 'express: ^4.18.2\naxios: ^1.6.0',
php: '{\n "require": {\n "guzzlehttp/guzzle": "^7.0"\n }\n}',
ruby: "gem 'httparty'\ngem 'pg'",
};
var label = document.getElementById('cc-deps-label');
var area = document.getElementById('cc-deps');
if (label) label.textContent = depsLabels[lang] || 'Зависимости';
if (area) area.placeholder = depsPlaceholders[lang] || '';
} }
function openCreateCode() { function openCreateCode() {
@@ -16,6 +33,7 @@ function openCreateCode() {
document.getElementById('cc-route').value = ''; document.getElementById('cc-route').value = '';
document.getElementById('cc-methods').value = 'GET'; document.getElementById('cc-methods').value = 'GET';
document.getElementById('cc-timeout').value = '60'; document.getElementById('cc-timeout').value = '60';
document.getElementById('cc-deps').value = '';
document.getElementById('cc-schedule-enabled').checked = false; document.getElementById('cc-schedule-enabled').checked = false;
document.getElementById('cc-cron').value = ''; document.getElementById('cc-cron').value = '';
toggleScheduleFields('cc'); toggleScheduleFields('cc');
@@ -47,7 +65,8 @@ async function submitCreateCode() {
route: document.getElementById('cc-route').value.trim(), route: document.getElementById('cc-route').value.trim(),
methods: parseMethods(document.getElementById('cc-methods').value), methods: parseMethods(document.getElementById('cc-methods').value),
timeout: parseTimeout(document.getElementById('cc-timeout').value), timeout: parseTimeout(document.getElementById('cc-timeout').value),
code: document.getElementById('cc-code').value code: document.getElementById('cc-code').value,
deps: document.getElementById('cc-deps').value.trim(),
}); });
try { try {
+8
View File
@@ -188,6 +188,13 @@ async function openEdit(name) {
else if (envName.includes('ruby')) lang = 'ruby'; else if (envName.includes('ruby')) lang = 'ruby';
else if (envName.includes('php')) lang = 'php'; else if (envName.includes('php')) lang = 'php';
document.getElementById('e-lang-hidden').value = lang; document.getElementById('e-lang-hidden').value = lang;
// Обновляем лейбл и плейсхолдер поля зависимостей
var depsLabels = {python:'Зависимости (requirements.txt)', nodejs:'Зависимости (package.json dependencies)', php:'Зависимости (composer.json)', ruby:'Зависимости (Gemfile)'};
var depsPlaceholders = {python:'boto3\nrequests>=2.28', nodejs:'express: ^4.18.2\naxios: ^1.6.0', php:'{\n "require": {\n "guzzlehttp/guzzle": "^7.0"\n }\n}', ruby:"gem 'httparty'\ngem 'pg'"};
var eDepsLabel = document.getElementById('e-deps-label');
var eDepsArea = document.getElementById('e-deps');
if (eDepsLabel) eDepsLabel.textContent = depsLabels[lang] || 'Зависимости';
if (eDepsArea) { eDepsArea.placeholder = depsPlaceholders[lang] || ''; eDepsArea.value = fn.deps || ''; }
var aiRes = document.getElementById('e-ai-result'); var aiRes = document.getElementById('e-ai-result');
if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; } if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; }
var warnEl = document.getElementById('e-tf-warn'); var warnEl = document.getElementById('e-tf-warn');
@@ -242,6 +249,7 @@ async function submitEdit() {
} else { } else {
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', { await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', {
code: document.getElementById('e-code').value, code: document.getElementById('e-code').value,
deps: document.getElementById('e-deps').value.trim(),
timeout: parseTimeout(document.getElementById('e-timeout').value) timeout: parseTimeout(document.getElementById('e-timeout').value)
}); });
} }
+3
View File
@@ -0,0 +1,3 @@
<svg width="126mm" height="91mm" viewBox="0 0 126 91" version="1.1" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="63" cy="45.5" rx="63" ry="45.5" fill="#cccccc" stroke="#222222" stroke-width="1" />
</svg>

After

Width:  |  Height:  |  Size: 210 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="210mm" height="297mm" viewBox="0 0 210 297" version="1.1" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="105" cy="148.5" rx="63" ry="45.5" fill="#cccccc" stroke="#222222" stroke-width="1" />
</svg>

After

Width:  |  Height:  |  Size: 214 B

+105
View File
@@ -0,0 +1,105 @@
#!/bin/bash
# Fission storage benchmark: local-path PV / vcd-disk-ext4 PV / S3
# Каждый вызов = полный путь: curl → Fission Router → Executor → Pod → Storage
set -e
N=${1:-10} # число итераций, по умолчанию 10
OUTDIR="$(cd "$(dirname "$0")/../../test-results" 2>/dev/null && pwd || echo /tmp)"
OUTFILE="$OUTDIR/bench-storage-$(date +%Y-%m-%d_%H-%M-%S).txt"
mkdir -p "$OUTDIR"
# Port-forward router
kubectl port-forward svc/router 8889:80 -n fission &>/tmp/pf-bench.log &
PF_PID=$!
trap "kill $PF_PID 2>/dev/null" EXIT
sleep 3
ROUTER="http://localhost:8889"
# JWT auth
PASSWORD=$(kubectl get secret router -n fission -o jsonpath="{.data.password}" | base64 -d)
USERNAME=$(kubectl get secret router -n fission -o jsonpath="{.data.username}" | base64 -d)
TOKEN=$(curl -s -X POST "$ROUTER/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['accesstoken'])")
if [ -z "$TOKEN" ]; then
echo "ERROR: не удалось получить JWT токен" >&2
exit 1
fi
AUTH="-H \"Authorization: Bearer $TOKEN\""
# Функция запуска N итераций и сбора статистики
bench_endpoint() {
local LABEL="$1"
local URL="$2"
local times=()
local errors=0
echo ""
echo "=== $LABEL ==="
printf "%-5s %-12s %s\n" "iter" "ms" "response"
for i in $(seq 1 $N); do
START=$(date +%s%N)
RESP=$(curl -s --max-time 60 -H "Authorization: Bearer $TOKEN" "$URL" 2>/dev/null)
END=$(date +%s%N)
MS=$(( (END - START) / 1000000 ))
if echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['status']=='ok'" 2>/dev/null; then
STATUS="ok"
INNER=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); k=list(d.keys()); print(' '.join(f'{k}={d[k]}' for k in k if k not in ['status']))" 2>/dev/null)
else
STATUS="ERR"
INNER="$RESP"
(( errors++ )) || true
fi
times+=($MS)
printf "%-5s %-12s %s\n" "$i" "${MS}ms" "$STATUS $INNER"
done
# Статистика
local sum=0 min=999999999 max=0
for t in "${times[@]}"; do
sum=$((sum + t))
[ $t -lt $min ] && min=$t
[ $t -gt $max ] && max=$t
done
local avg=$((sum / N))
local ok=$((N - errors))
echo "---"
printf " Успешно: %d/%d | min=%dms avg=%dms max=%dms\n" "$ok" "$N" "$min" "$avg" "$max"
}
# Заголовок отчёта
{
echo "============================================================"
echo " Fission Storage Benchmark"
echo " Дата: $(date)"
echo " Итераций: $N на endpoint"
echo " Стек: curl → Router → Executor → Pod → Storage"
echo "============================================================"
bench_endpoint "local-path PV (100Mi, rawfile CSI, WaitForFirstConsumer)" \
"$ROUTER/check/local"
bench_endpoint "vcd-disk-ext4 PV (10Gi, VMware Cloud Director)" \
"$ROUTER/check/vcd"
bench_endpoint "S3 ngcloud (s3.msk-1.ngcloud.ru, bucket=sless-functions)" \
"$ROUTER/check/s3"
echo ""
echo "============================================================"
echo " Завершено: $(date)"
echo "============================================================"
} | tee "$OUTFILE"
echo ""
echo "Результаты записаны: $OUTFILE"
+31
View File
@@ -0,0 +1,31 @@
import os
import time
def main(event, context):
path = "/mnt/data/check.txt"
test_data = "storage-check-ok"
try:
# Write
t0 = time.time()
with open(path, "w") as f:
f.write(test_data)
write_ms = round((time.time() - t0) * 1000, 2)
# Read
t0 = time.time()
with open(path, "r") as f:
result = f.read()
read_ms = round((time.time() - t0) * 1000, 2)
# Cleanup
os.remove(path)
if result == test_data:
return {"status": "ok", "write_ms": write_ms, "read_ms": read_ms, "mount": path}
else:
return {"status": "error", "detail": "data mismatch"}
except Exception as e:
return {"status": "error", "detail": str(e)}
+48
View File
@@ -0,0 +1,48 @@
import time
import boto3
from botocore.client import Config
def main(event, context):
# Credentials mounted by Fission at /secrets/<namespace>/<secretname>/<key>
try:
with open("/secrets/default/bench-s3-secret/access-key") as f:
access_key = f.read().strip()
with open("/secrets/default/bench-s3-secret/secret-key") as f:
secret_key = f.read().strip()
except Exception as e:
return {"status": "error", "detail": f"secret read: {e}"}
s3 = boto3.client(
"s3",
endpoint_url="https://s3.msk-1.ngcloud.ru",
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
config=Config(signature_version="s3v4"),
)
bucket = "sless-functions"
key = "storage-check/check.txt"
test_data = b"storage-check-ok"
try:
# Write
t0 = time.time()
s3.put_object(Bucket=bucket, Key=key, Body=test_data)
write_ms = round((time.time() - t0) * 1000, 2)
# Read
t0 = time.time()
result = s3.get_object(Bucket=bucket, Key=key)["Body"].read()
read_ms = round((time.time() - t0) * 1000, 2)
# Cleanup
s3.delete_object(Bucket=bucket, Key=key)
if result == test_data:
return {"status": "ok", "write_ms": write_ms, "read_ms": read_ms, "bucket": bucket}
else:
return {"status": "error", "detail": "data mismatch"}
except Exception as e:
return {"status": "error", "detail": str(e)}
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
NS=default
echo "=== Removing HTTP triggers ==="
fission httptrigger delete --name check-local-t --namespace $NS 2>/dev/null || true
fission httptrigger delete --name check-vcd-t --namespace $NS 2>/dev/null || true
fission httptrigger delete --name check-s3-t --namespace $NS 2>/dev/null || true
echo "=== Removing functions ==="
fission fn delete --name check-local --namespace $NS 2>/dev/null || true
fission fn delete --name check-vcd --namespace $NS 2>/dev/null || true
fission fn delete --name check-s3 --namespace $NS 2>/dev/null || true
echo "=== Removing environment ==="
fission env delete --name bench-py --namespace $NS 2>/dev/null || true
echo "=== Removing PVCs ==="
kubectl delete pvc storage-check-local -n $NS 2>/dev/null || true
kubectl delete pvc storage-check-vcd -n $NS 2>/dev/null || true
echo "=== Removing secret ==="
kubectl delete secret bench-s3-secret -n $NS 2>/dev/null || true
echo "Done."
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
NS=default
S3_KEY=0GLQRD38H4I6RBDB0EWJ
S3_SECRET=eTFibiHmBd96IApj9PYsboTR6OBoD7osxoarHykw
echo "=== PVCs ==="
kubectl apply -f pvc-local.yaml
kubectl apply -f pvc-vcd.yaml
echo "=== S3 Secret ==="
kubectl create secret generic bench-s3-secret \
--from-literal=access-key="$S3_KEY" \
--from-literal=secret-key="$S3_SECRET" \
-n $NS 2>/dev/null || echo "secret already exists"
echo "=== Environment (no resource limits to avoid quota) ==="
fission env create --name bench-py --image naeel/fission-python-env:v1.1 \
--mincpu 0 --maxcpu 0 --minmemory 0 --maxmemory 0 \
--poolsize 1 --namespace $NS 2>/dev/null || echo "env already exists"
echo "=== Functions ==="
# check-local и check-vcd используют newdeploy: 1 под = 1 PVC (RWO работает)
fission fn create --name check-local --env bench-py --code check_pv.py \
--executortype newdeploy --minscale 1 --maxscale 1 --namespace $NS 2>/dev/null || \
fission fn update --name check-local --code check_pv.py --namespace $NS
fission fn create --name check-vcd --env bench-py --code check_pv.py \
--executortype newdeploy --minscale 1 --maxscale 1 --namespace $NS 2>/dev/null || \
fission fn update --name check-vcd --code check_pv.py --namespace $NS
# check-s3 использует poolmgr
fission fn create --name check-s3 --env bench-py --code check_s3.py \
--namespace $NS --secret bench-s3-secret 2>/dev/null || \
fission fn update --name check-s3 --code check_s3.py --namespace $NS --secret bench-s3-secret
echo "=== Patching podspec (PVC mounts) ==="
kubectl patch function check-local -n $NS --type=merge -p "$(cat patch-local.json)"
kubectl patch function check-vcd -n $NS --type=merge -p "$(cat patch-vcd.json)"
echo "=== HTTP Triggers ==="
fission httptrigger create --name check-local-t --url /check/local --function check-local --method GET --namespace $NS 2>/dev/null || true
fission httptrigger create --name check-vcd-t --url /check/vcd --function check-vcd --method GET --namespace $NS 2>/dev/null || true
fission httptrigger create --name check-s3-t --url /check/s3 --function check-s3 --method GET --namespace $NS 2>/dev/null || true
echo ""
echo "Done! Жди ~60s (newdeploy pods + PVC binding), затем: ./test.sh"
@@ -0,0 +1,30 @@
apiVersion: fission.io/v1
kind: Function
metadata:
name: check-local
namespace: default
spec:
InvokeStrategy:
ExecutionStrategy:
ExecutorType: newdeploy
MinScale: 1
MaxScale: 1
StrategyType: execution
environment:
name: bench-py
namespace: default
package:
functionName: main.main
packageref:
name: check-local-7d087421-7802-4af1-b111-03d71fe738cd
namespace: default
podspec:
volumes:
- name: data
persistentVolumeClaim:
claimName: storage-check-local
containers:
- name: bench-py
volumeMounts:
- name: data
mountPath: /mnt/data
+30
View File
@@ -0,0 +1,30 @@
apiVersion: fission.io/v1
kind: Function
metadata:
name: check-vcd
namespace: default
spec:
InvokeStrategy:
ExecutionStrategy:
ExecutorType: newdeploy
MinScale: 1
MaxScale: 1
StrategyType: execution
environment:
name: bench-py
namespace: default
package:
functionName: main.main
packageref:
name: check-vcd-6e6076b2-2207-49f3-a12d-83fb50633285
namespace: default
podspec:
volumes:
- name: data
persistentVolumeClaim:
claimName: storage-check-vcd
containers:
- name: bench-py
volumeMounts:
- name: data
mountPath: /mnt/data
+25
View File
@@ -0,0 +1,25 @@
{
"spec": {
"podspec": {
"volumes": [
{
"name": "data",
"persistentVolumeClaim": {
"claimName": "storage-check-local"
}
}
],
"containers": [
{
"name": "bench-py",
"volumeMounts": [
{
"name": "data",
"mountPath": "/mnt/data"
}
]
}
]
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"spec": {
"podspec": {
"volumes": [
{
"name": "data",
"persistentVolumeClaim": {
"claimName": "storage-check-vcd"
}
}
],
"containers": [
{
"name": "bench-py",
"volumeMounts": [
{
"name": "data",
"mountPath": "/mnt/data"
}
]
}
]
}
}
}
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: storage-check-local
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 100Mi
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: storage-check-vcd
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: vcd-disk-ext4
resources:
requests:
storage: 10Gi
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -e
# Port-forward router to localhost:8888
kubectl port-forward svc/router 8888:80 -n fission &>/tmp/pf.log &
PF_PID=$!
trap "kill $PF_PID 2>/dev/null" EXIT
sleep 3
ROUTER="localhost:8888"
echo "Router via port-forward: $ROUTER"
echo ""
echo "=== [1/3] local-path ==="
curl -sf --max-time 30 "http://$ROUTER/check/local" | python3 -m json.tool || echo "ERROR: no response"
echo ""
echo "=== [2/3] vcd-disk-ext4 ==="
curl -sf --max-time 30 "http://$ROUTER/check/vcd" | python3 -m json.tool || echo "ERROR: no response"
echo ""
echo "=== [3/3] S3 (ngcloud) ==="
curl -sf --max-time 30 "http://$ROUTER/check/s3" | python3 -m json.tool || echo "ERROR: no response"
@@ -0,0 +1,79 @@
{
"dashboard": {
"title": "Weather Metrics",
"uid": "weather-metrics",
"timezone": "browser",
"refresh": "1m",
"time": { "from": "now-6h", "to": "now" },
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Temperature by City (°C)",
"gridPos": { "x": 0, "y": 0, "w": 24, "h": 10 },
"datasource": { "type": "grafana-postgresql-datasource", "uid": "fission-pg" },
"fieldConfig": {
"defaults": {
"unit": "celsius",
"custom": { "lineWidth": 2, "drawStyle": "line", "showPoints": "never", "spanNulls": true }
}
},
"options": { "legend": { "displayMode": "table", "placement": "right" } },
"targets": [
{
"refId": "A",
"rawSql": "SELECT\n recorded_at AS time,\n temperature,\n city\nFROM weather_metrics\nWHERE $__timeFilter(recorded_at)\nORDER BY recorded_at",
"format": "time_series"
}
]
},
{
"id": 2,
"type": "table",
"title": "Current Conditions",
"gridPos": { "x": 0, "y": 10, "w": 24, "h": 8 },
"datasource": { "type": "grafana-postgresql-datasource", "uid": "fission-pg" },
"options": { "sortBy": [{ "displayName": "City" }] },
"targets": [
{
"refId": "A",
"rawSql": "SELECT DISTINCT ON (city)\n city,\n country,\n temperature,\n feels_like,\n humidity,\n wind_speed,\n description,\n recorded_at\nFROM weather_metrics\nORDER BY city, recorded_at DESC",
"format": "table"
}
]
},
{
"id": 3,
"type": "timeseries",
"title": "Humidity by City (%)",
"gridPos": { "x": 0, "y": 18, "w": 12, "h": 8 },
"datasource": { "type": "grafana-postgresql-datasource", "uid": "fission-pg" },
"fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100, "custom": { "lineWidth": 2, "drawStyle": "line", "showPoints": "never", "spanNulls": true } } },
"targets": [
{
"refId": "A",
"rawSql": "SELECT\n recorded_at AS time,\n humidity,\n city\nFROM weather_metrics\nWHERE $__timeFilter(recorded_at)\nORDER BY recorded_at",
"format": "time_series"
}
]
},
{
"id": 4,
"type": "timeseries",
"title": "Wind Speed by City (m/s)",
"gridPos": { "x": 12, "y": 18, "w": 12, "h": 8 },
"datasource": { "type": "grafana-postgresql-datasource", "uid": "fission-pg" },
"fieldConfig": { "defaults": { "unit": "velocityms", "custom": { "lineWidth": 2, "drawStyle": "line", "showPoints": "never", "spanNulls": true } } },
"targets": [
{
"refId": "A",
"rawSql": "SELECT\n recorded_at AS time,\n wind_speed,\n city\nFROM weather_metrics\nWHERE $__timeFilter(recorded_at)\nORDER BY recorded_at",
"format": "time_series"
}
]
}
],
"schemaVersion": 38
},
"overwrite": true
}
-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
-66
View File
@@ -1,66 +0,0 @@
ntazetdinov@nubes.ru
stxVxLvM9eqssgZ
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWQ3NmM1LTQyNGMtN2MyYy04OWI3LWUzZjA5OTQ5YTk4ZiIsImV4cCI6MTc5MzA3ODQzOSwiaWF0IjoxNzc3NTI2NDM5LCJqdGkiOiI4MTU1ODNkNy0xZTRkLTRiMmMtOGM5ZS0wZDZmMGU2NmJjNTciLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiJXWjAxMTEyIiwiY29tcGFueV9pZCI6IjhlYzcwYWMwLTU0NmQtNDJhNy04Y2ZmLTMzOWM4ZmI1MWEyMyIsImNvbXBhbnlfbmFtZSI6ItCe0J7QniDCq9Cd0KPQkdCV0KHCuyIsImlkcF91c3JfdWlkIjoiMDE5ZDc2YzUtNDI0Yy03YzJjLTg5YjctZTNmMDk5NDlhOThmIiwibG9naW4iOiJudGF6ZXRkaW5vdkBudWJlcy5ydSIsImZpcnN0bmFtZSI6ItCd0LDQuNC70YwiLCJtaWRkbGVuYW1lIjoi0KTQsNGA0LjRgtC-0LLQuNGHIiwibGFzdG5hbWUiOiLQotCw0LfQtdGC0LTQuNC90L7QsiIsImdyb3VwcyI6bnVsbCwicHJlZmVycmVkX3VzZXJuYW1lIjoiIiwiZ2l2ZW5fbmFtZSI6IiIsImZhbWlseV9uYW1lIjoiIiwiZW1haWwiOiJudGF6ZXRkaW5vdkBudWJlcy5ydSJ9.SaZsKAha45-fvKuJkLHUe_09AFsbH5QpzBVdPnjiEDQGIhl1A3ThgnM-oEh_H6CBXXfnOs2QDczzgCD8K8_tYzZKU1Wgk5lj04YW_fbTI89kHTO5wLtrAht9tFEjzBQZ-kmnG8mUK5tgqyNEjsngdQcfqVWRvneF366TiiRXk_76poUPpXWQmqdgCCKb3wq1rRgWcKDZcUC3JvDpOaqYL40zITZlM855drlJhMut4Gkg-EEk7ZGykl6YCsTRWVjFKGQ-7C-BDU2dkmIWhxzBAkvzzM2kBwvtlczCEDiUin7bXE-F_io89oJUYDzY4yPeHzwREwxCp6x9eD1NAIKKuA
----------- token on VM --------------
https://deck-api-test.ngcloud.ru/api/v1 - для тест стенда где постгрес, файл с токеном -
naeel@naeel-vm:~/terra/sless$ ll test.token
-rw-r--r-- 1 naeel naeel 998 Feb 28 07:18 test.token
--------------------- pg IoT-PG -----------------------
users
JSON объект (раскройте для просмотра)
password:
BQUF5ruECa1ZFlq4wYt3gPJUEmtBMkA9QNK4MM5Sd8al4ArMDlmT16DIKHYBPyif
username:
super
Поля состояния (сгенерированные значения)
monitoring
{
"resourceMetrics": "https://grafana.ngcloud.ru/d/vzjb4zd/kubernetes-pods?orgId=954&var-namespace=dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5&var-resource_realm=iot-naeel"
}
users
{
"super": {
"role": "ddl_user",
"username": "super"
}
}
backups
{}
databases
{
"sqsdb": {
"dbName": "sqsdb",
"dbOwner": "super"
}
}
externalConnect
{
"slave": {
"ip": "",
"fqdn": "",
"uuid": "",
"isExternal": false
},
"master": {
"ip": "",
"fqdn": "",
"uuid": "",
"isExternal": false
}
}
internalConnect
{
"slave": "",
"master": "postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local"
}
+14
View File
@@ -0,0 +1,14 @@
<svg width="310mm" height="110mm" viewBox="0 0 310 110" xmlns="http://www.w3.org/2000/svg">
<!-- ОДНО наклонное кольцо: верхняя кромка синусоида y=40+40*sin(πx/L) -->
<polyline fill="none" stroke="#1976d2" stroke-width="2" points="
0,40 5,39.47 10,38.95 15,38.45 20,37.95 25,37.48 30,37.02 35,36.59 40,36.18 45,35.80 50,35.45 55,35.13 60,34.84 65,34.59 70,34.37 75,34.19 80,34.05 85,33.95 90,33.88 95,33.86 100,33.87 105,33.93 110,34.02 115,34.15 120,34.32 125,34.53 130,34.78 135,35.06 140,35.38 145,35.73 150,36.12 155,36.54 160,37.00 165,37.48 170,38.00 175,38.54 180,39.11 185,39.70 190,40.31 195,40.94 200,41.59 205,42.24 210,42.90 215,43.56 220,44.22 225,44.86 230,45.48 235,46.08 240,46.64 245,47.16 250,47.63 255,48.05 260,48.41 265,48.70 270,48.92 275,49.06 280,49.12 285,49.09 290,48.97 295,48.76 300,48.45 305,48.04
<!-- Нижняя кромка синусоида y=60+40*sin(πx/L) -->
<polyline fill="none" stroke="#d32f2f" stroke-width="2" points="
0,60 5,59.47 10,58.95 15,58.45 20,57.95 25,57.48 30,57.02 35,56.59 40,56.18 45,55.80 50,55.45 55,55.13 60,54.84 65,54.59 70,54.37 75,54.19 80,54.05 85,53.95 90,53.88 95,53.86 100,53.87 105,53.93 110,54.02 115,54.15 120,54.32 125,54.53 130,54.78 135,55.06 140,55.38 145,55.73 150,56.12 155,56.54 160,57.00 165,57.48 170,58.00 175,58.54 180,59.11 185,59.70 190,60.31 195,60.94 200,61.59 205,62.24 210,62.90 215,63.56 220,64.22 225,64.86 230,65.48 235,66.08 240,66.64 245,67.16 250,67.63 255,68.05 260,68.41 265,68.70 270,68.92 275,69.06 280,69.12 285,69.09 290,68.97 295,68.76 300,68.45 305,68.04
" />
<!-- Заливка между кромками -->
<polygon fill="#e0f7fa" fill-opacity="0.6" stroke="none" points="
0,40 5,39.47 10,38.95 15,38.45 20,37.95 25,37.48 30,37.02 35,36.59 40,36.18 45,35.80 50,35.45 55,35.13 60,34.84 65,34.59 70,34.37 75,34.19 80,34.05 85,33.95 90,33.88 95,33.86 100,33.87 105,33.93 110,34.02 115,34.15 120,34.32 125,34.53 130,34.78 135,35.06 140,35.38 145,35.73 150,36.12 155,36.54 160,37.00 165,37.48 170,38.00 175,38.54 180,39.11 185,39.70 190,40.31 195,40.94 200,41.59 205,42.24 210,42.90 215,43.56 220,44.22 225,44.86 230,45.48 235,46.08 240,46.64 245,47.16 250,47.63 255,48.05 260,48.41 265,48.70 270,48.92 275,49.06 280,49.12 285,49.09 290,48.97 295,48.76 300,48.45 305,48.04
305,68.04 300,68.45 295,68.76 290,68.97 285,69.09 280,69.12 275,69.06 270,68.92 265,68.70 260,68.41 255,68.05 250,67.63 245,67.16 240,66.64 235,66.08 230,65.48 225,64.86 220,64.22 215,63.56 210,62.90 205,62.24 200,61.59 195,60.94 190,60.31 185,59.70 180,59.11 175,58.54 170,58.00 165,57.48 160,57.00 155,56.54 150,56.12 145,55.73 140,55.38 135,55.06 130,54.78 125,54.53 120,54.32 115,54.15 110,54.02 105,53.93 100,53.87 95,53.86 90,53.88 85,53.95 80,54.05 75,54.19 70,54.37 65,54.59 60,54.84 55,55.13 50,55.45 45,55.80 40,56.18 35,56.59 30,57.02 25,57.48 20,57.95 15,58.45 10,58.95 5,59.47 0,60
" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="301.6mm" height="120mm" viewBox="0 0 301.6 120" xmlns="http://www.w3.org/2000/svg">
<polyline fill="none" stroke="#1976d2" stroke-width="1.5" points="0.0,50.00 3.0,52.51 6.0,55.01 9.0,57.50 12.1,59.95 15.1,62.36 18.1,64.72 21.1,67.03 24.1,69.27 27.1,71.43 30.2,73.51 33.2,75.50 36.2,77.38 39.2,79.16 42.2,80.82 45.2,82.36 48.3,83.77 51.3,85.05 54.3,86.19 57.3,87.19 60.3,88.04 63.3,88.74 66.4,89.29 69.4,89.68 72.4,89.92 75.4,90.00 78.4,89.92 81.4,89.68 84.4,89.29 87.5,88.74 90.5,88.04 93.5,87.19 96.5,86.19 99.5,85.05 102.5,83.77 105.6,82.36 108.6,80.82 111.6,79.16 114.6,77.38 117.6,75.50 120.6,73.51 123.7,71.43 126.7,69.27 129.7,67.03 132.7,64.72 135.7,62.36 138.7,59.95 141.7,57.50 144.8,55.01 147.8,52.51 150.8,50.00 153.8,47.49 156.8,44.99 159.8,42.50 162.9,40.05 165.9,37.64 168.9,35.28 171.9,32.97 174.9,30.73 177.9,28.57 181.0,26.49 184.0,24.50 187.0,22.62 190.0,20.84 193.0,19.18 196.0,17.64 199.1,16.23 202.1,14.95 205.1,13.81 208.1,12.81 211.1,11.96 214.1,11.26 217.1,10.71 220.2,10.32 223.2,10.08 226.2,10.00 229.2,10.08 232.2,10.32 235.2,10.71 238.3,11.26 241.3,11.96 244.3,12.81 247.3,13.81 250.3,14.95 253.3,16.23 256.4,17.64 259.4,19.18 262.4,20.84 265.4,22.62 268.4,24.50 271.4,26.49 274.4,28.57 277.5,30.73 280.5,32.97 283.5,35.28 286.5,37.64 289.5,40.05 292.5,42.50 295.6,44.99 298.6,47.49 301.6,50.00" />
<polyline fill="none" stroke="#d32f2f" stroke-width="1.5" points="0.0,70.00 3.0,72.51 6.0,75.01 9.0,77.50 12.1,79.95 15.1,82.36 18.1,84.72 21.1,87.03 24.1,89.27 27.1,91.43 30.2,93.51 33.2,95.50 36.2,97.38 39.2,99.16 42.2,100.82 45.2,102.36 48.3,103.77 51.3,105.05 54.3,106.19 57.3,107.19 60.3,108.04 63.3,108.74 66.4,109.29 69.4,109.68 72.4,109.92 75.4,110.00 78.4,109.92 81.4,109.68 84.4,109.29 87.5,108.74 90.5,108.04 93.5,107.19 96.5,106.19 99.5,105.05 102.5,103.77 105.6,102.36 108.6,100.82 111.6,99.16 114.6,97.38 117.6,95.50 120.6,93.51 123.7,91.43 126.7,89.27 129.7,87.03 132.7,84.72 135.7,82.36 138.7,79.95 141.7,77.50 144.8,75.01 147.8,72.51 150.8,70.00 153.8,67.49 156.8,64.99 159.8,62.50 162.9,60.05 165.9,57.64 168.9,55.28 171.9,52.97 174.9,50.73 177.9,48.57 181.0,46.49 184.0,44.50 187.0,42.62 190.0,40.84 193.0,39.18 196.0,37.64 199.1,36.23 202.1,34.95 205.1,33.81 208.1,32.81 211.1,31.96 214.1,31.26 217.1,30.71 220.2,30.32 223.2,30.08 226.2,30.00 229.2,30.08 232.2,30.32 235.2,30.71 238.3,31.26 241.3,31.96 244.3,32.81 247.3,33.81 250.3,34.95 253.3,36.23 256.4,37.64 259.4,39.18 262.4,40.84 265.4,42.62 268.4,44.50 271.4,46.49 274.4,48.57 277.5,50.73 280.5,52.97 283.5,55.28 286.5,57.64 289.5,60.05 292.5,62.50 295.6,64.99 298.6,67.49 301.6,70.00" />
<polygon fill="#e0f7fa" fill-opacity="0.5" stroke="none" points="0.0,50.00 3.0,52.51 6.0,55.01 9.0,57.50 12.1,59.95 15.1,62.36 18.1,64.72 21.1,67.03 24.1,69.27 27.1,71.43 30.2,73.51 33.2,75.50 36.2,77.38 39.2,79.16 42.2,80.82 45.2,82.36 48.3,83.77 51.3,85.05 54.3,86.19 57.3,87.19 60.3,88.04 63.3,88.74 66.4,89.29 69.4,89.68 72.4,89.92 75.4,90.00 78.4,89.92 81.4,89.68 84.4,89.29 87.5,88.74 90.5,88.04 93.5,87.19 96.5,86.19 99.5,85.05 102.5,83.77 105.6,82.36 108.6,80.82 111.6,79.16 114.6,77.38 117.6,75.50 120.6,73.51 123.7,71.43 126.7,69.27 129.7,67.03 132.7,64.72 135.7,62.36 138.7,59.95 141.7,57.50 144.8,55.01 147.8,52.51 150.8,50.00 153.8,47.49 156.8,44.99 159.8,42.50 162.9,40.05 165.9,37.64 168.9,35.28 171.9,32.97 174.9,30.73 177.9,28.57 181.0,26.49 184.0,24.50 187.0,22.62 190.0,20.84 193.0,19.18 196.0,17.64 199.1,16.23 202.1,14.95 205.1,13.81 208.1,12.81 211.1,11.96 214.1,11.26 217.1,10.71 220.2,10.32 223.2,10.08 226.2,10.00 229.2,10.08 232.2,10.32 235.2,10.71 238.3,11.26 241.3,11.96 244.3,12.81 247.3,13.81 250.3,14.95 253.3,16.23 256.4,17.64 259.4,19.18 262.4,20.84 265.4,22.62 268.4,24.50 271.4,26.49 274.4,28.57 277.5,30.73 280.5,32.97 283.5,35.28 286.5,37.64 289.5,40.05 292.5,42.50 295.6,44.99 298.6,47.49 301.6,50.00 301.6,70.00 298.6,67.49 295.6,64.99 292.5,62.50 289.5,60.05 286.5,57.64 283.5,55.28 280.5,52.97 277.5,50.73 274.4,48.57 271.4,46.49 268.4,44.50 265.4,42.62 262.4,40.84 259.4,39.18 256.4,37.64 253.3,36.23 250.3,34.95 247.3,33.81 244.3,32.81 241.3,31.96 238.3,31.26 235.2,30.71 232.2,30.32 229.2,30.08 226.2,30.00 223.2,30.08 220.2,30.32 217.1,30.71 214.1,31.26 211.1,31.96 208.1,32.81 205.1,33.81 202.1,34.95 199.1,36.23 196.0,37.64 193.0,39.18 190.0,40.84 187.0,42.62 184.0,44.50 181.0,46.49 177.9,48.57 174.9,50.73 171.9,52.97 168.9,55.28 165.9,57.64 162.9,60.05 159.8,62.50 156.8,64.99 153.8,67.49 150.8,70.00 147.8,72.51 144.8,75.01 141.7,77.50 138.7,79.95 135.7,82.36 132.7,84.72 129.7,87.03 126.7,89.27 123.7,91.43 120.6,93.51 117.6,95.50 114.6,97.38 111.6,99.16 108.6,100.82 105.6,102.36 102.5,103.77 99.5,105.05 96.5,106.19 93.5,107.19 90.5,108.04 87.5,108.74 84.4,109.29 81.4,109.68 78.4,109.92 75.4,110.00 72.4,109.92 69.4,109.68 66.4,109.29 63.3,108.74 60.3,108.04 57.3,107.19 54.3,106.19 51.3,105.05 48.3,103.77 45.2,102.36 42.2,100.82 39.2,99.16 36.2,97.38 33.2,95.50 30.2,93.51 27.1,91.43 24.1,89.27 21.1,87.03 18.1,84.72 15.1,82.36 12.1,79.95 9.0,77.50 6.0,75.01 3.0,72.51 0.0,70.00" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+3
View File
@@ -0,0 +1,3 @@
Диаметр цилиндра: 96 мм
Вертикальная проекция большой оси эллипса: 80 мм
Расстояние между срезами по вертикали: 20 мм
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="150.8mm" height="120mm" viewBox="0 0 150.8 120" xmlns="http://www.w3.org/2000/svg">
<!-- Часть 1 (левая) -->
<polygon fill="#e0f7fa" fill-opacity="0.5" stroke="none" points="0.00,50.00 1.51,51.26 3.02,52.51 4.52,53.76 6.03,55.01 7.54,56.26 9.05,57.50 10.56,58.73 12.06,59.95 13.57,61.16 15.08,62.36 16.59,63.55 18.10,64.72 19.60,65.89 21.11,67.03 22.62,68.16 24.13,69.27 25.64,70.36 27.14,71.43 28.65,72.48 30.16,73.51 31.67,74.52 33.18,75.50 34.68,76.45 36.19,77.38 37.70,78.28 39.21,79.16 40.72,80.00 42.22,80.82 43.73,81.61 45.24,82.36 46.75,83.08 48.25,83.77 49.76,84.43 51.27,85.05 52.78,85.64 54.29,86.19 55.79,86.71 57.30,87.19 58.81,87.64 60.32,88.04 61.83,88.41 63.33,88.74 64.84,89.04 66.35,89.29 67.86,89.51 69.37,89.68 70.87,89.82 72.38,89.92 73.89,89.98 75.40,90.00 76.91,89.98 78.41,89.92 79.92,89.82 81.43,89.68 82.94,89.51 84.45,89.29 85.95,89.04 87.46,88.74 88.97,88.41 90.48,88.04 91.99,87.64 93.49,87.19 95.00,86.71 96.51,86.19 98.02,85.64 99.53,85.05 101.03,84.43 102.54,83.77 104.05,83.08 105.56,82.36 107.07,81.61 108.57,80.82 110.08,80.00 111.59,79.16 113.10,78.28 114.61,77.38 116.11,76.45 117.62,75.50 119.13,74.52 120.64,73.51 122.15,72.48 123.65,71.43 125.16,70.36 126.67,69.27 128.18,68.16 129.68,67.03 131.19,65.89 132.70,64.72 134.21,63.55 135.72,62.36 137.22,61.16 138.73,59.95 140.24,58.73 141.75,57.50 143.26,56.26 144.76,55.01 146.27,53.76 147.78,52.51 149.29,51.26 150.80,50.00 150.80,70.00 149.29,71.26 147.78,72.51 146.27,73.76 144.76,75.01 143.26,76.26 141.75,77.50 140.24,78.73 138.73,79.95 137.22,81.16 135.72,82.36 134.21,83.55 132.70,84.72 131.19,85.89 129.68,87.03 128.18,88.16 126.67,89.27 125.16,90.36 123.65,91.43 122.15,92.48 120.64,93.51 119.13,94.52 117.62,95.50 116.11,96.45 114.61,97.38 113.10,98.28 111.59,99.16 110.08,100.00 108.57,100.82 107.07,101.61 105.56,102.36 104.05,103.08 102.54,103.77 101.03,104.43 99.53,105.05 98.02,105.64 96.51,106.19 95.00,106.71 93.49,107.19 91.99,107.64 90.48,108.04 88.97,108.41 87.46,108.74 85.95,109.04 84.45,109.29 82.94,109.51 81.43,109.68 79.92,109.82 78.41,109.92 76.91,109.98 75.40,110.00 73.89,109.98 72.38,109.92 70.87,109.82 69.37,109.68 67.86,109.51 66.35,109.29 64.84,109.04 63.33,108.74 61.83,108.41 60.32,108.04 58.81,107.64 57.30,107.19 55.79,106.71 54.29,106.19 52.78,105.64 51.27,105.05 49.76,104.43 48.25,103.77 46.75,103.08 45.24,102.36 43.73,101.61 42.22,100.82 40.72,100.00 39.21,99.16 37.70,98.28 36.19,97.38 34.68,96.45 33.18,95.50 31.67,94.52 30.16,93.51 28.65,92.48 27.14,91.43 25.64,90.36 24.13,89.27 22.62,88.16 21.11,87.03 19.60,85.89 18.10,84.72 16.59,83.55 15.08,82.36 13.57,81.16 12.06,79.95 10.56,78.73 9.05,77.50 7.54,76.26 6.03,75.01 4.52,73.76 3.02,72.51 1.51,71.26 0.00,70.00" />
<polyline fill="none" stroke="#1976d2" stroke-width="1.5" points="0.00,50.00 1.51,51.26 3.02,52.51 4.52,53.76 6.03,55.01 7.54,56.26 9.05,57.50 10.56,58.73 12.06,59.95 13.57,61.16 15.08,62.36 16.59,63.55 18.10,64.72 19.60,65.89 21.11,67.03 22.62,68.16 24.13,69.27 25.64,70.36 27.14,71.43 28.65,72.48 30.16,73.51 31.67,74.52 33.18,75.50 34.68,76.45 36.19,77.38 37.70,78.28 39.21,79.16 40.72,80.00 42.22,80.82 43.73,81.61 45.24,82.36 46.75,83.08 48.25,83.77 49.76,84.43 51.27,85.05 52.78,85.64 54.29,86.19 55.79,86.71 57.30,87.19 58.81,87.64 60.32,88.04 61.83,88.41 63.33,88.74 64.84,89.04 66.35,89.29 67.86,89.51 69.37,89.68 70.87,89.82 72.38,89.92 73.89,89.98 75.40,90.00 76.91,89.98 78.41,89.92 79.92,89.82 81.43,89.68 82.94,89.51 84.45,89.29 85.95,89.04 87.46,88.74 88.97,88.41 90.48,88.04 91.99,87.64 93.49,87.19 95.00,86.71 96.51,86.19 98.02,85.64 99.53,85.05 101.03,84.43 102.54,83.77 104.05,83.08 105.56,82.36 107.07,81.61 108.57,80.82 110.08,80.00 111.59,79.16 113.10,78.28 114.61,77.38 116.11,76.45 117.62,75.50 119.13,74.52 120.64,73.51 122.15,72.48 123.65,71.43 125.16,70.36 126.67,69.27 128.18,68.16 129.68,67.03 131.19,65.89 132.70,64.72 134.21,63.55 135.72,62.36 137.22,61.16 138.73,59.95 140.24,58.73 141.75,57.50 143.26,56.26 144.76,55.01 146.27,53.76 147.78,52.51 149.29,51.26 150.80,50.00" />
<polyline fill="none" stroke="#d32f2f" stroke-width="1.5" points="0.00,70.00 1.51,71.26 3.02,72.51 4.52,73.76 6.03,75.01 7.54,76.26 9.05,77.50 10.56,78.73 12.06,79.95 13.57,81.16 15.08,82.36 16.59,83.55 18.10,84.72 19.60,85.89 21.11,87.03 22.62,88.16 24.13,89.27 25.64,90.36 27.14,91.43 28.65,92.48 30.16,93.51 31.67,94.52 33.18,95.50 34.68,96.45 36.19,97.38 37.70,98.28 39.21,99.16 40.72,100.00 42.22,100.82 43.73,101.61 45.24,102.36 46.75,103.08 48.25,103.77 49.76,104.43 51.27,105.05 52.78,105.64 54.29,106.19 55.79,106.71 57.30,107.19 58.81,107.64 60.32,108.04 61.83,108.41 63.33,108.74 64.84,109.04 66.35,109.29 67.86,109.51 69.37,109.68 70.87,109.82 72.38,109.92 73.89,109.98 75.40,110.00 76.91,109.98 78.41,109.92 79.92,109.82 81.43,109.68 82.94,109.51 84.45,109.29 85.95,109.04 87.46,108.74 88.97,108.41 90.48,108.04 91.99,107.64 93.49,107.19 95.00,106.71 96.51,106.19 98.02,105.64 99.53,105.05 101.03,104.43 102.54,103.77 104.05,103.08 105.56,102.36 107.07,101.61 108.57,100.82 110.08,100.00 111.59,99.16 113.10,98.28 114.61,97.38 116.11,96.45 117.62,95.50 119.13,94.52 120.64,93.51 122.15,92.48 123.65,91.43 125.16,90.36 126.67,89.27 128.18,88.16 129.68,87.03 131.19,85.89 132.70,84.72 134.21,83.55 135.72,82.36 137.22,81.16 138.73,79.95 140.24,78.73 141.75,77.50 143.26,76.26 144.76,75.01 146.27,73.76 147.78,72.51 149.29,71.26 150.80,70.00" />
<!-- линия склейки -->
<line x1="150.8" y1="0" x2="150.8" y2="120" stroke="#888" stroke-width="0.5" stroke-dasharray="3,3"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="150.8mm" height="120mm" viewBox="0 0 150.8 120" xmlns="http://www.w3.org/2000/svg">
<!-- Часть 2 (правая) -->
<polygon fill="#e0f7fa" fill-opacity="0.5" stroke="none" points="0.00,50.00 1.51,48.74 3.02,47.49 4.52,46.24 6.03,44.99 7.54,43.74 9.05,42.50 10.56,41.27 12.06,40.05 13.57,38.84 15.08,37.64 16.59,36.45 18.10,35.28 19.60,34.11 21.11,32.97 22.62,31.84 24.13,30.73 25.64,29.64 27.14,28.57 28.65,27.52 30.16,26.49 31.67,25.48 33.18,24.50 34.68,23.55 36.19,22.62 37.70,21.72 39.21,20.84 40.72,20.00 42.22,19.18 43.73,18.39 45.24,17.64 46.75,16.92 48.25,16.23 49.76,15.57 51.27,14.95 52.78,14.36 54.29,13.81 55.79,13.29 57.30,12.81 58.81,12.36 60.32,11.96 61.83,11.59 63.33,11.26 64.84,10.96 66.35,10.71 67.86,10.49 69.37,10.32 70.87,10.18 72.38,10.08 73.89,10.02 75.40,10.00 76.91,10.02 78.41,10.08 79.92,10.18 81.43,10.32 82.94,10.49 84.45,10.71 85.95,10.96 87.46,11.26 88.97,11.59 90.48,11.96 91.99,12.36 93.49,12.81 95.00,13.29 96.51,13.81 98.02,14.36 99.53,14.95 101.03,15.57 102.54,16.23 104.05,16.92 105.56,17.64 107.07,18.39 108.57,19.18 110.08,20.00 111.59,20.84 113.10,21.72 114.61,22.62 116.11,23.55 117.62,24.50 119.13,25.48 120.64,26.49 122.15,27.52 123.65,28.57 125.16,29.64 126.67,30.73 128.18,31.84 129.68,32.97 131.19,34.11 132.70,35.28 134.21,36.45 135.72,37.64 137.22,38.84 138.73,40.05 140.24,41.27 141.75,42.50 143.26,43.74 144.76,44.99 146.27,46.24 147.78,47.49 149.29,48.74 150.80,50.00 150.80,70.00 149.29,68.74 147.78,67.49 146.27,66.24 144.76,64.99 143.26,63.74 141.75,62.50 140.24,61.27 138.73,60.05 137.22,58.84 135.72,57.64 134.21,56.45 132.70,55.28 131.19,54.11 129.68,52.97 128.18,51.84 126.67,50.73 125.16,49.64 123.65,48.57 122.15,47.52 120.64,46.49 119.13,45.48 117.62,44.50 116.11,43.55 114.61,42.62 113.10,41.72 111.59,40.84 110.08,40.00 108.57,39.18 107.07,38.39 105.56,37.64 104.05,36.92 102.54,36.23 101.03,35.57 99.53,34.95 98.02,34.36 96.51,33.81 95.00,33.29 93.49,32.81 91.99,32.36 90.48,31.96 88.97,31.59 87.46,31.26 85.95,30.96 84.45,30.71 82.94,30.49 81.43,30.32 79.92,30.18 78.41,30.08 76.91,30.02 75.40,30.00 73.89,30.02 72.38,30.08 70.87,30.18 69.37,30.32 67.86,30.49 66.35,30.71 64.84,30.96 63.33,31.26 61.83,31.59 60.32,31.96 58.81,32.36 57.30,32.81 55.79,33.29 54.29,33.81 52.78,34.36 51.27,34.95 49.76,35.57 48.25,36.23 46.75,36.92 45.24,37.64 43.73,38.39 42.22,39.18 40.72,40.00 39.21,40.84 37.70,41.72 36.19,42.62 34.68,43.55 33.18,44.50 31.67,45.48 30.16,46.49 28.65,47.52 27.14,48.57 25.64,49.64 24.13,50.73 22.62,51.84 21.11,52.97 19.60,54.11 18.10,55.28 16.59,56.45 15.08,57.64 13.57,58.84 12.06,60.05 10.56,61.27 9.05,62.50 7.54,63.74 6.03,64.99 4.52,66.24 3.02,67.49 1.51,68.74 0.00,70.00" />
<polyline fill="none" stroke="#1976d2" stroke-width="1.5" points="0.00,50.00 1.51,48.74 3.02,47.49 4.52,46.24 6.03,44.99 7.54,43.74 9.05,42.50 10.56,41.27 12.06,40.05 13.57,38.84 15.08,37.64 16.59,36.45 18.10,35.28 19.60,34.11 21.11,32.97 22.62,31.84 24.13,30.73 25.64,29.64 27.14,28.57 28.65,27.52 30.16,26.49 31.67,25.48 33.18,24.50 34.68,23.55 36.19,22.62 37.70,21.72 39.21,20.84 40.72,20.00 42.22,19.18 43.73,18.39 45.24,17.64 46.75,16.92 48.25,16.23 49.76,15.57 51.27,14.95 52.78,14.36 54.29,13.81 55.79,13.29 57.30,12.81 58.81,12.36 60.32,11.96 61.83,11.59 63.33,11.26 64.84,10.96 66.35,10.71 67.86,10.49 69.37,10.32 70.87,10.18 72.38,10.08 73.89,10.02 75.40,10.00 76.91,10.02 78.41,10.08 79.92,10.18 81.43,10.32 82.94,10.49 84.45,10.71 85.95,10.96 87.46,11.26 88.97,11.59 90.48,11.96 91.99,12.36 93.49,12.81 95.00,13.29 96.51,13.81 98.02,14.36 99.53,14.95 101.03,15.57 102.54,16.23 104.05,16.92 105.56,17.64 107.07,18.39 108.57,19.18 110.08,20.00 111.59,20.84 113.10,21.72 114.61,22.62 116.11,23.55 117.62,24.50 119.13,25.48 120.64,26.49 122.15,27.52 123.65,28.57 125.16,29.64 126.67,30.73 128.18,31.84 129.68,32.97 131.19,34.11 132.70,35.28 134.21,36.45 135.72,37.64 137.22,38.84 138.73,40.05 140.24,41.27 141.75,42.50 143.26,43.74 144.76,44.99 146.27,46.24 147.78,47.49 149.29,48.74 150.80,50.00" />
<polyline fill="none" stroke="#d32f2f" stroke-width="1.5" points="0.00,70.00 1.51,68.74 3.02,67.49 4.52,66.24 6.03,64.99 7.54,63.74 9.05,62.50 10.56,61.27 12.06,60.05 13.57,58.84 15.08,57.64 16.59,56.45 18.10,55.28 19.60,54.11 21.11,52.97 22.62,51.84 24.13,50.73 25.64,49.64 27.14,48.57 28.65,47.52 30.16,46.49 31.67,45.48 33.18,44.50 34.68,43.55 36.19,42.62 37.70,41.72 39.21,40.84 40.72,40.00 42.22,39.18 43.73,38.39 45.24,37.64 46.75,36.92 48.25,36.23 49.76,35.57 51.27,34.95 52.78,34.36 54.29,33.81 55.79,33.29 57.30,32.81 58.81,32.36 60.32,31.96 61.83,31.59 63.33,31.26 64.84,30.96 66.35,30.71 67.86,30.49 69.37,30.32 70.87,30.18 72.38,30.08 73.89,30.02 75.40,30.00 76.91,30.02 78.41,30.08 79.92,30.18 81.43,30.32 82.94,30.49 84.45,30.71 85.95,30.96 87.46,31.26 88.97,31.59 90.48,31.96 91.99,32.36 93.49,32.81 95.00,33.29 96.51,33.81 98.02,34.36 99.53,34.95 101.03,35.57 102.54,36.23 104.05,36.92 105.56,37.64 107.07,38.39 108.57,39.18 110.08,40.00 111.59,40.84 113.10,41.72 114.61,42.62 116.11,43.55 117.62,44.50 119.13,45.48 120.64,46.49 122.15,47.52 123.65,48.57 125.16,49.64 126.67,50.73 128.18,51.84 129.68,52.97 131.19,54.11 132.70,55.28 134.21,56.45 135.72,57.64 137.22,58.84 138.73,60.05 140.24,61.27 141.75,62.50 143.26,63.74 144.76,64.99 146.27,66.24 147.78,67.49 149.29,68.74 150.80,70.00" />
<!-- линия склейки -->
<line x1="150.8" y1="0" x2="150.8" y2="120" stroke="#888" stroke-width="0.5" stroke-dasharray="3,3"/>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg width="302mm" height="100mm" viewBox="0 0 302 100" xmlns="http://www.w3.org/2000/svg">
<!-- UPPER edge (blue) — синусоида от y=0 до y=80 mm -->
<polyline fill="none" stroke="#1976d2" stroke-width="2" points="
0.00,40.00 1.51,35.97 3.02,32.44 4.53,29.44 6.04,27.04 7.55,25.23 9.06,24.07 10.57,23.62 12.08,23.92 13.59,24.98 15.10,26.81 16.61,29.41 18.12,32.75 19.63,36.82 21.14,41.58 22.65,46.98 24.16,52.96 25.67,59.45 27.18,66.36 28.69,73.62 30.20,81.16 31.71,88.92 33.22,96.82 34.73,104.78 36.24,112.73 37.75,120.60 39.26,128.30 40.77,135.78 42.28,142.97 43.79,149.82 45.30,156.25 46.81,162.21 48.32,167.64 49.83,172.49 51.34,176.71 52.85,180.24 54.36,183.03 55.87,185.05 57.38,186.28 58.89,186.70 60.40,186.28 61.91,185.05 63.42,183.03 64.93,180.24 66.44,176.71 67.95,172.49 69.46,167.64 70.97,162.21 72.48,156.25 73.99,149.82 75.50,142.97 77.01,135.78 78.52,128.30 80.03,120.60 81.54,112.73 83.05,104.78 84.56,96.82 86.07,88.92 87.58,81.16 89.09,73.62 90.60,66.36 92.11,59.45 93.62,52.96 95.13,46.98 96.64,41.58 98.15,36.82 99.66,32.75 101.17,29.41 102.68,26.81 104.19,24.98 105.70,23.92 107.21,23.62 108.72,24.07 110.23,25.23 111.74,27.04 113.25,29.44 114.76,32.44 116.27,35.97 117.78,40.00 119.29,44.50 120.80,49.42 122.31,54.70 123.82,60.29 125.33,66.14 126.84,72.19 128.35,78.38 129.86,84.65 131.37,90.94 132.88,97.18 134.39,103.30 135.90,109.23 137.41,114.90 138.92,120.24 140.43,125.18 141.94,129.66 143.45,133.62 144.96,137.00 146.47,139.73 147.98,141.77 149.49,143.07 151.00,143.61 152.51,143.35 154.02,142.27 155.53,140.35 157.04,137.57 158.55,133.92 160.06,129.40 161.57,124.00 163.08,117.70 164.59,110.50 166.10,102.40 167.61,93.41 169.12,83.55 170.63,72.88 172.14,61.49 173.65,49.49 175.16,37.02 176.67,24.23 178.18,11.26 179.69,-1.76 181.20,-14.80 182.71,-27.74 184.22,-40.50 185.73,-52.94 187.24,-64.99 188.75,-76.54 190.26,-87.53 191.77,-97.79 193.28,-107.27 194.79,-115.88 196.30,-123.55 197.81,-130.22 199.32,-135.81 200.83,-140.26 202.34,-143.52 203.85,-145.54 205.36,-146.30 206.87,-145.77 208.38,-143.92 209.89,-140.75 211.40,-136.24 212.91,-130.38 214.42,-123.17 215.93,-114.63 217.44,-104.76 218.95,-93.61 220.46,-81.23 221.97,-67.66 223.48,-52.98 224.99,-37.27 226.50,-20.61 228.01,-3.10 229.52,15.57 231.03,35.43 232.54,56.24 234.05,77.85 235.56,100.10 237.07,122.82 238.58,145.86 240.09,169.08 241.60,192.31 243.11,215.41 244.62,238.14 246.13,260.38 247.64,281.98 249.15,302.80 250.66,322.71 252.17,341.57 253.68,359.24 255.19,375.61 256.70,390.57 258.21,403.98 259.72,415.76 261.23,425.78 262.74,433.99 264.25,440.36 265.76,444.87 267.27,447.50 268.78,448.24 270.29,447.09 271.80,444.04 273.31,439.11 274.82,432.30 276.33,423.64 278.18,413.17 278.36,403.12 279.35,399.86 280.86,388.78 282.37,376.32 283.88,362.74 285.39,348.28 286.90,333.16 288.41,317.54 289.92,301.57 291.43,285.38 292.94,269.11 294.45,252.90 295.96,236.88 297.47,221.18 298.98,206.00 300.49,191.44 302.00,177.65
" />
<!-- LOWER edge (red) — синусоида 20 mm ниже (y = base + 20) -->
<polyline fill="none" stroke="#d32f2f" stroke-width="2" points="
0.00,60.00 1.51,55.97 3.02,52.44 4.53,49.44 6.04,47.04 7.55,45.23 9.06,44.07 10.57,43.62 12.08,43.92 13.59,44.98 15.10,46.81 16.61,49.41 18.12,52.75 19.63,56.82 21.14,61.58 22.65,66.98 24.16,72.96 25.67,79.45 27.18,86.36 28.69,93.62 30.20,101.16 31.71,108.92 33.22,116.82 34.73,124.78 36.24,132.73 37.75,140.60 39.26,148.30 40.77,155.78 42.28,162.97 43.79,169.82 45.30,176.25 46.81,182.21 48.32,187.64 49.83,192.49 51.34,196.71 52.85,200.24 54.36,203.03 55.87,205.05 57.38,206.28 58.89,206.70 60.40,206.28 61.91,205.05 63.42,203.03 64.93,200.24 66.44,196.71 67.95,192.49 69.46,187.64 70.97,182.21 72.48,176.25 73.99,169.82 75.50,162.97 77.01,155.78 78.52,148.30 80.03,140.60 81.54,132.73 83.05,124.78 84.56,116.82 86.07,108.92 87.58,101.16 89.09,93.62 90.60,86.36 92.11,79.45 93.62,72.96 95.13,66.98 96.64,61.58 98.15,56.82 99.66,52.75 101.17,49.41 102.68,46.81 104.19,44.98 105.70,43.92 107.21,43.62 108.72,44.07 110.23,45.23 111.74,47.04 113.25,49.44 114.76,52.44 116.27,55.97 117.78,60.00 119.29,64.50 120.80,69.42 122.31,74.70 123.82,80.29 125.33,86.14 126.84,92.19 128.35,98.38 129.86,104.65 131.37,110.94 132.88,117.18 134.39,123.30 135.90,129.23 137.41,134.90 138.92,140.24 140.43,145.18 141.94,149.66 143.45,153.62 144.96,157.00 146.47,159.73 147.98,161.77 149.49,163.07 151.00,163.61 152.51,163.35 154.02,162.27 155.53,160.35 157.04,157.57 158.55,153.92 160.06,149.40 161.57,144.00 163.08,137.70 164.59,130.50 166.10,122.40 167.61,113.41 169.12,103.55 170.63,92.88 172.14,81.49 173.65,69.49 175.16,57.02 176.67,44.23 178.18,31.26 179.69,18.24 181.20,5.20 182.71,-7.74 184.22,-20.50 185.73,-32.94 187.24,-44.99 188.75,-56.54 190.26,-67.53 191.77,-77.79 193.28,-87.27 194.79,-95.88 196.30,-103.55 197.81,-110.22 199.32,-115.81 200.83,-120.26 202.34,-123.52 203.85,-125.54 205.36,-126.30 206.87,-125.77 208.38,-123.92 209.89,-120.75 211.40,-116.24 212.91,-110.38 214.42,-103.17 215.93,-94.63 217.44,-84.76 218.95,-73.61 220.46,-61.23 221.97,-47.66 223.48,-32.98 224.99,-17.27 226.50,-0.61 228.01,16.89 229.52,35.43 231.03,55.43 232.54,76.24 234.05,97.85 235.56,120.10 237.07,142.82 238.58,165.86 240.09,189.08 241.60,212.31 243.11,235.41 244.62,258.14 246.13,280.38 247.64,301.98 249.15,322.80 250.66,342.71 252.17,361.57 253.68,379.24 255.19,395.61 256.70,410.57 258.21,423.98 259.72,435.76 261.23,445.78 262.74,453.99 264.25,460.36 265.76,464.87 267.27,467.50 268.78,468.24 270.29,467.09 271.80,464.04 273.31,459.11 274.82,452.30 276.33,443.64 278.18,433.17 278.36,423.12 279.35,419.86 280.86,408.78 282.37,396.32 283.88,382.74 285.39,368.28 286.90,353.16 288.41,337.54 289.92,321.57 291.43,305.38 292.94,289.11 294.45,272.90 295.96,256.88 297.47,241.18 298.98,226.00 300.49,211.44 302.00,197.65
" />
<polygon fill="#e0f7fa" fill-opacity="0.5" stroke="none" points="
0.00,30.00 1.51,29.50 3.02,29.05 4.53,28.65 6.04,28.32 7.55,28.06 9.06,27.86 10.57,27.75 12.08,27.71 13.59,27.75 15.10,27.86 16.61,28.06 18.12,28.32 19.63,28.65 21.14,29.05 22.65,29.50 24.16,30.00 25.67,30.50 27.18,30.95 28.69,31.35 30.20,31.68 31.71,31.94 33.22,32.14 34.73,32.25 36.24,32.29 37.75,32.25 39.26,32.14 40.77,31.94 42.28,31.68 43.79,31.35 45.30,30.95 46.81,30.50 48.32,30.00 49.83,29.50 51.34,29.05 52.85,28.65 54.36,28.32 55.87,28.06 57.38,27.86 58.89,27.75 60.40,27.71 61.91,27.75 63.42,27.86 64.93,28.06 66.44,28.32 67.95,28.65 69.46,29.05 70.97,29.50 72.48,30.00 73.99,30.50 75.50,30.95 77.01,31.35 78.52,31.68 80.03,31.94 81.54,32.14 83.05,32.25 84.56,32.29 86.07,32.25 87.58,32.14 89.09,31.94 90.60,31.68 92.11,31.35 93.62,30.95 95.13,30.50 96.64,30.00 98.15,29.50 99.66,29.05 101.17,28.65 102.68,28.32 104.19,28.06 105.70,27.86 107.21,27.75 108.72,27.71 110.23,27.75 111.74,27.86 113.25,28.06 114.76,28.32 116.27,28.65 117.78,29.05 119.29,29.50 120.80,30.00 122.31,30.50 123.82,30.95 125.33,31.35 126.84,31.68 128.35,31.94 129.86,32.14 131.37,32.25 132.88,32.29 134.39,32.25 135.90,32.14 137.41,31.94 138.92,31.68 140.43,31.35 141.94,30.95 143.45,30.50 144.96,30.00 146.47,29.50 147.98,29.05 149.49,28.65 151.00,28.32 152.51,28.06 154.02,27.86 155.53,27.75 157.04,27.71 158.55,27.75 160.06,27.86 161.57,28.06 163.08,28.32 164.59,28.65 166.10,29.05 167.61,29.50 169.12,30.00 170.63,30.50 172.14,30.95 173.65,31.35 175.16,31.68 176.67,31.94 178.18,32.14 179.69,32.25 181.20,32.29 182.71,32.25 184.22,32.14 185.73,31.94 187.24,31.68 188.75,31.35 190.26,30.95 191.77,30.50 193.28,30.00 194.79,29.50 196.30,29.05 197.81,28.65 199.32,28.32 200.83,28.06 202.34,27.86 203.85,27.75 205.36,27.71 206.87,27.75 208.38,27.86 209.89,28.06 211.40,28.32 212.91,28.65 214.42,29.05 215.93,29.50 217.44,30.00 218.95,30.50 220.46,30.95 221.97,31.35 223.48,31.68 224.99,31.94 226.50,32.14 228.01,32.25 229.52,32.29 231.03,32.25 232.54,32.14 234.05,31.94 235.56,31.68 237.07,31.35 238.58,30.95 240.09,30.50 241.60,30.00 243.11,29.50 244.62,29.05 246.13,28.65 247.64,28.32 249.15,28.06 250.66,27.86 252.17,27.75 253.68,27.71 255.19,27.75 256.70,27.86 258.21,28.06 259.72,28.32 261.23,28.65 262.74,29.05 264.25,29.50 265.76,30.00 267.27,30.50 268.78,30.95 270.29,31.35 271.80,31.68 273.31,31.94 274.82,32.14 276.33,32.25 277.84,32.29 279.35,32.25 280.86,32.14 282.37,31.94 283.88,31.68 285.39,31.35 286.90,30.95 288.41,30.50 289.92,30.00 291.43,29.50 292.94,29.05 294.45,28.65 295.96,28.32 297.47,28.06 298.98,27.86 300.49,27.75 302.00,27.71 302.00,47.71 300.49,47.75 298.98,47.86 297.47,48.06 295.96,48.32 294.45,48.65 292.94,49.05 291.43,49.50 289.92,50.00 288.41,50.50 286.90,50.95 285.39,51.35 283.88,51.68 282.37,51.94 280.86,52.14 279.35,52.25 277.84,52.29 276.33,52.25 274.82,52.14 273.31,51.94 271.80,51.68 270.29,51.35 268.78,50.95 267.27,50.50 265.76,50.00 264.25,49.50 262.74,49.05 261.23,48.65 259.72,48.32 258.21,48.06 256.70,47.86 255.19,47.75 253.68,47.71 252.17,47.75 250.66,47.86 249.15,48.06 247.64,48.32 246.13,48.65 244.62,49.05 243.11,49.50 241.60,50.00 240.09,50.50 238.58,50.95 237.07,51.35 235.56,51.68 234.05,51.94 232.54,52.14 231.03,52.25 229.52,52.29 228.01,52.25 226.50,52.14 224.99,51.94 223.48,51.68 221.97,51.35 220.46,50.95 218.95,50.50 217.44,50.00 215.93,49.50 214.42,49.05 212.91,48.65 211.40,48.32 209.89,48.06 208.38,47.86 206.87,47.75 205.36,47.71 203.85,47.75 202.34,47.86 200.83,48.06 199.32,48.32 197.81,48.65 196.30,49.05 194.79,49.50 193.28,50.00 191.77,50.50 190.26,50.95 188.75,51.35 187.24,51.68 185.73,51.94 184.22,52.14 182.71,52.25 181.20,52.29 179.69,52.25 178.18,52.14 176.67,51.94 175.16,51.68 173.65,51.35 172.14,50.95 170.63,50.50 169.12,50.00 167.61,49.50 166.10,49.05 164.59,48.65 163.08,48.32 161.57,48.06 160.06,47.86 158.55,47.75 157.04,47.71 155.53,47.75 154.02,47.86 152.51,48.06 151.00,48.32 149.49,48.65 147.98,49.05 146.47,49.50 144.96,50.00 143.45,50.50 141.94,50.95 140.43,51.35 138.92,51.68 137.41,51.94 135.90,52.14 134.39,52.25 132.88,52.29 131.37,52.25 129.86,52.14 128.35,51.94 126.84,51.68 125.33,51.35 123.82,50.95 122.31,50.50 120.80,50.00 119.29,49.50 117.78,49.05 116.27,48.65 114.76,48.32 113.25,48.06 111.74,47.86 110.23,47.75 108.72,47.71 107.21,47.75 105.70,47.86 104.19,48.06 102.68,48.32 101.17,48.65 99.66,49.05 98.15,49.50 96.64,50.00 95.13,50.50 93.62,50.95 92.11,51.35 90.60,51.68 89.09,51.94 87.58,52.14 86.07,52.25 84.56,52.29 83.05,52.25 81.54,52.14 80.03,51.94 78.52,51.68 77.01,51.35 75.50,50.95 73.99,50.50 72.48,50.00 70.97,49.50 69.46,49.05 67.95,48.65 66.44,48.32 64.93,48.06 63.42,47.86 61.91,47.75 60.40,47.71 58.89,47.75 57.38,47.86 55.87,48.06 54.36,48.32 52.85,48.65 51.34,49.05 49.83,49.50 48.32,50.00 46.81,50.50 45.30,50.95 43.79,51.35 42.28,51.68 40.77,51.94 39.26,52.14 37.75,52.25 36.24,52.29 34.73,52.25 33.22,52.14 31.71,51.94 30.20,51.68 28.69,51.35 27.18,50.95 25.67,50.50 24.16,50.00 22.65,49.50 21.14,49.05 19.63,48.65 18.12,48.32 16.61,48.06 15.10,47.86 13.59,47.75 12.08,47.71 10.57,47.75 9.06,47.86 7.55,48.06 6.04,48.32 4.53,48.65 3.02,49.05 1.51,49.50 0.00,50.00
" />
</svg>

After

Width:  |  Height:  |  Size: 11 KiB