feat: web-console — HTML UI + source viewer + trigger toggle
- operator: GET /v1/namespaces/{ns}/functions/{name}/source
reads build context tar.gz from S3, strips Dockerfile, returns JSON files
- funcs-service v0.2.0:
- Accept: text/html → dark-themed HTML console with accordion cards
- GET /funcs/{ns}/source/{fn} → proxy to operator (service token auth)
- PATCH /funcs/{ns}/triggers/{name} → proxy enable/disable (only enabled field)
- curl (no text/html Accept) → plain text as before (backward compat)
- highlight.js syntax highlighting per file extension
- operator v0.1.34, funcs-service v0.2.0
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
// Создано: 2026-04-25
|
||||
// source.go — handler для GET /v1/namespaces/{namespace}/functions/{name}/source
|
||||
// Возвращает список файлов исходного кода функции из tar.gz контекста сборки (S3).
|
||||
// Dockerfile исключается — он сгенерирован builder-ом, не является кодом пользователя.
|
||||
// Если код ещё не загружен (S3Key пустой) — возвращает пустой список.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
|
||||
)
|
||||
|
||||
// sourceFileEntry — один файл из исходного кода функции.
|
||||
type sourceFileEntry struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"` // UTF-8 текст; бинарные файлы — base64 encoded
|
||||
Binary bool `json:"binary"` // true если файл бинарный (base64 в content)
|
||||
}
|
||||
|
||||
// GetSource — GET /v1/namespaces/{namespace}/functions/{name}/source
|
||||
// Скачивает tar.gz контекст сборки из S3 и возвращает пользовательские файлы (без Dockerfile).
|
||||
func (h *Handler) GetSource(w http.ResponseWriter, r *http.Request) {
|
||||
ns := namespace(r)
|
||||
name := pathVar(r, "name")
|
||||
|
||||
fn := &slessv1alpha1.Function{}
|
||||
if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, fn); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
writeJSON(w, http.StatusNotFound, errResp("function not found"))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Код ещё не был загружен — возвращаем пустой список без ошибки
|
||||
if fn.Spec.S3Key == "" {
|
||||
writeJSON(w, http.StatusOK, []sourceFileEntry{})
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := h.S3.Download(r.Context(), fn.Spec.S3Key)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("download from S3: "+err.Error()))
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
files, err := extractSourceFilesFromTarGz(rc)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("extract source: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, files)
|
||||
}
|
||||
|
||||
// extractSourceFilesFromTarGz читает tar.gz и возвращает пользовательские файлы.
|
||||
// Dockerfile пропускается — он генерируется builder-ом автоматически.
|
||||
// Бинарные файлы (содержат null bytes) возвращаются в base64 с binary=true.
|
||||
func extractSourceFilesFromTarGz(r io.Reader) ([]sourceFileEntry, error) {
|
||||
gr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gzip: %w", err)
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
tr := tar.NewReader(gr)
|
||||
var result []sourceFileEntry
|
||||
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tar next: %w", err)
|
||||
}
|
||||
|
||||
// Dockerfile сгенерирован builder-ом — не показываем пользователю
|
||||
if hdr.Name == "Dockerfile" || hdr.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", hdr.Name, err)
|
||||
}
|
||||
|
||||
// Бинарные файлы — base64, текстовые — как есть
|
||||
content := string(data)
|
||||
isBinary := strings.ContainsRune(content, '\x00')
|
||||
if isBinary {
|
||||
content = base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
result = append(result, sourceFileEntry{
|
||||
Name: hdr.Name,
|
||||
Content: content,
|
||||
Binary: isBinary,
|
||||
})
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = []sourceFileEntry{}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user