Files
fission-console/client-console/internal/api/package.go
T

191 lines
6.1 KiB
Go

package api
import (
"archive/zip"
"bytes"
"context"
"encoding/base64"
"encoding/json"
"io"
"log"
"net/http"
"regexp"
"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 {
log.Printf("extractPackageSourceCode: fetchPackageArchive %s: %v", urlValue, err)
continue
}
if code, err := decodeArchiveBytesToSource(archiveBytes); err == nil && strings.TrimSpace(code) != "" {
return code
} else {
log.Printf("extractPackageSourceCode: decodeArchiveBytesToSource %s: err=%v, len=%d", urlValue, err, len(archiveBytes))
}
}
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
}
if src, ok := decodeNodeJSWrapperSource(decoded); ok {
return src, nil
}
// Если байты — валидный 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
}
var nodeJSWrapperPattern = regexp.MustCompile(`(?s)^const __mod = \{ exports: \{\} \};\s*\(new Function\('module', 'exports', (?:'require', )?(.+?)\)\)\(__mod, __mod\.exports(?:, require)?\);\s*const _fn = __mod\.exports;`)
func decodeNodeJSWrapperSource(decoded []byte) (string, bool) {
text := string(decoded)
matches := nodeJSWrapperPattern.FindStringSubmatch(text)
if len(matches) != 2 {
return "", false
}
var code string
if err := json.Unmarshal([]byte(matches[1]), &code); err != nil {
return "", false
}
if strings.TrimSpace(code) == "" {
return "", false
}
return code, true
}
// 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) {
if src, ok := decodeNodeJSWrapperSource(content); ok {
return src, nil
}
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) {
if src, ok := decodeNodeJSWrapperSource(content); ok {
return src, nil
}
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 архива.