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:
@@ -1,4 +1,4 @@
|
||||
// Изменено: 2026-03-07
|
||||
// Изменено: 2026-03-11
|
||||
// functions.go — CRUD handlers для Function CRD.
|
||||
// Принимает JSON, создаёт/обновляет/удаляет k8s ресурсы Function.
|
||||
// Namespace берётся из URL: /v1/namespaces/{namespace}/functions/{name}
|
||||
@@ -80,6 +80,12 @@ func (h *Handler) ListFunctions(w http.ResponseWriter, r *http.Request) {
|
||||
// CreateFunction — POST /v1/namespaces/{namespace}/functions
|
||||
func (h *Handler) CreateFunction(w http.ResponseWriter, r *http.Request) {
|
||||
ns := namespace(r)
|
||||
// Гарантируем существование namespace перед созданием Function CRD.
|
||||
// Подробнее про логику — см. ensureNamespace() в handler.go.
|
||||
if err := h.ensureNamespace(r.Context(), ns); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("ensure namespace: "+err.Error()))
|
||||
return
|
||||
}
|
||||
var req functionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error()))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ func jobToResponse(j *slessv1alpha1.FunctionJob) jobResponse {
|
||||
// Создаёт FunctionJob CR. Оператор запустит k8s Job асинхронно.
|
||||
func (h *Handler) CreateJob(w http.ResponseWriter, r *http.Request) {
|
||||
ns := namespace(r)
|
||||
// Гарантируем существование namespace — аналогично CreateFunction.
|
||||
if err := h.ensureNamespace(r.Context(), ns); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("ensure namespace: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var req jobRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
|
||||
@@ -74,6 +74,11 @@ func (h *Handler) ListTriggers(w http.ResponseWriter, r *http.Request) {
|
||||
// CreateTrigger — POST /v1/namespaces/{namespace}/triggers
|
||||
func (h *Handler) CreateTrigger(w http.ResponseWriter, r *http.Request) {
|
||||
ns := namespace(r)
|
||||
// Гарантируем существование namespace — аналогично CreateFunction.
|
||||
if err := h.ensureNamespace(r.Context(), ns); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp("ensure namespace: "+err.Error()))
|
||||
return
|
||||
}
|
||||
var req triggerRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error()))
|
||||
|
||||
Reference in New Issue
Block a user