Files
sless/terraform/provider/internal/provider/provider.go
T

180 lines
6.9 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-20 (function-service-split: добавлен NewServiceResource)
// 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.NewServiceResource,
resources.NewTriggerResource,
resources.NewJobResource,
resources.NewIoTDeviceResource,
}
}
func (p *SlessProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return nil
}