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)
This commit is contained in:
“Naeel”
2026-03-11 07:35:49 +04:00
parent 6dff628975
commit f41cd39b26
6 changed files with 236 additions and 24 deletions
+50 -3
View File
@@ -1,16 +1,28 @@
// Изменено: 2026-03-07
// Изменено: 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"
@@ -18,6 +30,11 @@ import (
"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
@@ -44,10 +61,40 @@ func pathVar(r *http.Request, key string) string {
return mux.Vars(r)[key]
}
// namespace читает {namespace} из пути, fallback — "default".
// namespace читает {namespace} из пути URL.
// Fallback — defaultNamespace (используется только в dev/тестах).
func namespace(r *http.Request) string {
if ns := mux.Vars(r)["namespace"]; ns != "" {
return ns
}
return "default"
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)
}