Files
sless/terraform/provider/internal/provider/provider.go
T
“Naeel” a1774e178f refactor: SoC — EnsureNamespace в namespace.go, маршрут /ensure, client.EnsureNamespace, fix secrets в .gitignore
- 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
2026-03-11 08:37:33 +04:00

178 lines
6.7 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
// 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
}