Files
sless/internal/builder/context.go
T
“Naeel” 18f25e7a65 refactor: Builder SoC + JWKS stub + unit tests
Builder SoC (builder/context.go):
- Moved generateDockerfile, runtimeBaseImage, zipToTarGz from handler/upload.go
  to internal/builder/context.go.
  Reason: knowledge about runtime images and build context structure is a
  build concern, not an HTTP handler concern.
- Added PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error) —
  single public entry point. Handler calls one function, gets ready buffer.
- zipToTarGz now accepts *zip.Reader instead of []byte to avoid double parsing.
- upload.go reduced from ~200 LOC to ~60 LOC (build logic gone).

auth.go — JWKS insertion point:
- Added verifySignature() stub with detailed comment explaining what v2
  implementation needs (JWKS endpoint, kid lookup, RS256/ES256 verify).
- Shows exactly where to add the call in validateJWT.

Unit tests (9 total, all pass):
- controllers: TestBuildDeployment_EnvVarsSorted, TestBuildDeployment_EmptyEnv
- handler: TestHopByHopHeaders_* (3 tests)
- builder: TestPrepareContext_PythonWithRequirements, _NodeNoPackageJSON,
           _UnsupportedRuntime, _DockerfileIsFirst
2026-03-11 09:34:34 +04:00

151 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Создано: 2026-03-11
// context.go — подготовка build context для kaniko.
//
// PrepareContext преобразует zip с кодом пользователя в tar.gz с Dockerfile.
// Эта логика живёт в builder/, а НЕ в handler/ — потому что:
// - Знание о runtime образах (какой базовый образ, зависимости) — деталь сборки, не API.
// - handler/upload.go отвечает только за приём HTTP запроса и сохранение результата в S3.
//
// Разделение ответственностей:
// handler/upload.go — HTTP: принять zip, вызвать PrepareContext, загрузить в S3.
// builder/context.go — Build: превратить zip+runtime → tar.gz+Dockerfile для kaniko.
package builder
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"io"
"time"
)
// PrepareContext преобразует zip-архив с кодом пользователя в tar.gz build context для kaniko.
// Сканирует zip, определяет нужны ли зависимости, генерирует Dockerfile, упаковывает всё.
// Возвращает готовый буфер для загрузки в S3.
func PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error) {
zr, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, fmt.Errorf("parse zip: %w", err)
}
// Сканируем содержимое zip: наличие файлов зависимостей зависит от runtime.
// Это знание принадлежит builder-у, не HTTP-хендлеру.
hasRequirements := false // requirements.txt — python3.11: pip install
hasPackageJSON := false // package.json — nodejs20: npm install
for _, f := range zr.File {
switch f.Name {
case "requirements.txt":
hasRequirements = true
case "package.json":
hasPackageJSON = true
}
}
dockerfileContent, err := generateDockerfile(runtime, hasRequirements, hasPackageJSON)
if err != nil {
// Ошибка здесь означает неподдерживаемый runtime — 400 на уровне handler'а.
return nil, err
}
var buf bytes.Buffer
if err := zipToTarGz(zr, dockerfileContent, &buf); err != nil {
return nil, fmt.Errorf("pack context: %w", err)
}
return &buf, nil
}
// runtimeBaseImage возвращает Docker образ базового runtime для данного идентификатора.
// Образы на DockerHub под аккаунтом naeel, тег = версия образа.
// Возвращает ошибку если runtime не поддерживается — граница валидации.
func runtimeBaseImage(runtime string) (string, error) {
switch runtime {
case "python3.11":
return "naeel/sless-runtime-python3.11:v0.1.1", nil
case "nodejs20":
return "naeel/sless-runtime-nodejs20:v0.1.2", nil
default:
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20)", runtime)
}
}
// generateDockerfile генерирует Dockerfile для kaniko.
// Базовый образ содержит HTTP-обёртку (server.py / server.js).
// Пользовательский код копируется в /app/function/ поверх базового образа.
// Зависимости устанавливаются ПОСЛЕ COPY — чтобы кеш слоёв работал при повторных сборках.
func generateDockerfile(runtime string, hasRequirements bool, hasPackageJSON bool) ([]byte, error) {
baseImage, err := runtimeBaseImage(runtime)
if err != nil {
return nil, err
}
content := fmt.Sprintf("FROM %s\nCOPY . /app/function/\n", baseImage)
switch runtime {
case "python3.11":
if hasRequirements {
content += "RUN pip install --no-cache-dir -r /app/function/requirements.txt\n"
}
case "nodejs20":
if hasPackageJSON {
// cd нужен т.к. npm install читает package.json из текущей директории
content += "RUN cd /app/function && npm install --omit=dev\n"
}
}
return []byte(content), nil
}
// zipToTarGz принимает уже распарсенный *zip.Reader и упаковывает содержимое + Dockerfile в tar.gz.
// Принимает распарсенный zip чтобы не парсить zip дважды (первый раз уже в PrepareContext).
// kaniko не умеет читать zip-контекст, только tar(.gz) или OCI layout.
func zipToTarGz(zr *zip.Reader, dockerfileContent []byte, buf *bytes.Buffer) error {
gw := gzip.NewWriter(buf)
tw := tar.NewWriter(gw)
// Первым файлом пишем Dockerfile — kaniko ищет его в корне контекста
if err := tw.WriteHeader(&tar.Header{
Name: "Dockerfile",
Mode: 0644,
Size: int64(len(dockerfileContent)),
ModTime: time.Now(),
}); err != nil {
return fmt.Errorf("write Dockerfile header: %w", err)
}
if _, err := tw.Write(dockerfileContent); err != nil {
return fmt.Errorf("write Dockerfile: %w", err)
}
// Копируем файлы из zip в tar
for _, f := range zr.File {
if f.FileInfo().IsDir() {
continue // пустые директории не нужны
}
rc, err := f.Open()
if err != nil {
return fmt.Errorf("open zip entry %s: %w", f.Name, err)
}
data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return fmt.Errorf("read zip entry %s: %w", f.Name, err)
}
if err := tw.WriteHeader(&tar.Header{
Name: f.Name,
Mode: 0644,
Size: int64(len(data)),
ModTime: f.Modified,
}); err != nil {
return fmt.Errorf("write tar header %s: %w", f.Name, err)
}
if _, err := tw.Write(data); err != nil {
return fmt.Errorf("write tar entry %s: %w", f.Name, err)
}
}
if err := tw.Close(); err != nil {
return fmt.Errorf("close tar: %w", err)
}
return gw.Close()
}