97 lines
4.0 KiB
Go
97 lines
4.0 KiB
Go
// Изменено: 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",
|
||
})
|
||
}
|