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
This commit is contained in:
@@ -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.89
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -329,7 +338,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
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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.89</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')">✨ Сгенерировать код LLM</button>
|
<button class="btn ghost" id="cc-gen-btn" onclick="showGenPrompt('cc')">✨ Сгенерировать код LLM</button>
|
||||||
<button class="btn ghost" id="cc-exp-btn" onclick="aiExplain('cc-code','cc-lang','cc-ai-result')">📖 LLM: Что делает?</button>
|
<button class="btn ghost" id="cc-exp-btn" onclick="aiExplain('cc-code','cc-lang','cc-ai-result')">📖 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="4" 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:vertical;"></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')">📖
|
<button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">📖
|
||||||
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="4" 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:vertical;"></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.89</span>
|
||||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = ''; }
|
||||||
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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user