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,8 +1,20 @@
|
||||
// 2026-03-07
|
||||
// 2026-03-11
|
||||
// provider.go — описание провайдера sless для Terraform.
|
||||
// Паттерн идентичен nubes провайдеру (NubesProvider) — для облегчения переноса.
|
||||
// Атрибуты: endpoint (URL оператора) + token (Bearer).
|
||||
// Env-переменные: SLESS_ENDPOINT, SLESS_API_TOKEN.
|
||||
//
|
||||
// Архитектура инициализации (Configure):
|
||||
// 1. Читаем token (JWT облака) — из конфига или env SLESS_API_TOKEN.
|
||||
// 2. Декодируем JWT → извлекаем sub (уникальный ID пользователя в облаке).
|
||||
// 3. Вычисляем namespace = SHA256(sub) → "sless-{hex}" (см. client.NamespaceFromSub).
|
||||
// 4. Если задан nubes_endpoint — пингуем nubes API для валидации токена.
|
||||
// 401/403 → ошибка инициализации; connection error → ошибка инициализации.
|
||||
// 5. Создаём client.Client с вычисленным namespace.
|
||||
//
|
||||
// Атрибуты провайдера:
|
||||
// endpoint — URL sless оператора (env: SLESS_ENDPOINT)
|
||||
// token — JWT токен облака, общий для nubes и sless (env: SLESS_API_TOKEN)
|
||||
// nubes_endpoint — URL nubes API для валидации токена (env: NUBES_ENDPOINT, опционально)
|
||||
//
|
||||
// Env-переменные: SLESS_ENDPOINT, SLESS_API_TOKEN, NUBES_ENDPOINT.
|
||||
package provider
|
||||
|
||||
import (
|
||||
@@ -28,8 +40,9 @@ type SlessProvider struct {
|
||||
|
||||
// SlessProviderModel — конфигурация блока provider {} в .tf файле.
|
||||
type SlessProviderModel struct {
|
||||
Endpoint types.String `tfsdk:"endpoint"`
|
||||
Token types.String `tfsdk:"token"`
|
||||
Endpoint types.String `tfsdk:"endpoint"`
|
||||
Token types.String `tfsdk:"token"`
|
||||
NubesEndpoint types.String `tfsdk:"nubes_endpoint"`
|
||||
}
|
||||
|
||||
// New возвращает фабрику провайдера — точная копия паттерна nubes.
|
||||
@@ -48,20 +61,25 @@ func (p *SlessProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp
|
||||
resp.Schema = schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"endpoint": schema.StringAttribute{
|
||||
MarkdownDescription: "sless operator API endpoint, например http://sless-operator.sless.svc:9090",
|
||||
MarkdownDescription: "sless operator API endpoint, например https://sless-api.kube5s.ru",
|
||||
Optional: true,
|
||||
},
|
||||
"token": schema.StringAttribute{
|
||||
MarkdownDescription: "Bearer-токен аутентификации (env: SLESS_API_TOKEN)",
|
||||
MarkdownDescription: "JWT Bearer-токен облака (env: SLESS_API_TOKEN). Тот же токен что в provider \"nubes\".",
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
"nubes_endpoint": schema.StringAttribute{
|
||||
MarkdownDescription: "URL nubes API для валидации токена (env: NUBES_ENDPOINT). Опционально. Пример: https://deck-api.ngcloud.ru/api/v1",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Configure инициализирует HTTP-клиент и кладёт его в ResourceData/DataSourceData.
|
||||
// Ресурсы получают клиент через Configure(req.ProviderData).
|
||||
// Порядок инициализации описан в комментарии к файлу (см. начало provider.go).
|
||||
func (p *SlessProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||
var config SlessProviderModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
@@ -69,15 +87,16 @@ func (p *SlessProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
||||
return
|
||||
}
|
||||
|
||||
// --- 1. Читаем endpoint sless оператора ---
|
||||
endpoint := "http://localhost:9090"
|
||||
token := ""
|
||||
|
||||
if !config.Endpoint.IsNull() && config.Endpoint.ValueString() != "" {
|
||||
endpoint = config.Endpoint.ValueString()
|
||||
} else if v := os.Getenv("SLESS_ENDPOINT"); v != "" {
|
||||
endpoint = v
|
||||
}
|
||||
|
||||
// --- 2. Читаем JWT токен ---
|
||||
token := ""
|
||||
if !config.Token.IsNull() {
|
||||
token = strings.TrimSpace(config.Token.ValueString())
|
||||
}
|
||||
@@ -87,7 +106,43 @@ func (p *SlessProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
||||
}
|
||||
}
|
||||
|
||||
c := client.New(endpoint, token)
|
||||
// --- 3. Декодируем JWT → sub → namespace ---
|
||||
// Если токен не задан (dev-режим без токена) — используем fallback namespace.
|
||||
// В production токен обязателен: без него нельзя определить namespace пользователя.
|
||||
namespace := "sless-dev"
|
||||
if token != "" {
|
||||
sub, err := client.SubFromJWT(token)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"Invalid token",
|
||||
"Cannot decode JWT token: "+err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
namespace = client.NamespaceFromSub(sub)
|
||||
}
|
||||
|
||||
// --- 4. Пингуем nubes API для валидации токена (если задан nubes_endpoint) ---
|
||||
// Если nubes_endpoint не задан — пропускаем проверку (dev-режим).
|
||||
nubesEndpoint := ""
|
||||
if !config.NubesEndpoint.IsNull() && config.NubesEndpoint.ValueString() != "" {
|
||||
nubesEndpoint = config.NubesEndpoint.ValueString()
|
||||
} else if v := os.Getenv("NUBES_ENDPOINT"); v != "" {
|
||||
nubesEndpoint = v
|
||||
}
|
||||
|
||||
if nubesEndpoint != "" && token != "" {
|
||||
if err := client.PingNubesAPI(ctx, nubesEndpoint, token); err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
"nubes API validation failed",
|
||||
err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5. Создаём клиент с вычисленным namespace ---
|
||||
c := client.New(endpoint, token, namespace)
|
||||
resp.ResourceData = c
|
||||
resp.DataSourceData = c
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user