feat: NodeJS pg-info function; funcs endpoint: filter + created_at/last_built_at; operator v0.1.32

This commit is contained in:
Naeel
2026-03-18 11:03:58 +03:00
parent 6010649e7b
commit 9bc91841c8
7 changed files with 352 additions and 70 deletions
+41 -20
View File
@@ -1,4 +1,4 @@
// Изменено: 2026-03-11
// Изменено: 2026-03-18 (добавлены created_at, last_built_at в functionResponse и fnToResponse)
// functions.go — CRUD handlers для Function CRD.
// Принимает JSON, создаёт/обновляет/удаляет k8s ресурсы Function.
// Namespace берётся из URL: /v1/namespaces/{namespace}/functions/{name}
@@ -30,23 +30,29 @@ type functionRequest struct {
// functionResponse — ответ при чтении функции.
type functionResponse struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Runtime string `json:"runtime"`
Entrypoint string `json:"entrypoint"`
MemoryMB int32 `json:"memory_mb"`
TimeoutSec int32 `json:"timeout_sec"`
Env map[string]string `json:"env_vars"`
S3Bucket string `json:"s3_bucket"`
S3Key string `json:"s3_key"`
Phase slessv1alpha1.FunctionPhase `json:"phase"`
ImageRef string `json:"image_ref"`
Message string `json:"message,omitempty"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Runtime string `json:"runtime"`
Entrypoint string `json:"entrypoint"`
MemoryMB int32 `json:"memory_mb"`
TimeoutSec int32 `json:"timeout_sec"`
Env map[string]string `json:"env_vars"`
S3Bucket string `json:"s3_bucket"`
S3Key string `json:"s3_key"`
Phase slessv1alpha1.FunctionPhase `json:"phase"`
ImageRef string `json:"image_ref"`
Message string `json:"message,omitempty"`
// CreatedAt — время создания CRD объекта (metadata.creationTimestamp).
// Пустое значение = "0001-01-01T00:00:00Z" сериализуется в "", опускаем через omitempty.
CreatedAt string `json:"created_at,omitempty"`
// LastBuiltAt — время последней успешной сборки образа (status.lastBuiltAt).
// nil если сборки ещё не было.
LastBuiltAt string `json:"last_built_at,omitempty"`
}
// fnToResponse конвертирует CRD в ответ API.
func fnToResponse(fn *slessv1alpha1.Function) functionResponse {
return functionResponse{
resp := functionResponse{
Name: fn.Name,
Namespace: fn.Namespace,
Runtime: fn.Spec.Runtime,
@@ -60,6 +66,14 @@ func fnToResponse(fn *slessv1alpha1.Function) functionResponse {
ImageRef: fn.Status.ImageRef,
Message: fn.Status.Message,
}
// creationTimestamp — всегда заполнен k8s, но zero value опускаем.
if !fn.CreationTimestamp.IsZero() {
resp.CreatedAt = fn.CreationTimestamp.UTC().Format("2006-01-02 15:04:05 UTC")
}
if fn.Status.LastBuiltAt != nil && !fn.Status.LastBuiltAt.IsZero() {
resp.LastBuiltAt = fn.Status.LastBuiltAt.UTC().Format("2006-01-02 15:04:05 UTC")
}
return resp
}
// ListFunctions — GET /v1/namespaces/{namespace}/functions
@@ -115,13 +129,20 @@ func (h *Handler) CreateFunction(w http.ResponseWriter, r *http.Request) {
}
if err := h.K8s.Create(r.Context(), fn); err != nil {
if errors.IsAlreadyExists(err) {
// Если существующая функция в статусе Failed (build провалился, terraform не
// добавил её в state) — удаляем её и пересоздаём, иначе клиент получит 409 навсегда.
// IsAlreadyExists может прийти из кеша controller-runtime (split-brain):
// объект удалён из etcd, но кеш informer ещё южив. Делаем uncached Get:
// если реально NotFound — кеш устарел, пересоздаём.
// если существует и фаза Failed — тоже пересоздаём (build провалился, терраформ не добавил в state).
// если существует и фаза Ready/Building — возвращаем 409 (функция реально есть).
existing := &slessv1alpha1.Function{}
if getErr := h.K8s.Get(r.Context(), client.ObjectKey{Name: req.Name, Namespace: ns}, existing); getErr == nil &&
existing.Status.Phase == slessv1alpha1.FunctionPhaseFailed {
_ = h.K8s.Delete(r.Context(), existing)
// Создаём заново с теми же параметрами
getErr := h.K8s.Get(r.Context(), client.ObjectKey{Name: req.Name, Namespace: ns}, existing)
shouldRecreate := errors.IsNotFound(getErr) ||
(getErr == nil && existing.Status.Phase == slessv1alpha1.FunctionPhaseFailed)
if shouldRecreate {
if getErr == nil {
_ = h.K8s.Delete(r.Context(), existing)
}
// Сбрасываем ResourceVersion — при split-brain etcd считает объект новым
fn.ResourceVersion = ""
if createErr := h.K8s.Create(r.Context(), fn); createErr != nil {
writeJSON(w, http.StatusInternalServerError, errResp(createErr.Error()))