240 lines
8.3 KiB
Go
240 lines
8.3 KiB
Go
// 2026-03-08, обновлено 2025-06-05
|
|
// 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"
|
|
"time"
|
|
|
|
"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 {
|
|
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{
|
|
"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, r.client.Namespace, 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
|
|
}
|
|
|
|
// Контроллер заполняет status.URL асинхронно — ждём до 30 секунд.
|
|
// Без этого URL остаётся пустым в state и требует ручного terraform refresh.
|
|
if tr.URL == "" && plan.Type.ValueString() == "http" {
|
|
ns := r.client.Namespace
|
|
name := plan.Name.ValueString()
|
|
deadline := time.Now().Add(30 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
time.Sleep(2 * time.Second)
|
|
got, gerr := r.client.GetTrigger(ctx, ns, name)
|
|
if gerr == nil && got != nil && got.URL != "" {
|
|
tr = got
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
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, r.client.Namespace, 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, r.client.Namespace, 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, r.client.Namespace, 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{
|
|
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
|
|
}
|