Files
sless/internal/api/handler/upload.go
T

97 lines
4.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
// upload.go — обработчик загрузки кода функции.
// Принимает zip от пользователя, вызывает builder.PrepareContext (Dockerfile + tar.gz),
// кладёт результат в S3 и обновляет Function CRD чтобы контроллер запустил kaniko.
//
// Разделение ответственностей:
// upload.go — HTTP: принять zip, сохранить в S3, обновить CRD.
// builder/context.go — Build: zip+runtime → tar.gz+Dockerfile для kaniko.
package handler
import (
"crypto/sha256"
"fmt"
"io"
"net/http"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
)
// UploadCode — POST /v1/namespaces/{namespace}/functions/{name}/upload
// Принимает multipart/form-data с полем "code" (zip архив с кодом функции).
// Генерирует Dockerfile, пакует tar.gz, загружает в S3, обновляет Function CRD.
func (h *Handler) UploadCode(w http.ResponseWriter, r *http.Request) {
ns := namespace(r)
name := pathVar(r, "name")
// Проверяем что Function CRD существует и получаем runtime
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
}
// Лимит 32MB на загрузку кода функции
if err := r.ParseMultipartForm(32 << 20); err != nil {
writeJSON(w, http.StatusBadRequest, errResp("invalid multipart form: "+err.Error()))
return
}
file, _, err := r.FormFile("code")
if err != nil {
writeJSON(w, http.StatusBadRequest, errResp(`field "code" is required (zip file)`))
return
}
defer file.Close()
zipData, err := io.ReadAll(file)
if err != nil {
writeJSON(w, http.StatusInternalServerError, errResp("read upload: "+err.Error()))
return
}
// Готовим build context: Dockerfile + tar.gz для kaniko.
// Знание о runtime образах и структуре контекста — в builder.PrepareContext, не здесь.
buf, err := builder.PrepareContext(zipData, fn.Spec.Runtime)
if err != nil {
writeJSON(w, http.StatusBadRequest, errResp("prepare build context: "+err.Error()))
return
}
// Версия = sha256(загруженный zip). Одинаковый код → одинаковый s3Key → cache hit.
// Изменился код → новый хеш → новый build.
zipHash := sha256.Sum256(zipData)
version := fmt.Sprintf("%x", zipHash[:])[:16]
s3Key, err := h.S3.UploadContext(r.Context(), ns, name, version, buf, int64(buf.Len()))
if err != nil {
writeJSON(w, http.StatusInternalServerError, errResp("upload to S3: "+err.Error()))
return
}
// Обновляем Function CRD: новый s3Key и bucket → контроллер запустит kaniko.
// Используем Patch вместо Update чтобы избежать конфликта resourceVersion:
// между Get() выше и Update() контроллер может изменить статус объекта.
// MergePatch обновляет только указанные поля, не требует точного resourceVersion.
patch := client.MergeFrom(fn.DeepCopy())
fn.Spec.S3Key = s3Key
fn.Spec.S3Bucket = h.S3.Bucket()
if err := h.K8s.Patch(r.Context(), fn, patch); err != nil {
writeJSON(w, http.StatusInternalServerError, errResp("update function: "+err.Error()))
return
}
writeJSON(w, http.StatusOK, map[string]string{
"s3_key": s3Key,
"phase": string(slessv1alpha1.FunctionPhasePending),
"message": "build queued",
})
}