package api import ( "archive/zip" "bytes" "context" "encoding/base64" "encoding/json" "io" "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 { 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 } 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', (.+?)\)\)\(__mod, __mod\.exports\);\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 архива.