// 2026-03-07 // trigger_resource.go — Terraform ресурс sless_trigger. // Поддерживает type=http (создаёт Service+Ingress) и type=cron (запускает по schedule). // Все ключевые поля имеют RequiresReplace — API не поддерживает обновление триггеров. // URL функции (для http-триггера) — computed, пишется оператором в status.url. package resources import ( "context" "fmt" "terraform-provider-sless/internal/client" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "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/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"` 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(), }, }, // 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(), }, }, // 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(), }) 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 — никогда не вызывается: все поля имеют RequiresReplace. // Метод обязателен интерфейсом resource.Resource. func (r *TriggerResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { resp.Diagnostics.AddError("update not supported", "all trigger fields require replacement") } 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, Active: types.BoolValue(tr.Active), URL: types.StringValue(tr.URL), } }