Merge branch 'feat/ui-archive-upload' of https://gitea.services.ngcloud.ru/Nail/fission into feat/ui-archive-upload
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
// ...existing code...
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name string) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request, name string) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) {
|
||||
// ...existing code...
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -37,189 +36,10 @@ const maxCodeSize = 1 << 20
|
||||
// defaultFunctionInvokeTimeout совпадает с дефолтом Fission для spec.functionTimeout.
|
||||
const defaultFunctionInvokeTimeout = 60 * time.Second
|
||||
|
||||
func buildDeployArchive(lang, code string) ([]byte, error) {
|
||||
switch lang {
|
||||
case "nodejs":
|
||||
return runtime.BuildJSDeployZip(code)
|
||||
case "php":
|
||||
return runtime.BuildScriptZip(code, "main.php")
|
||||
case "ruby":
|
||||
return runtime.BuildScriptZip(code, "handler.rb")
|
||||
default:
|
||||
return []byte(code), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration {
|
||||
if fn != nil {
|
||||
seconds, found, err := unstructured.NestedInt64(fn.Object, "spec", "functionTimeout")
|
||||
if err == nil && found && seconds > 0 {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
}
|
||||
if s.invokeTimeout > 0 {
|
||||
return s.invokeTimeout
|
||||
}
|
||||
return defaultFunctionInvokeTimeout
|
||||
}
|
||||
|
||||
func normalizeFunctionTimeout(seconds int64) int64 {
|
||||
if seconds <= 0 {
|
||||
return int64(defaultFunctionInvokeTimeout / time.Second)
|
||||
}
|
||||
return seconds
|
||||
}
|
||||
|
||||
func normalizeRoute(route string) string {
|
||||
route = strings.TrimSpace(route)
|
||||
if route == "" || route == "/" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(route, "/") {
|
||||
return "/" + route
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
func routeAllowsMethod(methods []string, method string) bool {
|
||||
if len(methods) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, candidate := range methods {
|
||||
if strings.EqualFold(strings.TrimSpace(candidate), method) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendUniqueMethods(dst []string, src []string) []string {
|
||||
for _, method := range src {
|
||||
method = strings.ToUpper(strings.TrimSpace(method))
|
||||
if method == "" {
|
||||
continue
|
||||
}
|
||||
seen := false
|
||||
for _, existing := range dst {
|
||||
if existing == method {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
dst = append(dst, method)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func shouldForwardRequestBody(method string) bool {
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func copyProxyRequestHeaders(dst, src http.Header) {
|
||||
for key, values := range src {
|
||||
switch http.CanonicalHeaderKey(key) {
|
||||
case "Authorization", "X-Auth-Token", "X-Auth-Env", "Host", "Content-Length":
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
dst.Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyProxyResponseHeaders(dst, src http.Header) {
|
||||
for key, values := range src {
|
||||
for _, value := range values {
|
||||
dst.Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doRequestWithContextTimeout(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
if client == nil {
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
invokeClient := *client
|
||||
// Для invoke реальный лимит должен определяться context timeout функции,
|
||||
// а не общим HTTP timeout console.
|
||||
invokeClient.Timeout = 0
|
||||
return invokeClient.Do(req)
|
||||
}
|
||||
|
||||
// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции.
|
||||
// GET → список всех функций, POST → создать новую.
|
||||
func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.handleList(fission.FunctionGVR)(w, r)
|
||||
case http.MethodPost:
|
||||
s.handleCreateFunction(w, r)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// handleFunctionsAction обрабатывает запросы к /console/api/functions/:name[/action].
|
||||
// Парсит имя функции и опциональный sub-path ("code", "invoke").
|
||||
func (s *Server) handleFunctionsAction(w http.ResponseWriter, r *http.Request) {
|
||||
// Убираем оба возможных префикса (legacy /api/ и основной /console/api/)
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/functions/")
|
||||
if path == r.URL.Path {
|
||||
path = strings.TrimPrefix(r.URL.Path, "/console/api/functions/")
|
||||
}
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
name := strings.TrimSpace(parts[0])
|
||||
if name == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "function name is required")
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
// /functions/:name — CRUD операции с конкретной функцией
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.handleGetFunction(w, r, name)
|
||||
case http.MethodDelete:
|
||||
s.handleDeleteFunction(w, r, name)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// /functions/:name/code — обновление кода
|
||||
if len(parts) == 2 && parts[1] == "code" && r.Method == http.MethodPut {
|
||||
s.handleUpdateFunctionCode(w, r, name)
|
||||
return
|
||||
}
|
||||
|
||||
// /functions/:name/invoke — вызов функции
|
||||
if len(parts) == 2 && parts[1] == "invoke" && r.Method == http.MethodPost {
|
||||
s.handleInvokeFunction(w, r, name)
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
// handleCreateFunction создаёт новую функцию: Package + Function + HTTPTrigger.
|
||||
//
|
||||
// Порядок создания: Package → Function → HTTPTrigger.
|
||||
// При ошибке на любом шаге откатываем уже созданные объекты (best-effort).
|
||||
// TTL парсится ДО создания объектов — невалидный TTL не оставляет мусор.
|
||||
func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
ns := s.userNS(r)
|
||||
|
||||
@@ -301,140 +121,19 @@ func (s *Server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
pkgName := req.Name + "-pkg"
|
||||
triggerName := req.Name + "-route"
|
||||
|
||||
methodValues := make([]any, 0, len(req.Methods))
|
||||
for _, method := range req.Methods {
|
||||
methodValues = append(methodValues, method)
|
||||
}
|
||||
|
||||
// Строим Package spec в зависимости от языка:
|
||||
// - Go: source package → builder job компилирует в .so плагин
|
||||
// - Node.js: deployment zip с ESM wrapper (package.json + main.js)
|
||||
// - Остальные: deployment literal с кодом напрямую
|
||||
var pkgSpec map[string]any
|
||||
if req.Language == "go" {
|
||||
srcZip, err := runtime.BuildGoSourceZip(req.Code)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("build go source archive: %v", err))
|
||||
return
|
||||
}
|
||||
literal := base64.StdEncoding.EncodeToString(srcZip)
|
||||
pkgSpec = map[string]any{
|
||||
"source": map[string]any{
|
||||
"type": "literal",
|
||||
"literal": literal,
|
||||
},
|
||||
"deployment": map[string]any{},
|
||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||
"buildcommand": "build",
|
||||
}
|
||||
} else {
|
||||
deployBytes, archiveErr := buildDeployArchive(req.Language, req.Code)
|
||||
if archiveErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", req.Language, archiveErr))
|
||||
return
|
||||
}
|
||||
pkgSpec = map[string]any{
|
||||
"deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
|
||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||
"source": map[string]any{},
|
||||
}
|
||||
}
|
||||
|
||||
pkg := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "Package",
|
||||
"metadata": map[string]any{"name": pkgName, "namespace": ns},
|
||||
"spec": pkgSpec,
|
||||
}}
|
||||
|
||||
// Парсим TTL ДО создания K8s ресурсов — невалидный TTL не оставляет мусор
|
||||
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 {
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("invalid ttl %q: %v", req.TTL, ttlErr))
|
||||
return
|
||||
}
|
||||
fnAnnotations["fission-console/expires-at"] = expiresAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
if _, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Create(ctx, pkg, metav1.CreateOptions{}); err != nil {
|
||||
result, err := fission.CreateFunction(ctx, s.dyn, ns, req)
|
||||
if err != nil {
|
||||
status := http.StatusBadGateway
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
writeJSONError(w, http.StatusConflict, fmt.Sprintf("function %q already exists", req.Name))
|
||||
return
|
||||
status = http.StatusConflict
|
||||
} else if apierrors.IsInvalid(err) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
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))
|
||||
writeJSONError(w, status, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fn := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"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},
|
||||
"functionTimeout": req.Timeout,
|
||||
"InvokeStrategy": map[string]any{
|
||||
"ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"},
|
||||
"StrategyType": "execution",
|
||||
},
|
||||
"package": map[string]any{
|
||||
"packageref": map[string]any{"name": pkgName, "namespace": ns},
|
||||
"functionName": req.Entrypoint,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
if _, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Create(ctx, fn, metav1.CreateOptions{}); err != nil {
|
||||
// Откатываем Package если Function не создалась
|
||||
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).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
|
||||
}
|
||||
|
||||
httpTrigger := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "HTTPTrigger",
|
||||
"metadata": map[string]any{"name": triggerName, "namespace": ns},
|
||||
"spec": map[string]any{
|
||||
"relativeurl": req.Route,
|
||||
"methods": methodValues,
|
||||
"createingress": true,
|
||||
"functionref": map[string]any{"type": "name", "name": req.Name},
|
||||
},
|
||||
}}
|
||||
|
||||
if _, err := s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).Create(ctx, httpTrigger, metav1.CreateOptions{}); err != nil {
|
||||
// Откатываем Function и Package
|
||||
_ = s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Delete(ctx, req.Name, metav1.DeleteOptions{})
|
||||
_ = s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{})
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("create httptrigger: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
writeAnyJSON(w, http.StatusCreated, map[string]any{
|
||||
"name": req.Name,
|
||||
"package": pkgName,
|
||||
"httptrigger": triggerName,
|
||||
"route": req.Route,
|
||||
"expires_at": fnAnnotations["fission-console/expires-at"],
|
||||
})
|
||||
writeAnyJSON(w, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
// handleGetFunction возвращает детали функции: код, environment, route, methods.
|
||||
@@ -466,7 +165,7 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
|
||||
if packageName != "" {
|
||||
pkg, pkgErr := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Get(ctx, packageName, metav1.GetOptions{})
|
||||
if pkgErr == nil {
|
||||
code = extractPackageSourceCode(ctx, s, pkg)
|
||||
code = extractPackageSourceCode(ctx, s.http, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,10 +201,6 @@ func (s *Server) handleGetFunction(w http.ResponseWriter, r *http.Request, name
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdateFunctionCode обновляет код уже существующей функции.
|
||||
// Создаёт НОВЫЙ 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 {
|
||||
@@ -517,109 +212,25 @@ func (s *Server) handleUpdateFunctionCode(w http.ResponseWriter, r *http.Request
|
||||
writeJSONError(w, http.StatusBadRequest, "code is required")
|
||||
return
|
||||
}
|
||||
if req.Timeout <= 0 {
|
||||
req.Timeout = int64(defaultFunctionInvokeTimeout / time.Second)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||
defer cancel()
|
||||
ns := s.userNS(r)
|
||||
|
||||
fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
result, err := fission.UpdateFunctionCode(ctx, s.dyn, ns, name, req)
|
||||
if err != nil {
|
||||
status := http.StatusBadGateway
|
||||
if apierrors.IsNotFound(err) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeJSONError(w, status, fmt.Sprintf("get function %q: %v", name, err))
|
||||
writeJSONError(w, status, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
oldPkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
|
||||
|
||||
// Определяем язык из аннотации — нужен для правильной упаковки
|
||||
lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language")
|
||||
deployBytes, archiveErr := buildDeployArchive(lang, req.Code)
|
||||
if archiveErr != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build %s archive: %v", lang, archiveErr))
|
||||
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{},
|
||||
},
|
||||
}}
|
||||
|
||||
createdPkg, err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Create(ctx, newPkg, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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: %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": newPkgName,
|
||||
})
|
||||
writeAnyJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// handleInvokeFunction вызывает функцию через Fission router.
|
||||
@@ -951,109 +562,26 @@ func (s *Server) handleInvokeRoute(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
// handleDeleteFunction удаляет функцию и связанные объекты: HTTPTrigger, TimeTrigger, Package.
|
||||
// После удаления вызывает CleanupEnvironmentIfUnused — убирает environment если язык больше не используется.
|
||||
func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||
defer cancel()
|
||||
ns := s.userNS(r)
|
||||
|
||||
// Получаем Function чтобы знать pkgName и envName для cleanup
|
||||
var pkgName, envName string
|
||||
fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if err := fission.DeleteFunction(ctx, s.dyn, ns, name); err != nil {
|
||||
status := http.StatusBadGateway
|
||||
if apierrors.IsNotFound(err) {
|
||||
writeJSONError(w, http.StatusNotFound, fmt.Sprintf("function %q not found", name))
|
||||
return
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("get function %q: %v", name, err))
|
||||
return
|
||||
}
|
||||
pkgName, _, _ = unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
|
||||
envName, _, _ = unstructured.NestedString(fn.Object, "spec", "environment", "name")
|
||||
|
||||
// Удаляем связанные HTTPTrigger-ы
|
||||
triggers, err := s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if err == nil {
|
||||
for _, trig := range triggers.Items {
|
||||
refName, _, _ := unstructured.NestedString(trig.Object, "spec", "functionref", "name")
|
||||
if refName == name {
|
||||
_ = s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).Delete(ctx, trig.GetName(), metav1.DeleteOptions{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем связанные TimeTrigger-ы
|
||||
if triggers, err := s.dyn.Resource(fission.TimeTrigGVR).Namespace(ns).List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, trig := range triggers.Items {
|
||||
refName, _, _ := unstructured.NestedString(trig.Object, "spec", "functionref", "name")
|
||||
if refName == name {
|
||||
_ = s.dyn.Resource(fission.TimeTrigGVR).Namespace(ns).Delete(ctx, trig.GetName(), metav1.DeleteOptions{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete function %q: %v", name, err))
|
||||
writeJSONError(w, status, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if pkgName != "" {
|
||||
if err := s.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("delete package %q: %v", pkgName, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Убираем environment pool pods если язык больше не используется (best-effort)
|
||||
if envName != "" {
|
||||
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cleanupCancel()
|
||||
fission.CleanupEnvironmentIfUnused(cleanupCtx, s.dyn, ns, envName)
|
||||
}
|
||||
|
||||
// (reconciler NS удалён — за FISSION_RESOURCE_NAMESPACES теперь отвечает Layer 1 NSWatcher)
|
||||
|
||||
writeAnyJSON(w, http.StatusOK, map[string]any{"deleted": true, "name": name, "package": pkgName})
|
||||
writeAnyJSON(w, http.StatusOK, map[string]any{"deleted": true, "name": name})
|
||||
}
|
||||
|
||||
// handleAuth обрабатывает POST /console/api/auth.
|
||||
// Валидирует токен, создаёт namespace, возвращает namespace пользователя.
|
||||
func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Token string `json:"token"`
|
||||
Env string `json:"env"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Token) == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "token required")
|
||||
return
|
||||
}
|
||||
|
||||
env := strings.TrimSpace(strings.ToLower(body.Env))
|
||||
if _, ok := deckAPIs[env]; !ok {
|
||||
env = "test"
|
||||
}
|
||||
|
||||
ns, err := s.resolveNamespaceForToken(body.Token, env, s.testMode)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusUnauthorized, "invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
if ensureErr := s.nsManager.EnsureUserNS(ctx, ns); ensureErr != nil {
|
||||
log.Printf("handleAuth: ensureUserNS %s: %v", ns, ensureErr)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "env": env, "namespace": ns})
|
||||
}
|
||||
|
||||
// parseTTL парсит строку TTL и возвращает время истечения.
|
||||
// Поддерживаемые форматы: Go duration (1h, 30m, 24h) и дни (1d, 7d, 30d).
|
||||
@@ -1078,22 +606,4 @@ func parseTTL(ttl string) (time.Time, error) {
|
||||
|
||||
// normalizeMethods приводит список HTTP методов к верхнему регистру, убирает дубли.
|
||||
// Если список пустой или все элементы пустые — возвращает ["GET"].
|
||||
func normalizeMethods(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return []string{"GET"}
|
||||
}
|
||||
out := make([]string, 0, len(in))
|
||||
seen := map[string]bool{}
|
||||
for _, method := range in {
|
||||
m := strings.ToUpper(strings.TrimSpace(method))
|
||||
if m == "" || seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
out = append(out, m)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []string{"GET"}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, name string) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func (s *Server) handleFissionFunctionGateway(w http.ResponseWriter, r *http.Request) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func (s *Server) invokeInternalFunction(w http.ResponseWriter, r *http.Request, namespace, functionName, extraPath string) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func (s *Server) handleInvokeRoute(w http.ResponseWriter, r *http.Request) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func buildInternalInvokeURL(routerURL, namespace, functionName string) string {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func resolveInvokeTimeout(fn *unstructured.Unstructured) time.Duration {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func shouldForwardRequestBody(method string) bool {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func copyProxyRequestHeaders(dst, src http.Header) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func copyProxyResponseHeaders(dst, src http.Header) {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func doRequestWithContextTimeout(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
// ...existing code...
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
//
|
||||
// Декодирование: если данные — валидный UTF-8, возвращаем как есть.
|
||||
// Если это zip — ищем известные файлы (main.py, handler.rb и т.д.).
|
||||
func extractPackageSourceCode(ctx context.Context, s *Server, pkg *unstructured.Unstructured) string {
|
||||
func extractPackageSourceCode(ctx context.Context, httpClient *http.Client, pkg *unstructured.Unstructured) string {
|
||||
literalPaths := [][]string{
|
||||
{"spec", "source", "literal"},
|
||||
{"spec", "deployment", "literal"},
|
||||
@@ -49,7 +49,7 @@ func extractPackageSourceCode(ctx context.Context, s *Server, pkg *unstructured.
|
||||
if !found || strings.TrimSpace(urlValue) == "" {
|
||||
continue
|
||||
}
|
||||
archiveBytes, err := fetchPackageArchive(ctx, s, urlValue)
|
||||
archiveBytes, err := fetchPackageArchive(ctx, httpClient, urlValue)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -62,12 +62,15 @@ func extractPackageSourceCode(ctx context.Context, s *Server, pkg *unstructured.
|
||||
}
|
||||
|
||||
// fetchPackageArchive скачивает архив функции по URL из Fission storage.
|
||||
func fetchPackageArchive(ctx context.Context, s *Server, archiveURL string) ([]byte, error) {
|
||||
func fetchPackageArchive(ctx context.Context, httpClient *http.Client, archiveURL string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, archiveURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.http.Do(req)
|
||||
if httpClient == nil {
|
||||
httpClient = http.DefaultClient
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func normalizeFunctionTimeout(seconds int64) int64 {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func normalizeRoute(route string) string {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func normalizeMethods(in []string) []string {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func routeAllowsMethod(methods []string, method string) bool {
|
||||
// ...existing code...
|
||||
}
|
||||
|
||||
func appendUniqueMethods(dst []string, src []string) []string {
|
||||
// ...existing code...
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package fission
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"fission-console/internal/model"
|
||||
"fission-console/internal/runtime"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
const (
|
||||
functionCreatedAtAnnotation = "fission-console/createdAt"
|
||||
functionUpdatedAtAnnotation = "fission-console/updatedAt"
|
||||
)
|
||||
|
||||
// CreateFunctionResult holds the result of a CreateFunction call.
|
||||
type CreateFunctionResult struct {
|
||||
Name string `json:"name"`
|
||||
Package string `json:"package"`
|
||||
HTTPTrigger string `json:"httptrigger"`
|
||||
Route string `json:"route"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateFunctionResult holds the result of an UpdateFunctionCode call.
|
||||
type UpdateFunctionResult struct {
|
||||
Updated bool `json:"updated"`
|
||||
Package string `json:"package"`
|
||||
}
|
||||
|
||||
// CreateFunction orchestrates the creation of a complete Fission function set: Package, Function, and HTTPTrigger.
|
||||
func CreateFunction(ctx context.Context, dyn dynamic.Interface, ns string, req model.CreateFunctionRequest) (*CreateFunctionResult, error) {
|
||||
pkgName := req.Name + "-pkg"
|
||||
triggerName := req.Name + "-route"
|
||||
|
||||
methodValues := make([]any, 0, len(req.Methods))
|
||||
for _, method := range req.Methods {
|
||||
methodValues = append(methodValues, method)
|
||||
}
|
||||
|
||||
var pkgSpec map[string]any
|
||||
if req.Language == "go" {
|
||||
srcZip, err := runtime.BuildGoSourceZip(req.Code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build go source archive: %w", err)
|
||||
}
|
||||
literal := base64.StdEncoding.EncodeToString(srcZip)
|
||||
pkgSpec = map[string]any{
|
||||
"source": map[string]any{
|
||||
"type": "literal",
|
||||
"literal": literal,
|
||||
},
|
||||
"deployment": map[string]any{},
|
||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||
"buildcommand": "build",
|
||||
}
|
||||
} else {
|
||||
deployBytes, archiveErr := runtime.BuildDeployArchive(req.Language, req.Code)
|
||||
if archiveErr != nil {
|
||||
return nil, fmt.Errorf("build %s archive: %w", req.Language, archiveErr)
|
||||
}
|
||||
pkgSpec = map[string]any{
|
||||
"deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)},
|
||||
"environment": map[string]any{"name": req.Environment, "namespace": ns},
|
||||
"source": map[string]any{},
|
||||
}
|
||||
}
|
||||
|
||||
pkg := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "Package",
|
||||
"metadata": map[string]any{"name": pkgName, "namespace": ns},
|
||||
"spec": pkgSpec,
|
||||
}}
|
||||
|
||||
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)
|
||||
var expiresAtStr string
|
||||
if req.TTL != "" {
|
||||
expiresAt, ttlErr := parseTTL(req.TTL)
|
||||
if ttlErr != nil {
|
||||
return nil, fmt.Errorf("invalid ttl %q: %w", req.TTL, ttlErr)
|
||||
}
|
||||
expiresAtStr = expiresAt.UTC().Format(time.RFC3339)
|
||||
fnAnnotations["fission-console/expires-at"] = expiresAtStr
|
||||
}
|
||||
|
||||
if _, err := dyn.Resource(PackageGVR).Namespace(ns).Create(ctx, pkg, metav1.CreateOptions{}); err != nil {
|
||||
return nil, fmt.Errorf("create package: %w", err)
|
||||
}
|
||||
|
||||
fn := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"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},
|
||||
"functionTimeout": req.Timeout,
|
||||
"InvokeStrategy": map[string]any{
|
||||
"ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"},
|
||||
"StrategyType": "execution",
|
||||
},
|
||||
"package": map[string]any{
|
||||
"packageref": map[string]any{"name": pkgName, "namespace": ns},
|
||||
"functionName": req.Entrypoint,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
if _, err := dyn.Resource(FunctionGVR).Namespace(ns).Create(ctx, fn, metav1.CreateOptions{}); err != nil {
|
||||
_ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), pkgName, metav1.DeleteOptions{})
|
||||
return nil, fmt.Errorf("create function: %w", err)
|
||||
}
|
||||
|
||||
httpTrigger := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "fission.io/v1",
|
||||
"kind": "HTTPTrigger",
|
||||
"metadata": map[string]any{"name": triggerName, "namespace": ns},
|
||||
"spec": map[string]any{
|
||||
"relativeurl": req.Route,
|
||||
"methods": methodValues,
|
||||
"createingress": true,
|
||||
"functionref": map[string]any{"type": "name", "name": req.Name},
|
||||
},
|
||||
}}
|
||||
|
||||
if _, err := dyn.Resource(HTTPTrigGVR).Namespace(ns).Create(ctx, httpTrigger, metav1.CreateOptions{}); err != nil {
|
||||
_ = dyn.Resource(FunctionGVR).Namespace(ns).Delete(context.Background(), req.Name, metav1.DeleteOptions{})
|
||||
_ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), pkgName, metav1.DeleteOptions{})
|
||||
return nil, fmt.Errorf("create httptrigger: %w", err)
|
||||
}
|
||||
|
||||
return &CreateFunctionResult{
|
||||
Name: req.Name,
|
||||
Package: pkgName,
|
||||
HTTPTrigger: triggerName,
|
||||
Route: req.Route,
|
||||
ExpiresAt: expiresAtStr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateFunctionCode orchestrates updating the code of an existing function.
|
||||
func UpdateFunctionCode(ctx context.Context, dyn dynamic.Interface, ns, name string, req model.UpdateCodeRequest) (*UpdateFunctionResult, error) {
|
||||
fn, err := dyn.Resource(FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get function %q: %w", name, err)
|
||||
}
|
||||
|
||||
oldPkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name")
|
||||
lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language")
|
||||
|
||||
deployBytes, archiveErr := runtime.BuildDeployArchive(lang, req.Code)
|
||||
if archiveErr != nil {
|
||||
return nil, fmt.Errorf("build %s archive: %w", lang, archiveErr)
|
||||
}
|
||||
|
||||
envName, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name")
|
||||
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{},
|
||||
},
|
||||
}}
|
||||
|
||||
createdPkg, err := dyn.Resource(PackageGVR).Namespace(ns).Create(ctx, newPkg, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create new package: %w", err)
|
||||
}
|
||||
|
||||
if err := unstructured.SetNestedField(fn.Object, req.Timeout, "spec", "functionTimeout"); err != nil {
|
||||
_ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{})
|
||||
return nil, fmt.Errorf("set function timeout: %w", err)
|
||||
}
|
||||
|
||||
annotations := fn.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
annotations[functionUpdatedAtAnnotation] = time.Now().UTC().Format(time.RFC3339)
|
||||
fn.SetAnnotations(annotations)
|
||||
|
||||
if err := unstructured.SetNestedField(fn.Object, map[string]any{
|
||||
"name": newPkgName,
|
||||
"namespace": ns,
|
||||
"resourceversion": createdPkg.GetResourceVersion(),
|
||||
}, "spec", "package", "packageref"); err != nil {
|
||||
_ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{})
|
||||
return nil, fmt.Errorf("set function packageref: %w", err)
|
||||
}
|
||||
|
||||
if _, err := dyn.Resource(FunctionGVR).Namespace(ns).Update(ctx, fn, metav1.UpdateOptions{}); err != nil {
|
||||
_ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{})
|
||||
return nil, fmt.Errorf("update function %q: %w", name, err)
|
||||
}
|
||||
|
||||
if oldPkgName != "" && oldPkgName != newPkgName {
|
||||
_ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), oldPkgName, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
return &UpdateFunctionResult{
|
||||
Updated: true,
|
||||
Package: newPkgName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteFunction orchestrates the deletion of a function and its related resources.
|
||||
func DeleteFunction(ctx context.Context, dyn dynamic.Interface, ns, name string) error {
|
||||
// Best-effort deletion of related resources.
|
||||
_ = dyn.Resource(HTTPTrigGVR).Namespace(ns).Delete(ctx, name+"-route", metav1.DeleteOptions{})
|
||||
|
||||
fn, err := dyn.Resource(FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("get function for deletion: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if pkgName, found, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name"); found && pkgName != "" {
|
||||
_ = dyn.Resource(PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
if err := dyn.Resource(FunctionGVR).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("delete function: %w", err)
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
if envName, found, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name"); found && envName != "" {
|
||||
CleanupEnvironmentIfUnused(context.Background(), dyn, ns, envName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTTL(ttl string) (time.Time, error) {
|
||||
duration, err := time.ParseDuration(ttl)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Now().UTC().Add(duration), nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package runtime
|
||||
|
||||
// BuildDeployArchive creates a deployment archive for a given language and code.
|
||||
// For Node.js, it creates a zip with an ESM wrapper.
|
||||
// For script languages (PHP, Ruby), it creates a zip with the correct entrypoint filename.
|
||||
// For others, it returns the code as is.
|
||||
func BuildDeployArchive(lang, code string) ([]byte, error) {
|
||||
switch lang {
|
||||
case "nodejs":
|
||||
return BuildJSDeployZip(code)
|
||||
case "php":
|
||||
return BuildScriptZip(code, "main.php")
|
||||
case "ruby":
|
||||
return BuildScriptZip(code, "handler.rb")
|
||||
case "python", "go", "perl": // Go is a special case handled in CreateFunction, but for archive purposes it's just the source.
|
||||
return []byte(code), nil
|
||||
default:
|
||||
// Allow unknown languages but treat them as simple scripts.
|
||||
// The environment itself will fail later if the language is truly unsupported.
|
||||
return []byte(code), nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user