From cf55828ba6bfa547826f6483a476ee50b899f806 Mon Sep 17 00:00:00 2001 From: Naeel Date: Tue, 14 Apr 2026 22:40:11 +0300 Subject: [PATCH] feat: implement function and http trigger resources via CRD --- doc/progress.md | 30 ++ doc/thinking/2026-04-14.md | 40 +++ terraform/provider/internal/client/client.go | 92 ++++++ .../internal/resources/function_resource.go | 214 +++++++++++++- .../resources/http_trigger_resource.go | 262 +++++++++++++++++- 5 files changed, 624 insertions(+), 14 deletions(-) diff --git a/doc/progress.md b/doc/progress.md index 6599e40..cbb6ef3 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -38,3 +38,33 @@ ### Следующий шаг - Реализовать `fission_function` через CRD `functions.fission.io`. + +### Обновление этапа (fission_function) +- Реализован CRUD в `internal/client/client.go` для `functions.fission.io`: + - `CreateFunction` + - `GetFunction` + - `UpdateFunction` + - `DeleteFunction` +- Реализован ресурс `fission_function` в `internal/resources/function_resource.go`: + - schema: `name`, `environment`, `package_name`, `entrypoint`, `namespace`, computed `id`, `uid` + - Configure/Create/Read/Update/Delete + - маппинг в CRD `Function` (`spec.environment`, `spec.package.packageref`, `spec.package.functionName`) +- Проверка сборки: `go build ./...` успешно. + +### Следующий шаг +- Реализовать `fission_http_trigger` через CRD `httptriggers.fission.io`. + +### Обновление этапа (fission_http_trigger) +- Реализован CRUD в `internal/client/client.go` для `httptriggers.fission.io`: + - `CreateHTTPTrigger` + - `GetHTTPTrigger` + - `UpdateHTTPTrigger` + - `DeleteHTTPTrigger` +- Реализован ресурс `fission_http_trigger` в `internal/resources/http_trigger_resource.go`: + - schema: `name`, `function`, `url`, `methods`, `create_ingress`, `host`, `namespace`, computed `id`, `uid` + - Configure/Create/Read/Update/Delete + - маппинг в CRD `HTTPTrigger` (`spec.relativeurl`, `spec.methods`, `spec.functionref`, `spec.createingress`) +- Проверка сборки: `go build ./...` успешно. + +### Следующий шаг +- Добавить пример `examples/hello-python/main.tf` и выполнить smoke Terraform apply. diff --git a/doc/thinking/2026-04-14.md b/doc/thinking/2026-04-14.md index 7295e49..a29a96c 100644 --- a/doc/thinking/2026-04-14.md +++ b/doc/thinking/2026-04-14.md @@ -60,3 +60,43 @@ - Create/Read/Update/Delete через CRD API - Сборка `go build ./...` успешна. - Следующий этап: `fission_function`. + +--- + +Агент: GitHub Copilot +Модель: GPT-5.3-Codex + +## План (до действий, этап 4) +1. Реализовать CRUD методы клиента для `functions.fission.io`. +2. Реализовать ресурс `fission_function`. +3. Проверить сборку. +4. Обновить документацию этапа. + +## Результат (после действий, этап 4) +- В `internal/client/client.go` добавлены методы CRUD для Function CRD. +- В `internal/resources/function_resource.go` реализован рабочий ресурс `fission_function`: + - schema с обязательными `name`, `environment`, `package_name`, `entrypoint` + - Configure/Create/Read/Update/Delete + - преобразование terraform model <-> CRD payload +- Сборка `go build ./...` успешна. +- Следующий этап: `fission_http_trigger`. + +--- + +Агент: GitHub Copilot +Модель: GPT-5.3-Codex + +## План (до действий, этап 5) +1. Реализовать CRUD методы клиента для `httptriggers.fission.io`. +2. Реализовать ресурс `fission_http_trigger`. +3. Проверить сборку. +4. Обновить документацию этапа. + +## Результат (после действий, этап 5) +- В `internal/client/client.go` добавлены методы CRUD для HTTPTrigger CRD. +- В `internal/resources/http_trigger_resource.go` реализован рабочий ресурс `fission_http_trigger`: + - schema с `name`, `function`, `url`, `methods`, `create_ingress`, `host` + - Configure/Create/Read/Update/Delete + - преобразование terraform model <-> CRD payload +- Сборка `go build ./...` успешна. +- Следующий этап: пример Terraform и smoke apply. diff --git a/terraform/provider/internal/client/client.go b/terraform/provider/internal/client/client.go index bd30f3d..cdd4e8b 100644 --- a/terraform/provider/internal/client/client.go +++ b/terraform/provider/internal/client/client.go @@ -26,6 +26,18 @@ var packageGVR = schema.GroupVersionResource{ Resource: "packages", } +var functionGVR = schema.GroupVersionResource{ + Group: "fission.io", + Version: "v1", + Resource: "functions", +} + +var httpTriggerGVR = schema.GroupVersionResource{ + Group: "fission.io", + Version: "v1", + Resource: "httptriggers", +} + // Client хранит клиентов Kubernetes API для работы с CRD Fission. type Client struct { DynClient dynamic.Interface @@ -161,3 +173,83 @@ func (c *Client) DeletePackage(ctx context.Context, namespace, name string) erro return nil } + +// CreateFunction создает объект Function в заданном namespace. +func (c *Client) CreateFunction(ctx context.Context, fn *unstructured.Unstructured) (*unstructured.Unstructured, error) { + createdFunction, err := c.DynClient.Resource(functionGVR).Namespace(fn.GetNamespace()).Create(ctx, fn, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("create function %q: %w", fn.GetName(), err) + } + + return createdFunction, nil +} + +// GetFunction получает объект Function по имени. +func (c *Client) GetFunction(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) { + fn, err := c.DynClient.Resource(functionGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("get function %q: %w", name, err) + } + + return fn, nil +} + +// UpdateFunction обновляет существующий объект Function. +func (c *Client) UpdateFunction(ctx context.Context, fn *unstructured.Unstructured) (*unstructured.Unstructured, error) { + updatedFunction, err := c.DynClient.Resource(functionGVR).Namespace(fn.GetNamespace()).Update(ctx, fn, metav1.UpdateOptions{}) + if err != nil { + return nil, fmt.Errorf("update function %q: %w", fn.GetName(), err) + } + + return updatedFunction, nil +} + +// DeleteFunction удаляет объект Function по имени. +func (c *Client) DeleteFunction(ctx context.Context, namespace, name string) error { + err := c.DynClient.Resource(functionGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("delete function %q: %w", name, err) + } + + return nil +} + +// CreateHTTPTrigger создает объект HTTPTrigger в заданном namespace. +func (c *Client) CreateHTTPTrigger(ctx context.Context, trigger *unstructured.Unstructured) (*unstructured.Unstructured, error) { + createdTrigger, err := c.DynClient.Resource(httpTriggerGVR).Namespace(trigger.GetNamespace()).Create(ctx, trigger, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("create http trigger %q: %w", trigger.GetName(), err) + } + + return createdTrigger, nil +} + +// GetHTTPTrigger получает объект HTTPTrigger по имени. +func (c *Client) GetHTTPTrigger(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) { + trigger, err := c.DynClient.Resource(httpTriggerGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("get http trigger %q: %w", name, err) + } + + return trigger, nil +} + +// UpdateHTTPTrigger обновляет существующий объект HTTPTrigger. +func (c *Client) UpdateHTTPTrigger(ctx context.Context, trigger *unstructured.Unstructured) (*unstructured.Unstructured, error) { + updatedTrigger, err := c.DynClient.Resource(httpTriggerGVR).Namespace(trigger.GetNamespace()).Update(ctx, trigger, metav1.UpdateOptions{}) + if err != nil { + return nil, fmt.Errorf("update http trigger %q: %w", trigger.GetName(), err) + } + + return updatedTrigger, nil +} + +// DeleteHTTPTrigger удаляет объект HTTPTrigger по имени. +func (c *Client) DeleteHTTPTrigger(ctx context.Context, namespace, name string) error { + err := c.DynClient.Resource(httpTriggerGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("delete http trigger %q: %w", name, err) + } + + return nil +} diff --git a/terraform/provider/internal/resources/function_resource.go b/terraform/provider/internal/resources/function_resource.go index fbc56b5..a99518a 100644 --- a/terraform/provider/internal/resources/function_resource.go +++ b/terraform/provider/internal/resources/function_resource.go @@ -2,34 +2,234 @@ package resources import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "terraform-provider-fission/internal/client" ) var _ resource.Resource = &FunctionResource{} -type FunctionResource struct{} +// Изменено: 2026-04-14 20:00 UTC. +// Resource для управления Fission Function через Kubernetes CRD API. +type FunctionResource struct { + client *client.Client +} +// functionResourceModel описывает состояние terraform ресурса fission_function. +type functionResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Environment types.String `tfsdk:"environment"` + PackageName types.String `tfsdk:"package_name"` + Entrypoint types.String `tfsdk:"entrypoint"` + Namespace types.String `tfsdk:"namespace"` + UID types.String `tfsdk:"uid"` +} + +// NewFunctionResource создает инстанс ресурса функции. func NewFunctionResource() resource.Resource { return &FunctionResource{} } +// Metadata задает имя ресурса fission_function. func (r *FunctionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_function" } +// Schema задает Terraform schema для Function. func (r *FunctionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{} + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Идентификатор ресурса (namespace/name).", + }, + "name": schema.StringAttribute{ + Required: true, + Description: "Имя Fission Function.", + }, + "environment": schema.StringAttribute{ + Required: true, + Description: "Имя Environment, в котором выполняется функция.", + }, + "package_name": schema.StringAttribute{ + Required: true, + Description: "Имя Fission Package с кодом функции.", + }, + "entrypoint": schema.StringAttribute{ + Required: true, + Description: "Имя точки входа в пакете (например main.main).", + }, + "namespace": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Namespace, где создается Function. По умолчанию используется provider namespace.", + }, + "uid": schema.StringAttribute{ + Computed: true, + Description: "UID Kubernetes объекта Function.", + }, + }, + } } -func (r *FunctionResource) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { +// Configure получает клиент из provider.Configure(). +func (r *FunctionResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + fissionClient, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Некорректный тип provider data", + fmt.Sprintf("Ожидался *client.Client, получен: %T", req.ProviderData), + ) + return + } + + r.client = fissionClient } -func (r *FunctionResource) Create(_ context.Context, _ resource.CreateRequest, _ *resource.CreateResponse) { + +// Create создает новый Function CRD объект. +func (r *FunctionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan functionResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(plan.Namespace, r.client.Namespace) + functionObject := functionToUnstructured(plan, namespace) + + createdFunction, err := r.client.CreateFunction(ctx, functionObject) + if err != nil { + resp.Diagnostics.AddError("Ошибка создания Fission Function", err.Error()) + return + } + + state := unstructuredToFunctionModel(createdFunction, plan) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -func (r *FunctionResource) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { + +// Read синхронизирует Terraform state с текущим состоянием Function CRD. +func (r *FunctionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state functionResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(state.Namespace, r.client.Namespace) + functionObject, err := r.client.GetFunction(ctx, namespace, state.Name.ValueString()) + if err != nil { + if client.IsNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.AddError("Ошибка чтения Fission Function", err.Error()) + return + } + + updatedState := unstructuredToFunctionModel(functionObject, state) + resp.Diagnostics.Append(resp.State.Set(ctx, &updatedState)...) } -func (r *FunctionResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { + +// Update обновляет существующий Function CRD объект. +func (r *FunctionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan functionResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(plan.Namespace, r.client.Namespace) + existingFunction, err := r.client.GetFunction(ctx, namespace, plan.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка получения Function перед обновлением", err.Error()) + return + } + + functionObject := functionToUnstructured(plan, namespace) + functionObject.SetResourceVersion(existingFunction.GetResourceVersion()) + + updatedFunction, err := r.client.UpdateFunction(ctx, functionObject) + if err != nil { + resp.Diagnostics.AddError("Ошибка обновления Fission Function", err.Error()) + return + } + + state := unstructuredToFunctionModel(updatedFunction, plan) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -func (r *FunctionResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) { + +// Delete удаляет Function CRD объект. +func (r *FunctionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state functionResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(state.Namespace, r.client.Namespace) + err := r.client.DeleteFunction(ctx, namespace, state.Name.ValueString()) + if err != nil && !client.IsNotFound(err) { + resp.Diagnostics.AddError("Ошибка удаления Fission Function", err.Error()) + } +} + +// functionToUnstructured преобразует Terraform model в Kubernetes CRD payload. +func functionToUnstructured(model functionResourceModel, namespace string) *unstructured.Unstructured { + return &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "fission.io/v1", + "kind": "Function", + "metadata": map[string]interface{}{ + "name": model.Name.ValueString(), + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "environment": map[string]interface{}{ + "name": model.Environment.ValueString(), + "namespace": namespace, + }, + "package": map[string]interface{}{ + "packageref": map[string]interface{}{ + "name": model.PackageName.ValueString(), + "namespace": namespace, + }, + "functionName": model.Entrypoint.ValueString(), + }, + }, + }} +} + +// unstructuredToFunctionModel читает нужные поля из CRD объекта обратно в Terraform state. +func unstructuredToFunctionModel(functionObject *unstructured.Unstructured, base functionResourceModel) functionResourceModel { + environmentName, _, _ := unstructured.NestedString(functionObject.Object, "spec", "environment", "name") + packageName, _, _ := unstructured.NestedString(functionObject.Object, "spec", "package", "packageref", "name") + entrypoint, _, _ := unstructured.NestedString(functionObject.Object, "spec", "package", "functionName") + + state := base + state.Name = types.StringValue(functionObject.GetName()) + state.Namespace = types.StringValue(functionObject.GetNamespace()) + state.ID = types.StringValue(fmt.Sprintf("%s/%s", functionObject.GetNamespace(), functionObject.GetName())) + state.UID = types.StringValue(string(functionObject.GetUID())) + + if environmentName != "" { + state.Environment = types.StringValue(environmentName) + } + if packageName != "" { + state.PackageName = types.StringValue(packageName) + } + if entrypoint != "" { + state.Entrypoint = types.StringValue(entrypoint) + } + + return state } diff --git a/terraform/provider/internal/resources/http_trigger_resource.go b/terraform/provider/internal/resources/http_trigger_resource.go index 5a9fca4..3f39fd1 100644 --- a/terraform/provider/internal/resources/http_trigger_resource.go +++ b/terraform/provider/internal/resources/http_trigger_resource.go @@ -2,34 +2,282 @@ package resources import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "terraform-provider-fission/internal/client" ) var _ resource.Resource = &HTTPTriggerResource{} -type HTTPTriggerResource struct{} +// Изменено: 2026-04-14 20:15 UTC. +// Resource для управления Fission HTTPTrigger через Kubernetes CRD API. +type HTTPTriggerResource struct { + client *client.Client +} +// httpTriggerResourceModel описывает состояние terraform ресурса fission_http_trigger. +type httpTriggerResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Function types.String `tfsdk:"function"` + URL types.String `tfsdk:"url"` + Methods types.List `tfsdk:"methods"` + CreateIngress types.Bool `tfsdk:"create_ingress"` + Host types.String `tfsdk:"host"` + Namespace types.String `tfsdk:"namespace"` + UID types.String `tfsdk:"uid"` +} + +// NewHTTPTriggerResource создает инстанс ресурса HTTP trigger. func NewHTTPTriggerResource() resource.Resource { return &HTTPTriggerResource{} } +// Metadata задает имя ресурса fission_http_trigger. func (r *HTTPTriggerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_http_trigger" } +// Schema задает Terraform schema для HTTPTrigger. func (r *HTTPTriggerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{} + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Идентификатор ресурса (namespace/name).", + }, + "name": schema.StringAttribute{ + Required: true, + Description: "Имя Fission HTTPTrigger.", + }, + "function": schema.StringAttribute{ + Required: true, + Description: "Имя Fission Function, к которой привязан trigger.", + }, + "url": schema.StringAttribute{ + Required: true, + Description: "Относительный URL, например /hello.", + }, + "methods": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + Description: "HTTP методы trigger, например [\"GET\", \"POST\"].", + }, + "create_ingress": schema.BoolAttribute{ + Optional: true, + Description: "Включает создание ingress правила в Fission.", + }, + "host": schema.StringAttribute{ + Optional: true, + Description: "Host для ingressconfig.host при create_ingress=true.", + }, + "namespace": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Namespace, где создается HTTPTrigger. По умолчанию используется provider namespace.", + }, + "uid": schema.StringAttribute{ + Computed: true, + Description: "UID Kubernetes объекта HTTPTrigger.", + }, + }, + } } -func (r *HTTPTriggerResource) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { +// Configure получает клиент из provider.Configure(). +func (r *HTTPTriggerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + fissionClient, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Некорректный тип provider data", + fmt.Sprintf("Ожидался *client.Client, получен: %T", req.ProviderData), + ) + return + } + + r.client = fissionClient } -func (r *HTTPTriggerResource) Create(_ context.Context, _ resource.CreateRequest, _ *resource.CreateResponse) { + +// Create создает новый HTTPTrigger CRD объект. +func (r *HTTPTriggerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan httpTriggerResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(plan.Namespace, r.client.Namespace) + triggerObject, err := httpTriggerToUnstructured(ctx, plan, namespace) + if err != nil { + resp.Diagnostics.AddError("Ошибка подготовки HTTPTrigger", err.Error()) + return + } + + createdTrigger, err := r.client.CreateHTTPTrigger(ctx, triggerObject) + if err != nil { + resp.Diagnostics.AddError("Ошибка создания Fission HTTPTrigger", err.Error()) + return + } + + state := unstructuredToHTTPTriggerModel(ctx, createdTrigger, plan) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -func (r *HTTPTriggerResource) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { + +// Read синхронизирует Terraform state с текущим состоянием HTTPTrigger CRD. +func (r *HTTPTriggerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state httpTriggerResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(state.Namespace, r.client.Namespace) + triggerObject, err := r.client.GetHTTPTrigger(ctx, namespace, state.Name.ValueString()) + if err != nil { + if client.IsNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.AddError("Ошибка чтения Fission HTTPTrigger", err.Error()) + return + } + + updatedState := unstructuredToHTTPTriggerModel(ctx, triggerObject, state) + resp.Diagnostics.Append(resp.State.Set(ctx, &updatedState)...) } -func (r *HTTPTriggerResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { + +// Update обновляет существующий HTTPTrigger CRD объект. +func (r *HTTPTriggerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan httpTriggerResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(plan.Namespace, r.client.Namespace) + existingTrigger, err := r.client.GetHTTPTrigger(ctx, namespace, plan.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка получения HTTPTrigger перед обновлением", err.Error()) + return + } + + triggerObject, err := httpTriggerToUnstructured(ctx, plan, namespace) + if err != nil { + resp.Diagnostics.AddError("Ошибка подготовки HTTPTrigger", err.Error()) + return + } + triggerObject.SetResourceVersion(existingTrigger.GetResourceVersion()) + + updatedTrigger, err := r.client.UpdateHTTPTrigger(ctx, triggerObject) + if err != nil { + resp.Diagnostics.AddError("Ошибка обновления Fission HTTPTrigger", err.Error()) + return + } + + state := unstructuredToHTTPTriggerModel(ctx, updatedTrigger, plan) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -func (r *HTTPTriggerResource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) { + +// Delete удаляет HTTPTrigger CRD объект. +func (r *HTTPTriggerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state httpTriggerResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + namespace := resolveNamespace(state.Namespace, r.client.Namespace) + err := r.client.DeleteHTTPTrigger(ctx, namespace, state.Name.ValueString()) + if err != nil && !client.IsNotFound(err) { + resp.Diagnostics.AddError("Ошибка удаления Fission HTTPTrigger", err.Error()) + } +} + +// httpTriggerToUnstructured преобразует Terraform model в Kubernetes CRD payload. +func httpTriggerToUnstructured(ctx context.Context, model httpTriggerResourceModel, namespace string) (*unstructured.Unstructured, error) { + methods := []string{"GET"} + if !model.Methods.IsNull() && !model.Methods.IsUnknown() { + if len(model.Methods.Elements()) > 0 { + methods = []string{} + var methodValues []string + diags := model.Methods.ElementsAs(ctx, &methodValues, false) + if diags.HasError() { + return nil, fmt.Errorf("convert methods list") + } + methods = append(methods, methodValues...) + } + } + + createIngress := false + if !model.CreateIngress.IsNull() && !model.CreateIngress.IsUnknown() { + createIngress = model.CreateIngress.ValueBool() + } + + object := map[string]interface{}{ + "apiVersion": "fission.io/v1", + "kind": "HTTPTrigger", + "metadata": map[string]interface{}{ + "name": model.Name.ValueString(), + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "relativeurl": model.URL.ValueString(), + "methods": methods, + "functionref": map[string]interface{}{ + "type": "name", + "name": model.Function.ValueString(), + }, + "createingress": createIngress, + }, + } + + host := model.Host.ValueString() + if host != "" { + _ = unstructured.SetNestedField(object, map[string]interface{}{"host": host}, "spec", "ingressconfig") + } + + return &unstructured.Unstructured{Object: object}, nil +} + +// unstructuredToHTTPTriggerModel читает нужные поля из CRD объекта обратно в Terraform state. +func unstructuredToHTTPTriggerModel(ctx context.Context, triggerObject *unstructured.Unstructured, base httpTriggerResourceModel) httpTriggerResourceModel { + urlValue, _, _ := unstructured.NestedString(triggerObject.Object, "spec", "relativeurl") + functionName, _, _ := unstructured.NestedString(triggerObject.Object, "spec", "functionref", "name") + hostValue, _, _ := unstructured.NestedString(triggerObject.Object, "spec", "ingressconfig", "host") + createIngressValue, foundCreateIngress, _ := unstructured.NestedBool(triggerObject.Object, "spec", "createingress") + methodsValue, _, _ := unstructured.NestedStringSlice(triggerObject.Object, "spec", "methods") + + methodsList, _ := types.ListValueFrom(ctx, types.StringType, methodsValue) + + state := base + state.Name = types.StringValue(triggerObject.GetName()) + state.Namespace = types.StringValue(triggerObject.GetNamespace()) + state.ID = types.StringValue(fmt.Sprintf("%s/%s", triggerObject.GetNamespace(), triggerObject.GetName())) + state.UID = types.StringValue(string(triggerObject.GetUID())) + state.Methods = methodsList + + if urlValue != "" { + state.URL = types.StringValue(urlValue) + } + if functionName != "" { + state.Function = types.StringValue(functionName) + } + if hostValue != "" { + state.Host = types.StringValue(hostValue) + } + if foundCreateIngress { + state.CreateIngress = types.BoolValue(createIngressValue) + } + + return state }