157 lines
4.9 KiB
Go
157 lines
4.9 KiB
Go
// Создано: 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)
|
||
}
|
||
|
||
// GetServiceSource — GET /v1/namespaces/{namespace}/services/{name}/source
|
||
// Аналог GetSource для sless_service: скачивает tar.gz из S3 и возвращает пользовательские файлы.
|
||
func (h *Handler) GetServiceSource(w http.ResponseWriter, r *http.Request) {
|
||
ns := namespace(r)
|
||
name := pathVar(r, "name")
|
||
|
||
svc := &slessv1alpha1.Service{}
|
||
if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, svc); err != nil {
|
||
if errors.IsNotFound(err) {
|
||
writeJSON(w, http.StatusNotFound, errResp("service not found"))
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||
return
|
||
}
|
||
|
||
if svc.Spec.S3Key == "" {
|
||
writeJSON(w, http.StatusOK, []sourceFileEntry{})
|
||
return
|
||
}
|
||
|
||
rc, err := h.S3.Download(r.Context(), svc.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
|
||
}
|