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/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"` 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).", }, "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 { 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, }, "InvokeStrategy": map[string]interface{}{ "ExecutionStrategy": map[string]interface{}{ "ExecutorType": "poolmgr", }, "StrategyType": "execution", }, "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 } func validateEntrypointAgainstPackageSource(entrypoint string, pkg *unstructured.Unstructured) error { parts := strings.Split(entrypoint, ".") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return fmt.Errorf("entrypoint %q должен иметь формат module.function", entrypoint) } 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 }