network/pkg/gateway/handlers/serverless/deploy_handler.go
2026-01-20 10:03:55 +02:00

174 lines
4.5 KiB
Go

package serverless
import (
"context"
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/serverless"
"go.uber.org/zap"
)
// DeployFunction handles POST /v1/functions
// Deploys a new function or updates an existing one.
func (h *ServerlessHandlers) DeployFunction(w http.ResponseWriter, r *http.Request) {
// Parse multipart form (for WASM upload) or JSON
contentType := r.Header.Get("Content-Type")
var def serverless.FunctionDefinition
var wasmBytes []byte
if strings.HasPrefix(contentType, "multipart/form-data") {
// Parse multipart form
if err := r.ParseMultipartForm(32 << 20); err != nil { // 32MB max
writeError(w, http.StatusBadRequest, "Failed to parse form: "+err.Error())
return
}
// Get metadata from form field
metadataStr := r.FormValue("metadata")
if metadataStr != "" {
if err := json.Unmarshal([]byte(metadataStr), &def); err != nil {
writeError(w, http.StatusBadRequest, "Invalid metadata JSON: "+err.Error())
return
}
}
// Get name from form if not in metadata
if def.Name == "" {
def.Name = r.FormValue("name")
}
// Get namespace from form if not in metadata
if def.Namespace == "" {
def.Namespace = r.FormValue("namespace")
}
// Get other configuration fields from form
if v := r.FormValue("is_public"); v != "" {
def.IsPublic, _ = strconv.ParseBool(v)
}
if v := r.FormValue("memory_limit_mb"); v != "" {
def.MemoryLimitMB, _ = strconv.Atoi(v)
}
if v := r.FormValue("timeout_seconds"); v != "" {
def.TimeoutSeconds, _ = strconv.Atoi(v)
}
if v := r.FormValue("retry_count"); v != "" {
def.RetryCount, _ = strconv.Atoi(v)
}
if v := r.FormValue("retry_delay_seconds"); v != "" {
def.RetryDelaySeconds, _ = strconv.Atoi(v)
}
// Get WASM file
file, _, err := r.FormFile("wasm")
if err != nil {
writeError(w, http.StatusBadRequest, "WASM file required")
return
}
defer file.Close()
wasmBytes, err = io.ReadAll(file)
if err != nil {
writeError(w, http.StatusBadRequest, "Failed to read WASM file: "+err.Error())
return
}
} else {
// JSON body with base64-encoded WASM
var req struct {
serverless.FunctionDefinition
WASMBase64 string `json:"wasm_base64"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
def = req.FunctionDefinition
if req.WASMBase64 != "" {
// Decode base64 WASM - for now, just reject this method
writeError(w, http.StatusBadRequest, "Base64 WASM upload not supported, use multipart/form-data")
return
}
}
// Get namespace from JWT if not provided
if def.Namespace == "" {
def.Namespace = h.getNamespaceFromRequest(r)
}
if def.Name == "" {
writeError(w, http.StatusBadRequest, "Function name required")
return
}
if def.Namespace == "" {
writeError(w, http.StatusBadRequest, "Namespace required")
return
}
if len(wasmBytes) == 0 {
writeError(w, http.StatusBadRequest, "WASM bytecode required")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
oldFn, err := h.registry.Register(ctx, &def, wasmBytes)
if err != nil {
h.logger.Error("Failed to deploy function",
zap.String("name", def.Name),
zap.Error(err),
)
writeError(w, http.StatusInternalServerError, "Failed to deploy: "+err.Error())
return
}
// Invalidate cache for the old version to ensure the new one is loaded
if oldFn != nil {
h.invoker.InvalidateCache(oldFn.WASMCID)
h.logger.Debug("Invalidated function cache",
zap.String("name", def.Name),
zap.String("old_wasm_cid", oldFn.WASMCID),
)
}
h.logger.Info("Function deployed",
zap.String("name", def.Name),
zap.String("namespace", def.Namespace),
)
// Fetch the deployed function to return
fn, err := h.registry.Get(ctx, def.Namespace, def.Name, def.Version)
if err != nil {
writeJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Function deployed successfully",
"name": def.Name,
})
return
}
writeJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Function deployed successfully",
"function": fn,
})
}
// writeJSON writes JSON with status code
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
// writeError writes a standardized JSON error
func writeError(w http.ResponseWriter, code int, msg string) {
writeJSON(w, code, map[string]any{"error": msg})
}