Files
Naeel fd236d87ea feat: provider audit — builder support, function tuning, source archive
Аудит провайдера vs каноничный Fission. Добавлено:

Environment:
- builder_image, builder_command для Go и языков с build step

Package:
- deploy_type (literal/source) для переключения deployment/source archive
- loadPackageSourceArchive() — zip-упаковка source_dir
- Убран пустой source:{} из literal mode

Function:
- executor_type (poolmgr/newdeploy/container)
- function_timeout, idle_timeout
- min_scale, max_scale для ExecutionStrategy

Все 21 тест пройден. Обратная совместимость проверена.
Документация: doc/AUDIT_PROVIDER_VS_FISSION_2026-06-03.md
2026-04-15 17:46:47 +03:00

412 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package resources
import (
"context"
"encoding/base64"
"fmt"
"strings"
"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/stringdefault"
"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.ResourceWithImportState = &FunctionResource{}
// Изменено: 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"`
ExecutorType types.String `tfsdk:"executor_type"`
FunctionTimeout types.Int64 `tfsdk:"function_timeout"`
IdleTimeout types.Int64 `tfsdk:"idle_timeout"`
MinScale types.Int64 `tfsdk:"min_scale"`
MaxScale types.Int64 `tfsdk:"max_scale"`
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{
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).",
},
"executor_type": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("poolmgr"),
Description: "Тип executor: poolmgr (default), newdeploy или container.",
},
"function_timeout": schema.Int64Attribute{
Optional: true,
Description: "Таймаут выполнения функции в секундах (Fission default: 60).",
},
"idle_timeout": schema.Int64Attribute{
Optional: true,
Description: "Время простоя до scale-to-zero в секундах (Fission default: 120).",
},
"min_scale": schema.Int64Attribute{
Optional: true,
Description: "Минимальное число реплик (для newdeploy/container).",
},
"max_scale": schema.Int64Attribute{
Optional: true,
Description: "Максимальное число реплик (для newdeploy/container).",
},
"namespace": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Namespace, где создается Function. По умолчанию используется provider namespace.",
},
"uid": schema.StringAttribute{
Computed: true,
Description: "UID Kubernetes объекта Function.",
},
},
}
}
// 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
}
// 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)
if err := ensureEnvironmentExists(ctx, r.client, namespace, plan.Environment.ValueString()); err != nil {
resp.Diagnostics.AddError("Ошибка валидации Environment для Function", err.Error())
return
}
pkg, err := ensurePackageExists(ctx, r.client, namespace, plan.PackageName.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка валидации Package для Function", err.Error())
return
}
if err := validateEntrypointAgainstPackageSource(plan.Entrypoint.ValueString(), pkg); err != nil {
resp.Diagnostics.AddError("Ошибка валидации entrypoint", err.Error())
return
}
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)...)
}
// 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)...)
}
// 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)
if err := ensureEnvironmentExists(ctx, r.client, namespace, plan.Environment.ValueString()); err != nil {
resp.Diagnostics.AddError("Ошибка валидации Environment для Function", err.Error())
return
}
pkg, err := ensurePackageExists(ctx, r.client, namespace, plan.PackageName.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка валидации Package для Function", err.Error())
return
}
if err := validateEntrypointAgainstPackageSource(plan.Entrypoint.ValueString(), pkg); err != nil {
resp.Diagnostics.AddError("Ошибка валидации entrypoint", err.Error())
return
}
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)...)
}
// 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())
}
}
// ImportState поддерживает импорт ресурса в формате namespace/name.
func (r *FunctionResource) 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)...)
}
// functionToUnstructured преобразует Terraform model в Kubernetes CRD payload.
func functionToUnstructured(model functionResourceModel, namespace string) *unstructured.Unstructured {
executorType := "poolmgr"
if !model.ExecutorType.IsNull() && !model.ExecutorType.IsUnknown() && model.ExecutorType.ValueString() != "" {
executorType = model.ExecutorType.ValueString()
}
executionStrategy := map[string]interface{}{
"ExecutorType": executorType,
}
if !model.MinScale.IsNull() && !model.MinScale.IsUnknown() {
executionStrategy["MinScale"] = model.MinScale.ValueInt64()
}
if !model.MaxScale.IsNull() && !model.MaxScale.IsUnknown() {
executionStrategy["MaxScale"] = model.MaxScale.ValueInt64()
}
spec := map[string]interface{}{
"environment": map[string]interface{}{
"name": model.Environment.ValueString(),
"namespace": namespace,
},
"InvokeStrategy": map[string]interface{}{
"ExecutionStrategy": executionStrategy,
"StrategyType": "execution",
},
"package": map[string]interface{}{
"packageref": map[string]interface{}{
"name": model.PackageName.ValueString(),
"namespace": namespace,
},
"functionName": model.Entrypoint.ValueString(),
},
}
if !model.FunctionTimeout.IsNull() && !model.FunctionTimeout.IsUnknown() {
spec["functionTimeout"] = model.FunctionTimeout.ValueInt64()
}
if !model.IdleTimeout.IsNull() && !model.IdleTimeout.IsUnknown() {
spec["idletimeout"] = model.IdleTimeout.ValueInt64()
}
return &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "fission.io/v1",
"kind": "Function",
"metadata": map[string]interface{}{
"name": model.Name.ValueString(),
"namespace": namespace,
},
"spec": spec,
}}
}
// 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")
executorType, _, _ := unstructured.NestedString(functionObject.Object, "spec", "InvokeStrategy", "ExecutionStrategy", "ExecutorType")
functionTimeout, foundFT, _ := unstructured.NestedInt64(functionObject.Object, "spec", "functionTimeout")
idleTimeout, foundIT, _ := unstructured.NestedInt64(functionObject.Object, "spec", "idletimeout")
minScale, foundMin, _ := unstructured.NestedInt64(functionObject.Object, "spec", "InvokeStrategy", "ExecutionStrategy", "MinScale")
maxScale, foundMax, _ := unstructured.NestedInt64(functionObject.Object, "spec", "InvokeStrategy", "ExecutionStrategy", "MaxScale")
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)
}
if executorType != "" {
state.ExecutorType = types.StringValue(executorType)
}
if foundFT {
state.FunctionTimeout = types.Int64Value(functionTimeout)
}
if foundIT {
state.IdleTimeout = types.Int64Value(idleTimeout)
}
if foundMin {
state.MinScale = types.Int64Value(minScale)
}
if foundMax {
state.MaxScale = types.Int64Value(maxScale)
}
return state
}
func validateEntrypointAgainstPackageSource(entrypoint string, pkg *unstructured.Unstructured) error {
if entrypoint == "" {
return fmt.Errorf("entrypoint не может быть пустым")
}
// Для Python/Go/JS: валидируем формат module.function и наличие функции в исходнике.
// Для PHP (module::function), Ruby (function), Perl (function) — допускаем любой непустой формат.
parts := strings.Split(entrypoint, ".")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
// Не стандартный module.function — допускаем (PHP, Ruby, Perl и др.)
return nil
}
if parts[0] != "main" {
return nil
}
literalSource, found, err := unstructured.NestedString(pkg.Object, "spec", "deployment", "literal")
if err != nil || !found || literalSource == "" {
return nil
}
literalBytes, err := base64.StdEncoding.DecodeString(literalSource)
if err != nil {
return nil
}
source := string(literalBytes)
if looksLikePythonSource(source) {
signature := fmt.Sprintf("def %s(", parts[1])
if !strings.Contains(source, signature) {
return fmt.Errorf("entrypoint %q не найден в Python исходнике пакета (ожидался %q)", entrypoint, signature)
}
}
return nil
}
func looksLikePythonSource(source string) bool {
trimmed := strings.TrimSpace(source)
if strings.HasPrefix(trimmed, "def ") || strings.Contains(source, "\ndef ") {
return true
}
if strings.Contains(source, "import ") && !strings.Contains(source, "func ") && !strings.Contains(source, "module.exports") {
return true
}
return false
}