diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml index 491177f..f8a3a0f 100644 --- a/console/deploy/console.yaml +++ b/console/deploy/console.yaml @@ -58,7 +58,7 @@ spec: serviceAccountName: fission-console containers: - name: console - image: naeel/fission-console:v1.3.88 + image: naeel/fission-console:v1.3.89 imagePullPolicy: Always ports: - containerPort: 8090 diff --git a/console/internal/api/function_code.go b/console/internal/api/function_code.go index 91fa34a..ba700ad 100644 --- a/console/internal/api/function_code.go +++ b/console/internal/api/function_code.go @@ -35,17 +35,26 @@ import ( // buildDeployArchive упаковывает исходный код в байты для deployment Package. // Для nodejs — ESM-обёртка (package.json + main.js). // Для php/ruby — zip с одним файлом скрипта. -// Для остальных (python) — raw bytes кода. -func buildDeployArchive(lang, code string) ([]byte, error) { +// Для остальных (python) — raw bytes кода (или zip если есть deps). +// deps — содержимое файла зависимостей (requirements.txt, Gemfile, composer.json). +// Если deps пустой — поведение как раньше. +func buildDeployArchive(lang, code, deps string) ([]byte, error) { switch lang { case "nodejs": + // TODO: поддержка package.json с deps для nodejs — пока игнорируем deps return runtime.BuildJSDeployZip(code) case "php": + if deps != "" { + return runtime.BuildScriptZipWithDeps(code, "main.php", deps, "composer.json") + } return runtime.BuildScriptZip(code, "main.php") case "ruby": + if deps != "" { + return runtime.BuildScriptZipWithDeps(code, "handler.rb", deps, "Gemfile") + } return runtime.BuildScriptZip(code, "handler.rb") - default: - return []byte(code), nil + default: // python + return runtime.BuildPythonZip(code, deps) } } @@ -173,7 +182,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) { "buildcommand": "build", } } else { - deployBytes, archiveErr := buildDeployArchive(req.Language, req.Code) + deployBytes, archiveErr := buildDeployArchive(req.Language, req.Code, req.Deps) if archiveErr != nil { writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr)) return @@ -329,7 +338,7 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request // Определяем язык из аннотации — нужен для правильной упаковки 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 { writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", lang, archiveErr)) return diff --git a/console/internal/model/types.go b/console/internal/model/types.go index 3f3ffd9..74e115f 100644 --- a/console/internal/model/types.go +++ b/console/internal/model/types.go @@ -5,11 +5,14 @@ package model // CreateFunctionRequest — тело POST /console/api/functions. // TTL пустой → функция живёт вечно; "1d", "24h" — протухнет через указанное время. +// Deps — содержимое файла зависимостей: requirements.txt (python), package.json deps (nodejs), +// Gemfile (ruby), composer.json (php). Если задан — код упаковывается в zip вместе с deps-файлом. type CreateFunctionRequest struct { Name string `json:"name"` Language string `json:"language"` Environment string `json:"environment"` Code string `json:"code"` + Deps string `json:"deps"` // содержимое файла зависимостей (опционально) Entrypoint string `json:"entrypoint"` Route string `json:"route"` Methods []string `json:"methods"` @@ -51,6 +54,7 @@ type CreateKWTriggerRequest struct { // UpdateCodeRequest — тело PUT /console/api/functions/:name/code. type UpdateCodeRequest struct { Code string `json:"code"` + Deps string `json:"deps"` // содержимое файла зависимостей (опционально) Timeout int64 `json:"timeout"` } diff --git a/console/internal/runtime/helpers.go b/console/internal/runtime/helpers.go index 91b2889..9ae9205 100644 --- a/console/internal/runtime/helpers.go +++ b/console/internal/runtime/helpers.go @@ -25,3 +25,28 @@ func buildZip(fileName string, content []byte) ([]byte, error) { } 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 +} diff --git a/console/internal/runtime/script.go b/console/internal/runtime/script.go index da631b7..f379359 100644 --- a/console/internal/runtime/script.go +++ b/console/internal/runtime/script.go @@ -11,3 +11,20 @@ package runtime func BuildScriptZip(code, fileName string) ([]byte, error) { 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)) +} diff --git a/console/ui/index.html b/console/ui/index.html index 905eed1..421d8b2 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -103,7 +103,7 @@
NUBES
FISSION CONSOLE
-
v1.3.88
+
v1.3.89
@@ -306,6 +306,12 @@
+
+ + +
+
+ + +
diff --git a/console/ui/js/fn-code.js b/console/ui/js/fn-code.js index 2ac02f1..19eeebf 100644 --- a/console/ui/js/fn-code.js +++ b/console/ui/js/fn-code.js @@ -7,6 +7,23 @@ function onLangChangeCode() { document.getElementById('cc-entry').value = t.entrypoint; 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() { @@ -16,6 +33,7 @@ function openCreateCode() { document.getElementById('cc-route').value = ''; document.getElementById('cc-methods').value = 'GET'; document.getElementById('cc-timeout').value = '60'; + document.getElementById('cc-deps').value = ''; document.getElementById('cc-schedule-enabled').checked = false; document.getElementById('cc-cron').value = ''; toggleScheduleFields('cc'); @@ -47,7 +65,8 @@ async function submitCreateCode() { route: document.getElementById('cc-route').value.trim(), methods: parseMethods(document.getElementById('cc-methods').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 { diff --git a/console/ui/js/functions.js b/console/ui/js/functions.js index df9bca3..fb0e0a2 100644 --- a/console/ui/js/functions.js +++ b/console/ui/js/functions.js @@ -188,6 +188,13 @@ async function openEdit(name) { else if (envName.includes('ruby')) lang = 'ruby'; else if (envName.includes('php')) lang = 'php'; 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 = ''; } var aiRes = document.getElementById('e-ai-result'); if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; } var warnEl = document.getElementById('e-tf-warn'); @@ -242,6 +249,7 @@ async function submitEdit() { } else { await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', { code: document.getElementById('e-code').value, + deps: document.getElementById('e-deps').value.trim(), timeout: parseTimeout(document.getElementById('e-timeout').value) }); }