157 lines
5.0 KiB
Go
157 lines
5.0 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
)
|
|
|
|
// extractPackageSourceCode пытается извлечь исходный код из Fission Package.
|
|
// Порядок попыток:
|
|
// 1. spec.source.literal (base64) — Go функции (source package)
|
|
// 2. spec.deployment.literal (base64) — интерпретируемые языки
|
|
// 3. spec.source.url / spec.deployment.url — скачиваем архив по URL
|
|
//
|
|
// Декодирование: если данные — валидный UTF-8, возвращаем как есть.
|
|
// Если это zip — ищем известные файлы (main.py, handler.rb и т.д.).
|
|
func extractPackageSourceCode(ctx context.Context, s *Server, pkg *unstructured.Unstructured) string {
|
|
literalPaths := [][]string{
|
|
{"spec", "source", "literal"},
|
|
{"spec", "deployment", "literal"},
|
|
}
|
|
for _, p := range literalPaths {
|
|
literal, found, _ := unstructured.NestedString(pkg.Object, p...)
|
|
if !found || strings.TrimSpace(literal) == "" {
|
|
continue
|
|
}
|
|
if code, err := decodeLiteralToSource(literal); err == nil && strings.TrimSpace(code) != "" {
|
|
return code
|
|
}
|
|
}
|
|
|
|
// Fallback: скачиваем по URL
|
|
urlPaths := [][]string{
|
|
{"spec", "source", "url"},
|
|
{"spec", "deployment", "url"},
|
|
}
|
|
for _, p := range urlPaths {
|
|
urlValue, found, _ := unstructured.NestedString(pkg.Object, p...)
|
|
if !found || strings.TrimSpace(urlValue) == "" {
|
|
continue
|
|
}
|
|
archiveBytes, err := fetchPackageArchive(ctx, s, urlValue)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if code, err := decodeArchiveBytesToSource(archiveBytes); err == nil && strings.TrimSpace(code) != "" {
|
|
return code
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// fetchPackageArchive скачивает архив функции по URL из Fission storage.
|
|
func fetchPackageArchive(ctx context.Context, s *Server, archiveURL string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, archiveURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := s.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
|
|
// decodeLiteralToSource декодирует base64-строку из Package.spec.*.literal в исходный код.
|
|
func decodeLiteralToSource(literal string) (string, error) {
|
|
decoded, err := base64.StdEncoding.DecodeString(literal)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return decodeArchiveBytesToSource(decoded)
|
|
}
|
|
|
|
// decodeArchiveBytesToSource преобразует байты (utf-8 строка или zip архив) в исходный код.
|
|
func decodeArchiveBytesToSource(decoded []byte) (string, error) {
|
|
if len(decoded) == 0 {
|
|
return "", io.ErrUnexpectedEOF
|
|
}
|
|
// Если байты — валидный UTF-8, возвращаем напрямую (python, ruby, php)
|
|
if utf8.Valid(decoded) {
|
|
return string(decoded), nil
|
|
}
|
|
// PK signature: zip архив (nodejs, go source)
|
|
if len(decoded) >= 4 && bytes.Equal(decoded[:4], []byte{'P', 'K', 3, 4}) {
|
|
if src, zipErr := decodeZipSource(decoded); zipErr == nil {
|
|
return src, nil
|
|
}
|
|
}
|
|
return "", io.ErrUnexpectedEOF
|
|
}
|
|
|
|
// decodeZipSource извлекает исходный UTF-8 файл из zip архива.
|
|
// Предпочитает хорошо известные имена файлов (main.py, handler.go и т.д.).
|
|
func decodeZipSource(zipBytes []byte) (string, error) {
|
|
reader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Сначала ищем по приоритетным именам
|
|
preferred := []string{"main.py", "main.js", "main.go", "main.php", "handler.rb", "handler.pl", "handler.go", "handler.js", "handler.py"}
|
|
for _, name := range preferred {
|
|
for _, file := range reader.File {
|
|
if strings.EqualFold(file.Name, name) {
|
|
content, readErr := readZipFile(file)
|
|
if readErr == nil && utf8.Valid(content) {
|
|
return string(content), nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: первый UTF-8 файл по алфавиту (не директория)
|
|
files := make([]*zip.File, 0, len(reader.File))
|
|
for _, file := range reader.File {
|
|
if !file.FileInfo().IsDir() {
|
|
files = append(files, file)
|
|
}
|
|
}
|
|
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
|
|
|
|
for _, file := range files {
|
|
content, readErr := readZipFile(file)
|
|
if readErr == nil && utf8.Valid(content) {
|
|
return string(content), nil
|
|
}
|
|
}
|
|
|
|
return "", io.ErrUnexpectedEOF
|
|
}
|
|
|
|
// readZipFile открывает и читает содержимое файла из zip архива.
|
|
func readZipFile(file *zip.File) ([]byte, error) {
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
return io.ReadAll(rc)
|
|
}
|
|
|
|
// decodeZipSource извлекает исходный UTF-8 файл из zip архива.
|