Files
sless/internal/api/handler/handler.go
T
“Naeel” f41cd39b26 feat: namespace-per-user via JWT sub SHA256 + ensureNamespace in operator
- operator: ensureNamespace() создаёт k8s namespace при первом Create-запросе
- operator: defaultNamespace константа вместо хардкода 'default'
- provider: SubFromJWT декодирует JWT payload, извлекает sub
- provider: NamespaceFromSub вычисляет sless-{sha256[:8]} из sub
- provider: PingNubesAPI валидирует токен запросом к nubes API
- provider: Configure вычисляет namespace и создаёт Client с ним
- provider: новый атрибут nubes_endpoint (опционально, env: NUBES_ENDPOINT)
2026-03-11 07:35:49 +04:00

101 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Изменено: 2026-03-11
// Handler — общий контейнер зависимостей для всех REST handlers.
// Все handlers получают доступ к k8s, S3 и Postgres через эту структуру.
// Логирование через slog, маршрутизация через gorilla/mux.
//
// Архитектура namespace:
// - Каждый пользователь работает в своём k8s namespace.
// - Имя namespace вычисляется провайдером из JWT-токена (SHA256 от sub).
// - Оператор сам не создаёт namespace заранее — он делает это при первом
// Create-запросе через ensureNamespace().
// - defaultNamespace используется только как fallback для dev/тестов,
// когда namespace не передан в URL.
package handler
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"github.com/gorilla/mux"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/postgres"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/s3"
)
// defaultNamespace — fallback namespace для dev/тестов.
// В production namespace определяется из JWT-токена провайдером.
// Все обращения к "default" строке идут через эту константу — одно место замены.
const defaultNamespace = "default"
// Handler содержит зависимости для всех REST-обработчиков.
type Handler struct {
K8s client.Client
Scheme *runtime.Scheme
S3 *s3.Client
PG *postgres.Store
Log *slog.Logger
}
// writeJSON отправляет JSON-ответ с указанным статусом.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// errResp возвращает структуру ошибки для JSON.
func errResp(msg string) map[string]string {
return map[string]string{"error": msg}
}
// pathVar читает переменную пути из gorilla/mux.
func pathVar(r *http.Request, key string) string {
return mux.Vars(r)[key]
}
// namespace читает {namespace} из пути URL.
// Fallback — defaultNamespace (используется только в dev/тестах).
func namespace(r *http.Request) string {
if ns := mux.Vars(r)["namespace"]; ns != "" {
return ns
}
return defaultNamespace
}
// ensureNamespace создаёт k8s namespace если он не существует.
// Вызывается в начале каждого Create-хендлера (CreateFunction, CreateTrigger, CreateJob).
// Идемпотентен: если namespace уже есть — не ошибается.
// Зачем здесь, а не в контроллере: оператор получает запросы ДО того как
// контроллер может что-то создать, поэтому namespace нужен прямо при Create.
func (h *Handler) ensureNamespace(ctx context.Context, ns string) error {
existing := &corev1.Namespace{}
err := h.K8s.Get(ctx, client.ObjectKey{Name: ns}, existing)
if err == nil {
// namespace уже существует — всё ок
return nil
}
if !k8serrors.IsNotFound(err) {
// неожиданная ошибка k8s API
return err
}
// namespace не существует — создаём
ns_obj := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ns,
// label для идентификации namespace как принадлежащего sless
Labels: map[string]string{
"managed-by": "sless-operator",
},
},
}
return h.K8s.Create(ctx, ns_obj)
}