@@ -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
}