Organize LLM assets and function lifecycle fixes

This commit is contained in:
Naeel
2026-04-28 13:04:08 +03:00
parent 82eba079f6
commit a534fddd2c
22 changed files with 1409 additions and 72 deletions
+71 -37
View File
@@ -354,6 +354,9 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
fnAnnotations := map[string]any{
"fission-console/language": req.Language,
}
now := time.Now().UTC()
fnAnnotations[functionCreatedAtAnnotation] = now.Format(time.RFC3339)
fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339)
if req.TTL != "" {
expiresAt, ttlErr := parseTTL(req.TTL)
if ttlErr != nil {
@@ -381,7 +384,7 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
"kind": "Function",
"metadata": map[string]any{"name": req.Name, "namespace": ns, "annotations": fnAnnotations},
"spec": map[string]any{
"environment": map[string]any{"name": req.Environment, "namespace": ns},
"environment": map[string]any{"name": req.Environment, "namespace": ns},
"functionTimeout": req.Timeout,
"InvokeStrategy": map[string]any{
"ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"},
@@ -490,6 +493,8 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
"package": packageName,
"entrypoint": entrypoint,
"timeout": functionTimeout,
"created_at": functionTimestampResponse(fn)["created_at"],
"updated_at": functionTimestampResponse(fn)["updated_at"],
"code": code,
"route": route,
"methods": methods,
@@ -498,11 +503,9 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
}
// handleUpdateFunctionCode обновляет код уже существующей функции.
// Обновляет Package.spec.deployment.literal и синхронизирует resourceVersion в Function.
//
// Почему нужно обновлять resourceVersion в Function:
// Fission executor кэширует Package по resourceVersion. Без обновления в Function
// executor будет использовать старый код до перезапуска pod.
// Создаёт НОВЫЙ Package (вместо обновления старого) чтобы executor сбросил кэш:
// executor кэширует function service по functionUid и не видит изменений в том же Package.
// Новое имя пакета гарантирует cache miss в executor.
func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request, name string) {
var req model.UpdateCodeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -529,21 +532,7 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
return
}
pkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
if pkgName == "" {
writeJSONError(w, http.StatusBadGateway, "function has no package reference")
return
}
pkg, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Get(ctx, pkgName, metav1.GetOptions{})
if err != nil {
status := http.StatusBadGateway
if apierrors.IsNotFound(err) {
status = http.StatusNotFound
}
writeJSONError(w, status, fmt.Sprintf("get package %q: %v", pkgName, err))
return
}
oldPkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
// Определяем язык из аннотации — нужен для правильной упаковки
lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language")
@@ -553,38 +542,83 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
return
}
literal := base64.StdEncoding.EncodeToString(deployBytes)
if err := unstructured.SetNestedField(pkg.Object, literal, "spec", "deployment", "literal"); err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set package literal: %v", err))
return
}
// Создаём новый Package с уникальным именем.
// Это единственный способ сбросить кэш executor: он кэширует по functionUid и
// не замечает изменений в существующем Package.
envName, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name")
createdAt := func() time.Time {
ann := fn.GetAnnotations()
if ann != nil {
if v := strings.TrimSpace(ann[functionCreatedAtAnnotation]); v != "" {
if ts, err := parseRFC3339(v); err == nil {
return ts.UTC()
}
}
}
if ts := fn.GetCreationTimestamp(); !ts.IsZero() {
return ts.UTC()
}
return time.Time{}
}()
now := time.Now().UTC()
newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36)
newPkg := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Package",
"metadata": map[string]any{"name": newPkgName, "namespace": ns},
"spec": map[string]any{
"deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
"environment": map[string]any{"name": envName, "namespace": ns},
"source": map[string]any{},
},
}}
updatedPkg, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Update(ctx, pkg, metav1.UpdateOptions{})
createdPkg, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Create(ctx, newPkg, metav1.CreateOptions{})
if err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("update package %q: %v", pkgName, err))
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create new package: %v", err))
return
}
// Обновляем Function на новый Package
if err := unstructured.SetNestedField(fn.Object, normalizeFunctionTimeout(req.Timeout), "spec", "functionTimeout"); err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set function timeout: %v", err))
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{})
return
}
// Синхронизируем resourceVersion в Function.spec.package.packageref
// Это триггерит executor перезагрузить код в pool pod
if err := unstructured.SetNestedField(fn.Object, updatedPkg.GetResourceVersion(), "spec", "package", "packageref", "resourceversion"); err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set function package resourceversion: %v", err))
ensureFunctionTimestamps(fn, now)
if createdAt.IsZero() {
createdAt = now
}
fnAnnotations := fn.GetAnnotations()
if fnAnnotations == nil {
fnAnnotations = map[string]string{}
}
fnAnnotations[functionCreatedAtAnnotation] = createdAt.UTC().Format(time.RFC3339)
fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339)
fn.SetAnnotations(fnAnnotations)
if err := unstructured.SetNestedField(fn.Object, map[string]any{
"name": newPkgName,
"namespace": ns,
"resourceversion": createdPkg.GetResourceVersion(),
}, "spec", "package", "packageref"); err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("set function packageref: %v", err))
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{})
return
}
if _, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Update(ctx, fn, metav1.UpdateOptions{}); err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("update function %q package ref: %v", name, err))
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("update function %q: %v", name, err))
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, newPkgName, metav1.DeleteOptions{})
return
}
// Удаляем старый Package (best effort)
if oldPkgName != "" && oldPkgName != newPkgName {
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, oldPkgName, metav1.DeleteOptions{})
}
writeAnyJSON(w, http.StatusOK, map[string]any{
"updated": true,
"package": pkgName,
"package_resourceversion": updatedPkg.GetResourceVersion(),
"updated": true,
"package": newPkgName,
})
}