diff --git a/doc/report-2026-05-19-pv-migration-bugs.md b/doc/report-2026-05-19-pv-migration-bugs.md new file mode 100644 index 0000000..48c0e11 --- /dev/null +++ b/doc/report-2026-05-19-pv-migration-bugs.md @@ -0,0 +1,118 @@ +# Отчёт: миграция S3→PV + баги helm upgrade — 2026-05-19 + +## Что делали + +1. Бенчмарк 3 хранилищ (local-path PV, vcd-disk-ext4 PV, S3) +2. Переехали storagesvc с S3 на PV (vcd-disk-ext4, 2Gi) +3. Обнаружили что S3 фактически не использовался (все Package URL — http://storagesvc..., не s3://) +4. Миграция была no-op: перенесено=0, пропущено=44 + +--- + +## Баги и ошибки + +### БАГ 1: helm upgrade откатил executor/router с v1.23.0 → v1.22.1 + +**Причина:** в `helm/fission-values.yaml` был прописан `imageTag: v1.22.1` вместо `v1.23.0`. +До helm upgrade executor/router жили вне helm (задеплоены через `kubectl set image`). + +**Симптом:** консоль не показывала функции. + +**Исправление:** изменён `imageTag: v1.23.0` в values, пересоздан executor/router. + +--- + +### БАГ 2: field manager конфликт — fission-console vs helm + +**Причина:** fission-console патчит `FISSION_RESOURCE_NAMESPACES` в executor и router через server-side apply. +Когда helm пытается обновить те же deployment-ы — конфликт field manager. + +**Симптом:** +``` +conflict with "fission-console" using apps/v1: +.spec.template.spec.containers[name="executor"].env[name="FISSION_RESOURCE_NAMESPACES"].value +``` + +**Обход:** перед `helm upgrade` удалять executor и router вручную: +```bash +kubectl delete deploy executor router -n fission +helm upgrade fission fission-charts/fission-all --version 1.22.1 \ + -n fission -f helm/fission-values.yaml --no-hooks --timeout 180s +``` + +**Системное решение:** нужно чтобы fission-console не трогал deployments executor/router напрямую (FISSION_RESOURCE_NAMESPACES должен читаться из ConfigMap или CRD, а не инжектироваться в deployment env). + +--- + +### БАГ 3: FETCHER_IMAGE = fission/fetcher:v1.22.0 — образ не существует + +**Причина:** helm chart `fission-all:1.22.1` имеет дефолтное значение: +```yaml +fetcher: + image: fission/fetcher + imageTag: v1.22.0 +``` +Этот образ на Docker Hub не существует (fission переехал на ghcr.io). + +До наших helm upgrade executor не управлялся helm — FETCHER_IMAGE либо не был задан (код падал на дефолт `ghcr.io/fission/fetcher`), либо был задан правильно вручную. + +После helm upgrade — chart прописал неправильный FETCHER_IMAGE. + +**Симптом:** +- Pool pods: `ImagePullBackOff` на контейнере `fetcher` +- `fission/fetcher:v1.22.0: not found` на Docker Hub +- Функции не запускаются (504 при invoke) + +**Правильное значение:** `naeel/fission-bundle:v1.23.0` +Fetcher — часть fission-bundle, отдельного образа нет. +Находится в: `fission-src/pkg/fetcher/` + +**Неправильные ключи в values (игнорируются чартом):** +```yaml +fetcherImage: naeel/fission-bundle # ← НЕПРАВИЛЬНО, такого ключа нет +fetcherImageTag: v1.23.0 # ← НЕПРАВИЛЬНО +``` + +**Правильные ключи:** +```yaml +fetcher: + image: naeel/fission-bundle # ← ПРАВИЛЬНО + imageTag: v1.23.0 # ← ПРАВИЛЬНО +``` + +Шаблон чарта (`charts/fission-all/templates/_helpers.tpl`): +``` +{{- define "fetcherImage" -}} +{{- $args := list (.Values.fetcher.repository | default .Values.repository) .Values.fetcher.image .Values.fetcher.imageTag -}} +``` + +--- + +### БАГ 4: router JWT пароль сменился при helm upgrade + +**Причина:** helm upgrade пересоздал secret `router` с новым паролем. + +| До | После | +|----|-------| +| `slxGj3G3FAD7l5ms2tJ9` | `58oKMuBIvmBBXHXqaLHw` | + +fission-console читает пароль из secret динамически (`valueFrom: secretKeyRef`) — поэтому сам работает. +CLI-инструменты с сохранённым токеном требуют перегенерации токена. + +--- + +## Текущее состояние (на момент отчёта) + +| Компонент | Образ | Статус | +|-----------|-------|--------| +| executor, router, buildermgr, storagesvc, timer, webhook, kubewatcher | `naeel/fission-bundle:v1.23.0` | ✅ Running | +| fission-console | `naeel/fission-console:v1.3.92` | ✅ Running | +| FETCHER_IMAGE (env в executor) | `fission/fetcher:v1.22.0` | ❌ НЕПРАВИЛЬНО | +| fission-storage-pvc | 2Gi, vcd-disk-ext4, Bound | ✅ OK | + +## Что нужно сделать + +1. Исправить `helm/fission-values.yaml` — заменить `fetcherImage`/`fetcherImageTag` на правильную секцию `fetcher:` +2. Сделать helm upgrade (с предварительным удалением executor/router) +3. Проверить что pool pods поднимаются без ImagePullBackOff +4. Проверить что функции nubes1 (`a1`, `c1`) запускаются diff --git a/helm/fission-values.yaml b/helm/fission-values.yaml new file mode 100644 index 0000000..7a7716a --- /dev/null +++ b/helm/fission-values.yaml @@ -0,0 +1,30 @@ +# Fission helm values — PV (local storage) backend +# Применять: helm upgrade fission fission-charts/fission-all --version 1.22.1 \ +# -n fission -f helm/fission-values.yaml +# +# Секретов нет — local storage не требует credentials. +# При деплое в новый кластер — только этот файл. + +authentication: + enabled: true + +defaultNamespace: fission-functions + +image: naeel/fission-bundle +imageTag: v1.23.0 +fetcher: + image: naeel/fetcher + imageTag: v1.23.0 + +persistence: + enabled: true + storageType: local + storageClass: vcd-disk-ext4 # сетевой block-device VMware Cloud Director (надёжнее local-path) + size: 2Gi + accessMode: ReadWriteOnce + +repository: "" +routerServiceType: ClusterIP +serviceType: ClusterIP + +analytics: false diff --git a/scripts/migrate_s3_to_pv.py b/scripts/migrate_s3_to_pv.py new file mode 100644 index 0000000..3c5e88d --- /dev/null +++ b/scripts/migrate_s3_to_pv.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +migrate_s3_to_pv.py — перелив архивов функций из S3 → storagesvc (PV-backend) + +Запуск: kubectl exec -n fission deploy/storagesvc -- python3 /tmp/migrate.py + ИЛИ: запустить как Job в кластере + +Что делает: + 1. Читает все Package CRD во всех namespace + 2. Для каждого пакета с URL s3://... скачивает архив из S3 + 3. Загружает в storagesvc HTTP API → получает новый URL + 4. Обновляет Package CRD: .spec.deployment.url = новый URL +""" + +import os +import sys +import json +import time +import urllib.request +import urllib.parse + +# S3 credentials (из env или хардкод для ручного запуска) +S3_ENDPOINT = os.environ.get("S3_ENDPOINT", "https://s3.msk-1.ngcloud.ru") +S3_BUCKET = os.environ.get("S3_BUCKET", "sless-functions") +S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY", "0GLQRD38H4I6RBDB0EWJ") +S3_SECRET_KEY = os.environ.get("S3_SECRET_KEY", "eTFibiHmBd96IApj9PYsboTR6OBoD7osxoarHykw") +S3_REGION = os.environ.get("S3_REGION", "msk-1") + +STORAGESVC_URL = os.environ.get("STORAGESVC_URL", "http://storagesvc.fission.svc.cluster.local") + +import subprocess + +def kubectl(args, input_data=None): + cmd = ["kubectl"] + args + r = subprocess.run(cmd, capture_output=True, text=True, input=input_data) + if r.returncode != 0: + raise RuntimeError(f"kubectl {' '.join(args)} failed: {r.stderr}") + return r.stdout + +def get_all_packages(): + out = kubectl(["get", "packages", "--all-namespaces", "-o", "json"]) + return json.loads(out)["items"] + +def is_s3_url(url): + return url and url.startswith("s3://") + +def s3_key_from_url(url): + # s3://sless-functions/fission/abc123 → fission/abc123 + path = url[len(f"s3://{S3_BUCKET}/"):] + return path + +def download_from_s3(s3_key): + import boto3 + s3 = boto3.client( + "s3", + endpoint_url=S3_ENDPOINT, + aws_access_key_id=S3_ACCESS_KEY, + aws_secret_access_key=S3_SECRET_KEY, + region_name=S3_REGION, + ) + obj = s3.get_object(Bucket=S3_BUCKET, Key=s3_key) + return obj["Body"].read() + +def upload_to_storagesvc(data): + import urllib.request, uuid + boundary = uuid.uuid4().hex + body = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="uploadfile"; filename="archive.zip"\r\n' + f"Content-Type: application/octet-stream\r\n\r\n" + ).encode() + data + f"\r\n--{boundary}--\r\n".encode() + + req = urllib.request.Request( + f"{STORAGESVC_URL}/v1/archive", + data=body, + method="POST", + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + result = json.loads(resp.read()) + return result["id"] # storagesvc возвращает {"id": "http://storagesvc.../v1/archive?id=..."} + +def update_package_url(ns, name, new_url): + patch = json.dumps({"spec": {"deployment": {"url": new_url, "type": "url"}}}) + kubectl(["patch", "package", name, "-n", ns, "--type=merge", f"--patch={patch}"]) + +def main(): + try: + import boto3 + except ImportError: + print("ERROR: boto3 не установлен. Запусти: pip install boto3") + sys.exit(1) + + packages = get_all_packages() + total = len(packages) + migrated = 0 + skipped = 0 + errors = 0 + + print(f"Всего пакетов: {total}") + print() + + for pkg in packages: + ns = pkg["metadata"]["namespace"] + name = pkg["metadata"]["name"] + url = pkg.get("spec", {}).get("deployment", {}).get("url", "") + + if not is_s3_url(url): + print(f" SKIP {ns}/{name} — url: {url or '(empty)'}") + skipped += 1 + continue + + s3_key = s3_key_from_url(url) + print(f" MIG {ns}/{name} s3://{S3_BUCKET}/{s3_key}", end=" ", flush=True) + + try: + data = download_from_s3(s3_key) + new_id = upload_to_storagesvc(data) + new_url = f"{STORAGESVC_URL}/v1/archive?id={new_id}" if not new_id.startswith("http") else new_id + update_package_url(ns, name, new_url) + print(f"→ {new_url}") + migrated += 1 + except Exception as e: + print(f" ERROR: {e}") + errors += 1 + + print() + print(f"Готово: перенесено={migrated} пропущено={skipped} ошибок={errors}") + +if __name__ == "__main__": + main()