refactor: split main.go into packages (internal/api, internal/fission, internal/runtime, cmd/server)

This commit is contained in:
Naeel
2026-04-25 23:53:20 +03:00
parent 1f4de2ec15
commit 60646af0c8
20 changed files with 2799 additions and 1 deletions
+125
View File
@@ -0,0 +1,125 @@
package api
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"
)
// ctxKeyNS — ключ для хранения namespace пользователя в context.Context.
// Использует приватный тип чтобы избежать коллизий с ключами из других пакетов.
type ctxKeyNS struct{}
// userNS возвращает namespace пользователя из контекста запроса.
// Устанавливается в authMiddleware после успешной аутентификации.
func (s *Server) userNS(r *http.Request) string {
if ns, ok := r.Context().Value(ctxKeyNS{}).(string); ok && ns != "" {
return ns
}
// Fallback: использовать системный namespace (не должно происходить в prod)
return s.ns
}
// authMiddleware оборачивает handler, добавляя аутентификацию и инициализацию namespace.
//
// В testMode (FISSION_TEST_MODE=true):
// - Deck API не вызывается
// - X-Test-Sub или X-Auth-Token задают sub → разные namespace-ы для тестирования
//
// В production:
// - X-Auth-Token валидируется через Deck API
// - Namespace вычисляется из JWT claim "sub"
func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var ns string
if s.testMode {
sub := strings.TrimSpace(r.Header.Get("X-Test-Sub"))
if sub == "" {
sub = strings.TrimSpace(r.Header.Get("X-Auth-Token"))
}
if sub == "" {
writeJSONError(w, http.StatusUnauthorized, "test mode: X-Test-Sub required")
return
}
h32 := sha256.Sum256([]byte(sub))
ns = "fission-" + hex.EncodeToString(h32[:8])
} else {
token := strings.TrimSpace(r.Header.Get("X-Auth-Token"))
env := strings.TrimSpace(strings.ToLower(r.Header.Get("X-Auth-Env")))
if _, ok := deckAPIs[env]; !ok {
env = "test"
}
if token == "" {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
if err := s.validateDeckToken(token, env); err != nil {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
var err error
ns, err = namespaceFromJWT(token)
if err != nil {
ns = s.ns
}
}
ctx := context.WithValue(r.Context(), ctxKeyNS{}, ns)
// Гарантируем что namespace + RBAC + quota + netpol существуют.
// EnsureUserNS реализует singleflight + кэш + семафор параллелизма.
if ensureErr := s.nsManager.EnsureUserNS(ctx, ns); ensureErr != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure namespace: %v", ensureErr))
return
}
h(w, r.WithContext(ctx))
}
}
// namespaceFromJWT декодирует JWT payload (без верификации подписи),
// извлекает claim "sub" и вычисляет namespace: "fission-" + hex(SHA256(sub)[:8]).
//
// Подпись не проверяется — токен уже валидирован через Deck API (validateDeckToken).
// Здесь нам нужен только deterministic namespace name из sub claim.
func namespaceFromJWT(token string) (string, error) {
parts := strings.SplitN(token, ".", 3)
if len(parts) != 3 {
return "", fmt.Errorf("invalid JWT format")
}
payload := parts[1]
// JWT использует base64url без padding — добавляем если нужно
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
// base64url без стандартного padding — пробуем оба варианта
decoded, err := base64.URLEncoding.DecodeString(payload)
if err != nil {
decoded, err = base64.StdEncoding.DecodeString(payload)
if err != nil {
return "", fmt.Errorf("decode JWT payload: %w", err)
}
}
var claims map[string]any
if err := json.Unmarshal(decoded, &claims); err != nil {
return "", fmt.Errorf("unmarshal JWT claims: %w", err)
}
sub, _ := claims["sub"].(string)
if sub == "" {
return "", fmt.Errorf("JWT missing sub claim")
}
h := sha256.Sum256([]byte(sub))
return "fission-" + hex.EncodeToString(h[:8]), nil
}