feat: implement function and http trigger resources via CRD
This commit is contained in:
@@ -38,3 +38,33 @@
|
|||||||
|
|
||||||
### Следующий шаг
|
### Следующий шаг
|
||||||
- Реализовать `fission_function` через CRD `functions.fission.io`.
|
- Реализовать `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.
|
||||||
|
|||||||
@@ -60,3 +60,43 @@
|
|||||||
- Create/Read/Update/Delete через CRD API
|
- Create/Read/Update/Delete через CRD API
|
||||||
- Сборка `go build ./...` успешна.
|
- Сборка `go build ./...` успешна.
|
||||||
- Следующий этап: `fission_function`.
|
- Следующий этап: `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.
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ var packageGVR = schema.GroupVersionResource{
|
|||||||
Resource: "packages",
|
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.
|
// Client хранит клиентов Kubernetes API для работы с CRD Fission.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
DynClient dynamic.Interface
|
DynClient dynamic.Interface
|
||||||
@@ -161,3 +173,83 @@ func (c *Client) DeletePackage(ctx context.Context, namespace, name string) erro
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,34 +2,234 @@ package resources
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
"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{}
|
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 {
|
func NewFunctionResource() resource.Resource {
|
||||||
return &FunctionResource{}
|
return &FunctionResource{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metadata задает имя ресурса fission_function.
|
||||||
func (r *FunctionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
func (r *FunctionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
resp.TypeName = req.ProviderTypeName + "_function"
|
resp.TypeName = req.ProviderTypeName + "_function"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schema задает Terraform schema для Function.
|
||||||
func (r *FunctionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,282 @@ package resources
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
"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{}
|
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 {
|
func NewHTTPTriggerResource() resource.Resource {
|
||||||
return &HTTPTriggerResource{}
|
return &HTTPTriggerResource{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metadata задает имя ресурса fission_http_trigger.
|
||||||
func (r *HTTPTriggerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
func (r *HTTPTriggerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
resp.TypeName = req.ProviderTypeName + "_http_trigger"
|
resp.TypeName = req.ProviderTypeName + "_http_trigger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schema задает Terraform schema для HTTPTrigger.
|
||||||
func (r *HTTPTriggerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user