- handler.go: убраны бизнес-логика и k8s-типы (corev1/k8serrors/metav1)
handler.go теперь только инфраструктура: Handler struct + helpers
- namespace.go: новый файл — EnsureNamespace хендлер живёт здесь
SoC: создание namespace — отдельная ответственность, не смешивается с CRUD
- router.go: добавлен маршрут POST /v1/namespaces/{namespace}/ensure
- client.go: добавлен метод EnsureNamespace(ctx, ns) → POST /ensure
- provider.go: Configure() вызывает c.EnsureNamespace(ctx, namespace) после создания Client
Namespace создаётся ОДИН РАЗ при инициализации провайдера
Resource-хендлеры (Function, Trigger, Job) namespace не трогают
- .gitignore: добавлена директория secrets/ (токены, ключи)
- provider v0.1.13, operator v0.1.21
Operator: naeel/sless-operator:v0.1.21
Provider: terra.k8c.ru/naeel/sless v0.1.13
178 lines
6.7 KiB
Go
178 lines
6.7 KiB
Go
// 2026-03-11
|
||
// provider.go — описание провайдера sless для Terraform.
|
||
//
|
||
// Архитектура инициализации (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 (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
|
||
"terraform-provider-sless/internal/client"
|
||
"terraform-provider-sless/internal/resources"
|
||
|
||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||
)
|
||
|
||
var _ provider.Provider = &SlessProvider{}
|
||
|
||
type SlessProvider struct {
|
||
version string
|
||
}
|
||
|
||
// SlessProviderModel — конфигурация блока provider {} в .tf файле.
|
||
type SlessProviderModel struct {
|
||
Endpoint types.String `tfsdk:"endpoint"`
|
||
Token types.String `tfsdk:"token"`
|
||
NubesEndpoint types.String `tfsdk:"nubes_endpoint"`
|
||
}
|
||
|
||
// New возвращает фабрику провайдера — точная копия паттерна nubes.
|
||
func New(version string) func() provider.Provider {
|
||
return func() provider.Provider {
|
||
return &SlessProvider{version: version}
|
||
}
|
||
}
|
||
|
||
func (p *SlessProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||
resp.TypeName = "sless"
|
||
resp.Version = p.version
|
||
}
|
||
|
||
func (p *SlessProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||
resp.Schema = schema.Schema{
|
||
Attributes: map[string]schema.Attribute{
|
||
"endpoint": schema.StringAttribute{
|
||
MarkdownDescription: "sless operator API endpoint, например https://sless-api.kube5s.ru",
|
||
Optional: true,
|
||
},
|
||
"token": schema.StringAttribute{
|
||
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)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
|
||
// --- 1. Читаем endpoint sless оператора ---
|
||
endpoint := "http://localhost:9090"
|
||
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())
|
||
}
|
||
if token == "" {
|
||
if v := os.Getenv("SLESS_API_TOKEN"); v != "" {
|
||
token = strings.TrimSpace(v)
|
||
}
|
||
}
|
||
|
||
// --- 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)
|
||
|
||
// --- 6. Создаём k8s namespace пользователя (идемпотентно) ---
|
||
// Namespace создаётся ОДИН РАЗ здесь — resource-хендлеры (Function, Trigger, Job)
|
||
// не занимаются созданием namespace; это не их ответственность.
|
||
// Если endpoint недоступен в dev-режиме без токена — пропускаем.
|
||
if token != "" {
|
||
if err := c.EnsureNamespace(ctx, namespace); err != nil {
|
||
resp.Diagnostics.AddError(
|
||
"Failed to ensure namespace",
|
||
fmt.Sprintf("Cannot create namespace %q: %s", namespace, err.Error()),
|
||
)
|
||
return
|
||
}
|
||
}
|
||
|
||
resp.ResourceData = c
|
||
resp.DataSourceData = c
|
||
}
|
||
|
||
func (p *SlessProvider) Resources(_ context.Context) []func() resource.Resource {
|
||
return []func() resource.Resource{
|
||
resources.NewFunctionResource,
|
||
resources.NewTriggerResource,
|
||
resources.NewJobResource,
|
||
}
|
||
}
|
||
|
||
func (p *SlessProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
||
return nil
|
||
}
|