package resources import ( "context" "fmt" "github.com/hashicorp/terraform-plugin-framework/path" "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/types" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "terraform-provider-fission/internal/client" ) var _ resource.Resource = &HTTPTriggerResource{} var _ resource.ResourceWithImportState = &HTTPTriggerResource{} // Изменено: 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{ 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, Computed: true, ElementType: types.StringType, Description: "HTTP методы trigger, например [\"GET\", \"POST\"].", }, "create_ingress": schema.BoolAttribute{ Optional: true, Computed: true, Default: booldefault.StaticBool(false), 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.", }, }, } } // 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 } // 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)...) } // 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)...) } // 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)...) } // 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()) } } // ImportState поддерживает импорт ресурса в формате namespace/name. func (r *HTTPTriggerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { namespace, name, err := parseImportID(req.ID) if err != nil { resp.Diagnostics.AddError("Некорректный формат import ID", err.Error()) return } resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("namespace"), namespace)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...) } // 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 }