Files
sless/terraform/provider/internal/resources/trigger_resource.go
T
“Naeel” 976fcadc36 fix: API validation + Terraform plan-time validators
API (operator v0.1.13):
- functions.go: добавлена валидация entrypoint (не пустой) и
  memory_mb (1-4096). Фиксирует БАГ-1/2/4 из негативных тестов.
- triggers.go: добавлена валидация type (только 'http'/'cron').
  Фиксирует БАГ-3 (неверное сообщение об ошибке).

Провайдер (v0.1.7):
- Добавлен пакет terraform-plugin-framework-validators v0.19.0
- function_resource: runtime OneOf, memory_mb 1-4096, timeout_sec 1-900
- trigger_resource: type OneOf(http, cron)
- job_resource: run_id AtLeast(0)
- examples/main.tf: обновлена версия до ~> 0.1.7

doc/errors/log.md: задокументированы исправления и результаты повторных тестов
2026-03-09 08:52:13 +04:00

231 lines
8.0 KiB
Go

// 2026-03-08
// trigger_resource.go — Terraform ресурс sless_trigger.
// Поддерживает type=http (создаёт Service+Ingress) и type=cron (запускает по schedule).
// enabled=false: масштабирует Deployment функции до 0 (не принимает запросы, не потребляет ресурсы).
// enabled не требует RequiresReplace — поддерживает in-place обновление через PATCH.
package resources
import (
"context"
"fmt"
"terraform-provider-sless/internal/client"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &TriggerResource{}
type TriggerResource struct {
client *client.Client
}
func NewTriggerResource() resource.Resource {
return &TriggerResource{}
}
// TriggerModel — модель состояния terraform для sless_trigger.
type TriggerModel struct {
Namespace types.String `tfsdk:"namespace"`
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
FunctionRef types.String `tfsdk:"function"`
Schedule types.String `tfsdk:"schedule"`
// Enabled — false = Deployment масштабируется до 0, функция не потребляет ресурсы.
// Не требует пересоздания - изменяется in-place через PATCH.
Enabled types.Bool `tfsdk:"enabled"`
Active types.Bool `tfsdk:"active"`
URL types.String `tfsdk:"url"`
}
func (r *TriggerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_trigger"
}
func (r *TriggerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"namespace": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
// type: "http" или "cron"
"type": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.OneOf("http", "cron"),
},
},
// function: имя Function CRD в том же namespace
"function": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
// schedule: только для type=cron, формат cron ("*/5 * * * *")
"schedule": schema.StringAttribute{
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
// enabled: false → Deployment масштабируется до 0 (не удаляет ресурс).
// Не имеет RequiresReplace: применяется in-place через PATCH.
"enabled": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
// active, url — только для чтения, вычисляются оператором
"active": schema.BoolAttribute{
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"url": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *TriggerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"unexpected provider data",
fmt.Sprintf("expected *client.Client, got: %T", req.ProviderData),
)
return
}
r.client = c
}
func (r *TriggerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan TriggerModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
tr, err := r.client.CreateTrigger(ctx, plan.Namespace.ValueString(), client.TriggerRequest{
Name: plan.Name.ValueString(),
Type: plan.Type.ValueString(),
FunctionRef: plan.FunctionRef.ValueString(),
Schedule: plan.Schedule.ValueString(),
Enabled: boolPtr(plan.Enabled.ValueBool()),
})
if err != nil {
resp.Diagnostics.AddError("create trigger", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, trToModel(tr))...)
}
func (r *TriggerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state TriggerModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
tr, err := r.client.GetTrigger(ctx, state.Namespace.ValueString(), state.Name.ValueString())
if err != nil {
resp.Diagnostics.AddError("read trigger", err.Error())
return
}
if tr == nil {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, trToModel(tr))...)
}
// Update — обновляет enabled через PATCH без пересоздания.
// единственное поле без RequiresReplace, поэтому Update срабатывает только если enabled изменился.
func (r *TriggerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state TriggerModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
enabled := plan.Enabled.ValueBool()
tr, err := r.client.UpdateTrigger(ctx, plan.Namespace.ValueString(), plan.Name.ValueString(), client.TriggerUpdateRequest{
Enabled: &enabled,
})
if err != nil {
resp.Diagnostics.AddError("update trigger", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, trToModel(tr))...)
}
func (r *TriggerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state TriggerModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.DeleteTrigger(ctx, state.Namespace.ValueString(), state.Name.ValueString()); err != nil {
resp.Diagnostics.AddError("delete trigger", err.Error())
}
}
// trToModel конвертирует API-ответ → state модель.
// schedule: возвращаем Null если пустая строка — terraform требует null для Optional-полей,
// которые не заданы в конфиге (иначе план: null → apply: "" → "inconsistent result").
func trToModel(tr *client.TriggerResponse) TriggerModel {
schedule := types.StringNull()
if tr.Schedule != "" {
schedule = types.StringValue(tr.Schedule)
}
return TriggerModel{
Namespace: types.StringValue(tr.Namespace),
Name: types.StringValue(tr.Name),
Type: types.StringValue(tr.Type),
FunctionRef: types.StringValue(tr.FunctionRef),
Schedule: schedule,
Enabled: types.BoolValue(tr.Enabled),
Active: types.BoolValue(tr.Active),
URL: types.StringValue(tr.URL),
}
}
// boolPtr — хелпер для получения указателя на bool (API требует *bool).
func boolPtr(v bool) *bool {
return &v
}