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/int64default" "github.com/hashicorp/terraform-plugin-framework/types" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "terraform-provider-fission/internal/client" ) var _ resource.Resource = &EnvironmentResource{} var _ resource.ResourceWithImportState = &EnvironmentResource{} // Изменено: 2026-04-14 19:15 UTC. // Resource для управления Fission Environment через Kubernetes CRD API. type EnvironmentResource struct { client *client.Client } type environmentResourceModel struct { ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` Image types.String `tfsdk:"image"` Version types.Int64 `tfsdk:"version"` PoolSize types.Int64 `tfsdk:"poolsize"` Namespace types.String `tfsdk:"namespace"` UID types.String `tfsdk:"uid"` } func NewEnvironmentResource() resource.Resource { return &EnvironmentResource{} } func (r *EnvironmentResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_environment" } func (r *EnvironmentResource) 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 Environment.", }, "image": schema.StringAttribute{ Required: true, Description: "Runtime image для Environment (например ghcr.io/fission/python-env).", }, "version": schema.Int64Attribute{ Optional: true, Computed: true, Default: int64default.StaticInt64(3), Description: "Версия спецификации Environment.", }, "poolsize": schema.Int64Attribute{ Optional: true, Computed: true, Default: int64default.StaticInt64(3), Description: "Размер пула pre-warmed контейнеров.", }, "namespace": schema.StringAttribute{ Optional: true, Computed: true, Description: "Namespace, где создается Environment. По умолчанию используется provider namespace.", }, "uid": schema.StringAttribute{ Computed: true, Description: "UID Kubernetes объекта Environment.", }, }, } } // Configure получает клиент из provider.Configure(). func (r *EnvironmentResource) 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 создает новый Environment CRD объект. func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan environmentResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } namespace := plan.Namespace.ValueString() if namespace == "" { namespace = r.client.Namespace } environmentObject := environmentToUnstructured(plan, namespace) createdEnvironment, err := r.client.CreateEnvironment(ctx, environmentObject) if err != nil { resp.Diagnostics.AddError("Ошибка создания Fission Environment", err.Error()) return } state := unstructuredToEnvironmentModel(createdEnvironment, plan) resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } // Read синхронизирует Terraform state с текущим состоянием CRD. func (r *EnvironmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state environmentResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } namespace := state.Namespace.ValueString() if namespace == "" { namespace = r.client.Namespace } environmentObject, err := r.client.GetEnvironment(ctx, namespace, state.Name.ValueString()) if err != nil { if client.IsNotFound(err) { resp.State.RemoveResource(ctx) return } resp.Diagnostics.AddError("Ошибка чтения Fission Environment", err.Error()) return } updatedState := unstructuredToEnvironmentModel(environmentObject, state) resp.Diagnostics.Append(resp.State.Set(ctx, &updatedState)...) } // Update обновляет существующий Environment CRD объект. func (r *EnvironmentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan environmentResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { return } namespace := plan.Namespace.ValueString() if namespace == "" { namespace = r.client.Namespace } environmentObject := environmentToUnstructured(plan, namespace) existingEnvironment, err := r.client.GetEnvironment(ctx, namespace, plan.Name.ValueString()) if err != nil { resp.Diagnostics.AddError("Ошибка получения Environment перед обновлением", err.Error()) return } environmentObject.SetResourceVersion(existingEnvironment.GetResourceVersion()) updatedEnvironment, err := r.client.UpdateEnvironment(ctx, environmentObject) if err != nil { resp.Diagnostics.AddError("Ошибка обновления Fission Environment", err.Error()) return } state := unstructuredToEnvironmentModel(updatedEnvironment, plan) resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } // Delete удаляет Environment CRD объект. func (r *EnvironmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state environmentResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } namespace := state.Namespace.ValueString() if namespace == "" { namespace = r.client.Namespace } err := r.client.DeleteEnvironment(ctx, namespace, state.Name.ValueString()) if err != nil && !client.IsNotFound(err) { resp.Diagnostics.AddError("Ошибка удаления Fission Environment", err.Error()) } } // ImportState поддерживает импорт ресурса в формате namespace/name. func (r *EnvironmentResource) 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)...) } // environmentToUnstructured преобразует Terraform model в Kubernetes CRD payload. func environmentToUnstructured(model environmentResourceModel, namespace string) *unstructured.Unstructured { return &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": "fission.io/v1", "kind": "Environment", "metadata": map[string]interface{}{ "name": model.Name.ValueString(), "namespace": namespace, }, "spec": map[string]interface{}{ "version": model.Version.ValueInt64(), "runtime": map[string]interface{}{ "image": model.Image.ValueString(), }, "poolsize": model.PoolSize.ValueInt64(), }, }} } // unstructuredToEnvironmentModel читает нужные поля из CRD объекта обратно в Terraform state. func unstructuredToEnvironmentModel(environmentObject *unstructured.Unstructured, base environmentResourceModel) environmentResourceModel { imageValue, _, _ := unstructured.NestedString(environmentObject.Object, "spec", "runtime", "image") versionValue, _, _ := unstructured.NestedInt64(environmentObject.Object, "spec", "version") poolsizeValue, _, _ := unstructured.NestedInt64(environmentObject.Object, "spec", "poolsize") state := base state.Name = types.StringValue(environmentObject.GetName()) state.Namespace = types.StringValue(environmentObject.GetNamespace()) state.ID = types.StringValue(fmt.Sprintf("%s/%s", environmentObject.GetNamespace(), environmentObject.GetName())) state.UID = types.StringValue(string(environmentObject.GetUID())) if imageValue != "" { state.Image = types.StringValue(imageValue) } if versionValue != 0 { state.Version = types.Int64Value(versionValue) } if poolsizeValue != 0 { state.PoolSize = types.Int64Value(poolsizeValue) } return state }