fix: v0.7.2 — name limit 57 chars (HTTPTrigger -route suffix = 63), tests A-T PASS=54/FAIL=0

This commit is contained in:
Naeel
2026-04-19 18:41:27 +03:00
parent 0545fb3aa3
commit 85a5a9c2cc
2 changed files with 203 additions and 8 deletions
+63 -7
View File
@@ -15,6 +15,7 @@ import (
"net"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
@@ -268,6 +269,12 @@ func (s *server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}
// validFuncName — RFC 1123 subdomain label: lowercase alphanumeric + hyphens, no leading/trailing hyphen, max 63 chars.
var validFuncName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
// maxCodeSize — максимальный размер кода функции (1 MB).
const maxCodeSize = 1 << 20
func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
var req createFunctionRequest
ns := s.userNS(r)
@@ -293,6 +300,19 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
req.Entrypoint = strings.TrimSpace(req.Entrypoint)
req.Route = strings.TrimSpace(req.Route)
// Валидация имени: RFC 1123 label, максимум 57 символов.
// Ограничение 57 (не 63): самый длинный суффикс — "-route" (HTTPTrigger) = 6 символов.
// 63 - 6 = 57. Fission webhook требует все объекты <= 63 символов.
if req.Name != "" && (!validFuncName.MatchString(req.Name) || len(req.Name) > 57) {
writeJSONError(w, http.StatusBadRequest, "invalid function name: must match ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ and be <= 57 chars")
return
}
// Лимит размера кода
if len(req.Code) > maxCodeSize {
writeJSONError(w, http.StatusBadRequest, "code exceeds 1MB limit")
return
}
// Resolve language → environment (lazy creation).
// ensureEnvironment создаёт Environment CRD если не существует — Fission увидит и поднимет pool pod.
if req.Language != "" {
@@ -398,13 +418,7 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
"spec": pkgSpec,
}}
if _, err := s.dyn.Resource(packageGVR).Namespace(s.userNS(r)).Create(ctx, pkg, metav1.CreateOptions{}); err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create package: %v", err))
return
}
// Парсим TTL — если указан, записываем аннотацию на Function CRD.
// reaper периодически читает эту аннотацию и удаляет протухшие функции + чистит environment если он больше не используется.
// Парсим TTL ДО создания k8s ресурсов — чтобы невалидный TTL не оставлял мусор
fnAnnotations := map[string]any{}
fnAnnotations["fission-console/language"] = req.Language
if req.TTL != "" {
@@ -416,6 +430,19 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
fnAnnotations["fission-console/expires-at"] = expiresAt.UTC().Format(time.RFC3339)
}
if _, err := s.dyn.Resource(packageGVR).Namespace(s.userNS(r)).Create(ctx, pkg, metav1.CreateOptions{}); err != nil {
if apierrors.IsAlreadyExists(err) {
writeJSONError(w, http.StatusConflict, fmt.Sprintf("function %q already exists", req.Name))
return
}
if apierrors.IsInvalid(err) {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("invalid function spec: %v", err))
return
}
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create package: %v", err))
return
}
fn := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Function",
@@ -442,6 +469,10 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
if _, err := s.dyn.Resource(functionGVR).Namespace(s.userNS(r)).Create(ctx, fn, metav1.CreateOptions{}); err != nil {
_ = s.dyn.Resource(packageGVR).Namespace(s.userNS(r)).Delete(ctx, pkgName, metav1.DeleteOptions{})
if apierrors.IsAlreadyExists(err) {
writeJSONError(w, http.StatusConflict, fmt.Sprintf("function %q already exists", req.Name))
return
}
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create function: %v", err))
return
}
@@ -609,6 +640,7 @@ func (s *server) runExpiryReap() {
// reapExpiredFunctionsInNS удаляет протухшие функции в конкретном namespace.
// Для каждой удалённой функции вызывает cleanupEnvironmentIfUnused.
// Также удаляет orphan packages — пакеты у которых нет соответствующей функции.
func (s *server) reapExpiredFunctionsInNS(ctx context.Context, ns string, now time.Time) {
functions, err := s.dyn.Resource(functionGVR).Namespace(ns).List(ctx, metav1.ListOptions{})
if err != nil {
@@ -616,6 +648,12 @@ func (s *server) reapExpiredFunctionsInNS(ctx context.Context, ns string, now ti
return
}
// Строим множество имён существующих функций для поиска orphan packages
activeFunctions := make(map[string]struct{}, len(functions.Items))
for _, fn := range functions.Items {
activeFunctions[fn.GetName()] = struct{}{}
}
for _, fn := range functions.Items {
expiresAtStr, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/expires-at")
if expiresAtStr == "" {
@@ -657,6 +695,24 @@ func (s *server) reapExpiredFunctionsInNS(ctx context.Context, ns string, now ti
s.cleanupEnvironmentIfUnused(ctx, ns, envName)
}
}
// Сканируем orphan packages — пакеты без соответствующей функции
// (могут остаться если под упал в середине удаления)
packages, pkgListErr := s.dyn.Resource(packageGVR).Namespace(ns).List(ctx, metav1.ListOptions{})
if pkgListErr == nil {
for _, pkg := range packages.Items {
pkgName := pkg.GetName()
// Конвенция именования: {fn-name}-pkg
if !strings.HasSuffix(pkgName, "-pkg") {
continue
}
fnName := strings.TrimSuffix(pkgName, "-pkg")
if _, exists := activeFunctions[fnName]; !exists {
log.Printf("expiryReaper: deleting orphan package %s/%s (no matching function)", ns, pkgName)
_ = s.dyn.Resource(packageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{})
}
}
}
}
func (s *server) buildGoSourceZip(code string) ([]byte, error) {