feat(job): merge sless_function into sless_job — self-contained build+run
- FunctionJobSpec: убран FunctionRef, добавлены Runtime/Entrypoint/Env/S3Key/MemoryMB/TimeoutSec
- FunctionJobStatus: новый ImageRef, новая фаза Building
- FunctionJobReconciler: Building фаза (kaniko), убрана зависимость от Function CRD
Builder+OperatorNamespace как поля struct; аннотация sless.kube5s.ru/build-job guard
- main.go: Builder+OperatorNamespace переданы в FunctionJobReconciler
- jobs.go handler: jobRequest/jobResponse без FunctionRef; новый UploadJobCode handler
- router.go: /jobs/{name}/upload маршрут
- client.go: JobRequest/JobResponse обновлены; UploadJobCode; uploadCodeToURL общий хелпер
- job_resource.go: полная переработка — источник/среда встроены в JobModel, ModifyPlan,
Create с upload, wait_timeout_sec=900 по умолчанию (kaniko + выполнение)
- examples/POSTGRES/functions.tf: раскомментирован, sless_function удалён,
sless_job самодостаточен (inline source_dir/runtime/entrypoint/env_vars)
This commit is contained in:
+110
-19
@@ -1,4 +1,4 @@
|
||||
// Изменено: 2026-03-08
|
||||
// Изменено: 2026-03-20 (merge: FunctionJob теперь самодостаточен — убран FunctionRef, добавлены Runtime/Entrypoint/Env)
|
||||
// jobs.go — CRUD handlers для FunctionJob CRD.
|
||||
// Создаёт/читает/удаляет k8s FunctionJob ресурсы.
|
||||
// Namespace берётся из URL: /v1/namespaces/{namespace}/jobs/{name}
|
||||
@@ -7,20 +7,29 @@ package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"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"
|
||||
)
|
||||
|
||||
// jobRequest — тело POST /v1/namespaces/{ns}/jobs
|
||||
type jobRequest struct {
|
||||
Name string `json:"name"`
|
||||
FunctionRef string `json:"function"`
|
||||
EventJSON string `json:"event_json,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Runtime string `json:"runtime"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
MemoryMB int32 `json:"memory_mb,omitempty"`
|
||||
TimeoutSec int32 `json:"timeout_sec,omitempty"`
|
||||
Env map[string]string `json:"env_vars,omitempty"`
|
||||
S3Bucket string `json:"s3_bucket,omitempty"`
|
||||
S3Key string `json:"s3_key,omitempty"`
|
||||
EventJSON string `json:"event_json,omitempty"`
|
||||
// RunID — идентификатор запуска. 0 = создать без запуска, >0 = запустить.
|
||||
RunID int64 `json:"run_id"`
|
||||
}
|
||||
@@ -29,10 +38,12 @@ type jobRequest struct {
|
||||
type jobResponse struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
FunctionRef string `json:"function"`
|
||||
Runtime string `json:"runtime"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
EventJSON string `json:"event_json"`
|
||||
RunID int64 `json:"run_id"`
|
||||
Phase string `json:"phase"`
|
||||
ImageRef string `json:"image_ref,omitempty"`
|
||||
JobName string `json:"job_name,omitempty"`
|
||||
StartTime string `json:"start_time,omitempty"`
|
||||
CompletionTime string `json:"completion_time,omitempty"`
|
||||
@@ -42,14 +53,16 @@ type jobResponse struct {
|
||||
// jobToResponse конвертирует FunctionJob CR → jobResponse.
|
||||
func jobToResponse(j *slessv1alpha1.FunctionJob) jobResponse {
|
||||
r := jobResponse{
|
||||
Name: j.Name,
|
||||
Namespace: j.Namespace,
|
||||
FunctionRef: j.Spec.FunctionRef,
|
||||
EventJSON: j.Spec.EventJSON,
|
||||
RunID: j.Spec.RunID,
|
||||
Phase: string(j.Status.Phase),
|
||||
JobName: j.Status.JobName,
|
||||
Message: j.Status.Message,
|
||||
Name: j.Name,
|
||||
Namespace: j.Namespace,
|
||||
Runtime: j.Spec.Runtime,
|
||||
Entrypoint: j.Spec.Entrypoint,
|
||||
EventJSON: j.Spec.EventJSON,
|
||||
RunID: j.Spec.RunID,
|
||||
Phase: string(j.Status.Phase),
|
||||
ImageRef: j.Status.ImageRef,
|
||||
JobName: j.Status.JobName,
|
||||
Message: j.Status.Message,
|
||||
}
|
||||
if j.Status.StartTime != nil {
|
||||
r.StartTime = j.Status.StartTime.UTC().Format("2006-01-02T15:04:05Z")
|
||||
@@ -61,7 +74,7 @@ func jobToResponse(j *slessv1alpha1.FunctionJob) jobResponse {
|
||||
}
|
||||
|
||||
// CreateJob — POST /v1/namespaces/{namespace}/jobs
|
||||
// Создаёт FunctionJob CR. Оператор запустит k8s Job асинхронно.
|
||||
// Создаёт FunctionJob CR. Оператор запустит kaniko сборку и затем k8s Job асинхронно.
|
||||
func (h *Handler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
ns := namespace(r)
|
||||
|
||||
@@ -74,8 +87,12 @@ func (h *Handler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("name is required"))
|
||||
return
|
||||
}
|
||||
if req.FunctionRef == "" {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("function is required"))
|
||||
if req.Runtime == "" {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("runtime is required"))
|
||||
return
|
||||
}
|
||||
if req.Entrypoint == "" {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("entrypoint is required"))
|
||||
return
|
||||
}
|
||||
if req.EventJSON == "" {
|
||||
@@ -88,9 +105,15 @@ func (h *Handler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
Namespace: ns,
|
||||
},
|
||||
Spec: slessv1alpha1.FunctionJobSpec{
|
||||
FunctionRef: req.FunctionRef,
|
||||
EventJSON: req.EventJSON,
|
||||
RunID: req.RunID,
|
||||
Runtime: req.Runtime,
|
||||
Entrypoint: req.Entrypoint,
|
||||
MemoryMB: req.MemoryMB,
|
||||
TimeoutSec: req.TimeoutSec,
|
||||
Env: req.Env,
|
||||
S3Bucket: req.S3Bucket,
|
||||
S3Key: req.S3Key,
|
||||
EventJSON: req.EventJSON,
|
||||
RunID: req.RunID,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -152,3 +175,71 @@ func (h *Handler) DeleteJob(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UploadJobCode — POST /v1/namespaces/{namespace}/jobs/{name}/upload
|
||||
// Принимает multipart/form-data с полем "code" (zip архив с кодом функции).
|
||||
// Аналогично UploadCode для Function, но работает с FunctionJob CRD.
|
||||
// После загрузки обновляет Spec.S3Key — контроллер начнёт kaniko сборку.
|
||||
func (h *Handler) UploadJobCode(w http.ResponseWriter, r *http.Request) {
|
||||
ns := namespace(r)
|
||||
name := pathVar(r, "name")
|
||||
|
||||
// Читаем FunctionJob для получения runtime
|
||||
fj := &slessv1alpha1.FunctionJob{}
|
||||
if err := h.K8s.Get(r.Context(), client.ObjectKey{Name: name, Namespace: ns}, fj); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
writeJSON(w, http.StatusNotFound, errResp("job 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
|
||||
buf, err := builder.PrepareContext(zipData, fj.Spec.Runtime)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("prepare build context: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Версия на основе timestamp — каждый upload → новый уникальный ключ в S3
|
||||
version := time.Now().Format("20060102150405")
|
||||
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
|
||||
}
|
||||
|
||||
// Обновляем FunctionJob CRD: новый s3Key → контроллер начнёт сборку
|
||||
patch := client.MergeFrom(fj.DeepCopy())
|
||||
fj.Spec.S3Key = s3Key
|
||||
fj.Spec.S3Bucket = h.S3.Bucket()
|
||||
if err := h.K8s.Patch(r.Context(), fj, patch); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("update job: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"s3_key": s3Key,
|
||||
"phase": string(slessv1alpha1.FunctionJobPhasePending),
|
||||
"message": "build queued",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ func NewRouter(h *handler.Handler, log *slog.Logger) http.Handler {
|
||||
v1.HandleFunc("/namespaces/{namespace}/jobs", h.CreateJob).Methods(http.MethodPost)
|
||||
v1.HandleFunc("/namespaces/{namespace}/jobs/{name}", h.GetJob).Methods(http.MethodGet)
|
||||
v1.HandleFunc("/namespaces/{namespace}/jobs/{name}", h.DeleteJob).Methods(http.MethodDelete)
|
||||
v1.HandleFunc("/namespaces/{namespace}/jobs/{name}/upload", h.UploadJobCode).Methods(http.MethodPost)
|
||||
|
||||
// Цепочка middleware: logging → (auth только для /v1/) → router
|
||||
// /fn/ — без auth, /v1/ — с auth.
|
||||
|
||||
Reference in New Issue
Block a user