358 lines
13 KiB
Go
358 lines
13 KiB
Go
package resources
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
|
"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 = &PackageResource{}
|
|
var _ resource.ResourceWithImportState = &PackageResource{}
|
|
|
|
// Изменено: 2026-04-14 19:45 UTC.
|
|
// Resource для управления Fission Package через Kubernetes CRD API.
|
|
type PackageResource struct {
|
|
client *client.Client
|
|
}
|
|
|
|
// packageResourceModel описывает состояние terraform ресурса fission_package.
|
|
type packageResourceModel struct {
|
|
ID types.String `tfsdk:"id"`
|
|
Name types.String `tfsdk:"name"`
|
|
Environment types.String `tfsdk:"environment"`
|
|
SourceDir types.String `tfsdk:"source_dir"`
|
|
CodePath types.String `tfsdk:"code_path"`
|
|
CodeHash types.String `tfsdk:"code_hash"`
|
|
BuildCmd types.String `tfsdk:"build_command"`
|
|
Namespace types.String `tfsdk:"namespace"`
|
|
UID types.String `tfsdk:"uid"`
|
|
BuildStatus types.String `tfsdk:"build_status"`
|
|
BuildLog types.String `tfsdk:"build_log"`
|
|
}
|
|
|
|
// NewPackageResource создает инстанс ресурса пакета.
|
|
func NewPackageResource() resource.Resource {
|
|
return &PackageResource{}
|
|
}
|
|
|
|
// Metadata задает имя ресурса fission_package.
|
|
func (r *PackageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + "_package"
|
|
}
|
|
|
|
// Schema задает Terraform schema для Package.
|
|
func (r *PackageResource) 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 Package.",
|
|
},
|
|
"environment": schema.StringAttribute{
|
|
Required: true,
|
|
Description: "Имя Fission Environment, связанного с пакетом.",
|
|
},
|
|
"source_dir": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: "Путь к директории с кодом, которая будет упакована в zip.",
|
|
},
|
|
"code_path": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: "Путь к готовому zip-архиву с кодом.",
|
|
},
|
|
"code_hash": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: "Произвольный хеш кода для контроля изменений.",
|
|
},
|
|
"build_command": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: "Команда сборки пакета в Fission.",
|
|
},
|
|
"namespace": schema.StringAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Description: "Namespace, где создается Package. По умолчанию используется provider namespace.",
|
|
},
|
|
"uid": schema.StringAttribute{
|
|
Computed: true,
|
|
Description: "UID Kubernetes объекта Package.",
|
|
},
|
|
"build_status": schema.StringAttribute{
|
|
Computed: true,
|
|
Description: "Текущий статус сборки пакета из поля status.buildstatus.",
|
|
},
|
|
"build_log": schema.StringAttribute{
|
|
Computed: true,
|
|
Description: "Лог сборки из поля status.buildlog.",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Configure получает клиент из provider.Configure().
|
|
func (r *PackageResource) 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 создает новый Package CRD объект.
|
|
func (r *PackageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
var plan packageResourceModel
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
namespace := resolveNamespace(plan.Namespace, r.client.Namespace)
|
|
if !validatePackageSource(plan.SourceDir, plan.CodePath, &resp.Diagnostics) {
|
|
return
|
|
}
|
|
|
|
literalBytes, err := loadPackageLiteral(plan.SourceDir.ValueString(), plan.CodePath.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Ошибка чтения исходного кода пакета", err.Error())
|
|
return
|
|
}
|
|
|
|
packageObject := packageToUnstructured(plan, namespace, literalBytes)
|
|
createdPackage, err := r.client.CreatePackage(ctx, packageObject)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Ошибка создания Fission Package", err.Error())
|
|
return
|
|
}
|
|
|
|
state := unstructuredToPackageModel(createdPackage, plan)
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
|
}
|
|
|
|
// Read синхронизирует Terraform state с текущим состоянием Package CRD.
|
|
func (r *PackageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
var state packageResourceModel
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
namespace := resolveNamespace(state.Namespace, r.client.Namespace)
|
|
packageObject, err := r.client.GetPackage(ctx, namespace, state.Name.ValueString())
|
|
if err != nil {
|
|
if client.IsNotFound(err) {
|
|
resp.State.RemoveResource(ctx)
|
|
return
|
|
}
|
|
|
|
resp.Diagnostics.AddError("Ошибка чтения Fission Package", err.Error())
|
|
return
|
|
}
|
|
|
|
updatedState := unstructuredToPackageModel(packageObject, state)
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &updatedState)...)
|
|
}
|
|
|
|
// Update обновляет существующий Package CRD объект.
|
|
func (r *PackageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
var plan packageResourceModel
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
namespace := resolveNamespace(plan.Namespace, r.client.Namespace)
|
|
if !validatePackageSource(plan.SourceDir, plan.CodePath, &resp.Diagnostics) {
|
|
return
|
|
}
|
|
|
|
literalBytes, err := loadPackageLiteral(plan.SourceDir.ValueString(), plan.CodePath.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Ошибка чтения исходного кода пакета", err.Error())
|
|
return
|
|
}
|
|
|
|
existingPackage, err := r.client.GetPackage(ctx, namespace, plan.Name.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Ошибка получения Package перед обновлением", err.Error())
|
|
return
|
|
}
|
|
|
|
packageObject := packageToUnstructured(plan, namespace, literalBytes)
|
|
packageObject.SetResourceVersion(existingPackage.GetResourceVersion())
|
|
|
|
updatedPackage, err := r.client.UpdatePackage(ctx, packageObject)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Ошибка обновления Fission Package", err.Error())
|
|
return
|
|
}
|
|
|
|
state := unstructuredToPackageModel(updatedPackage, plan)
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
|
}
|
|
|
|
// Delete удаляет Package CRD объект.
|
|
func (r *PackageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
var state packageResourceModel
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
namespace := resolveNamespace(state.Namespace, r.client.Namespace)
|
|
err := r.client.DeletePackage(ctx, namespace, state.Name.ValueString())
|
|
if err != nil && !client.IsNotFound(err) {
|
|
resp.Diagnostics.AddError("Ошибка удаления Fission Package", err.Error())
|
|
}
|
|
}
|
|
|
|
// ImportState поддерживает импорт ресурса в формате namespace/name.
|
|
func (r *PackageResource) 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)...)
|
|
}
|
|
|
|
// validatePackageSource проверяет, что задан ровно один источник кода: source_dir или code_path.
|
|
func validatePackageSource(sourceDir, codePath types.String, diagnostics *diag.Diagnostics) bool {
|
|
hasSourceDir := !sourceDir.IsNull() && !sourceDir.IsUnknown() && sourceDir.ValueString() != ""
|
|
hasCodePath := !codePath.IsNull() && !codePath.IsUnknown() && codePath.ValueString() != ""
|
|
|
|
if hasSourceDir == hasCodePath {
|
|
diagnostics.AddError(
|
|
"Некорректная конфигурация источника кода",
|
|
"Нужно указать ровно один параметр: source_dir или code_path.",
|
|
)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// resolveNamespace возвращает namespace ресурса или namespace из provider config.
|
|
func resolveNamespace(resourceNamespace types.String, providerNamespace string) string {
|
|
namespace := resourceNamespace.ValueString()
|
|
if namespace == "" {
|
|
namespace = providerNamespace
|
|
}
|
|
|
|
return namespace
|
|
}
|
|
|
|
// loadPackageLiteral читает bytes для spec.deployment.literal.
|
|
func loadPackageLiteral(sourceDir, codePath string) ([]byte, error) {
|
|
if sourceDir != "" {
|
|
mainFilePath := filepath.Join(sourceDir, "main.py")
|
|
mainFileBytes, err := os.ReadFile(mainFilePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read source_dir main.py %q: %w", mainFilePath, err)
|
|
}
|
|
|
|
return mainFileBytes, nil
|
|
}
|
|
|
|
literalBytes, err := os.ReadFile(codePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read code_path %q: %w", codePath, err)
|
|
}
|
|
|
|
return literalBytes, nil
|
|
}
|
|
|
|
// packageToUnstructured преобразует Terraform model в Kubernetes CRD payload.
|
|
func packageToUnstructured(model packageResourceModel, namespace string, literalBytes []byte) *unstructured.Unstructured {
|
|
literalSource := base64.StdEncoding.EncodeToString(literalBytes)
|
|
|
|
object := map[string]interface{}{
|
|
"apiVersion": "fission.io/v1",
|
|
"kind": "Package",
|
|
"metadata": map[string]interface{}{
|
|
"name": model.Name.ValueString(),
|
|
"namespace": namespace,
|
|
},
|
|
"spec": map[string]interface{}{
|
|
"deployment": map[string]interface{}{
|
|
"type": "literal",
|
|
"literal": literalSource,
|
|
},
|
|
"environment": map[string]interface{}{
|
|
"name": model.Environment.ValueString(),
|
|
"namespace": namespace,
|
|
},
|
|
"source": map[string]interface{}{},
|
|
},
|
|
}
|
|
|
|
if buildCommand := model.BuildCmd.ValueString(); buildCommand != "" {
|
|
_ = unstructured.SetNestedField(object, buildCommand, "spec", "buildcmd")
|
|
}
|
|
|
|
return &unstructured.Unstructured{Object: object}
|
|
}
|
|
|
|
// unstructuredToPackageModel читает нужные поля из CRD объекта обратно в Terraform state.
|
|
func unstructuredToPackageModel(packageObject *unstructured.Unstructured, base packageResourceModel) packageResourceModel {
|
|
environmentName, _, _ := unstructured.NestedString(packageObject.Object, "spec", "environment", "name")
|
|
buildCommand, _, _ := unstructured.NestedString(packageObject.Object, "spec", "buildcmd")
|
|
buildStatus, _, _ := unstructured.NestedString(packageObject.Object, "status", "buildstatus")
|
|
buildLog, _, _ := unstructured.NestedString(packageObject.Object, "status", "buildlog")
|
|
|
|
state := packageResourceModel{
|
|
ID: types.StringValue(fmt.Sprintf("%s/%s", packageObject.GetNamespace(), packageObject.GetName())),
|
|
Name: types.StringValue(packageObject.GetName()),
|
|
Environment: base.Environment,
|
|
SourceDir: base.SourceDir,
|
|
CodePath: base.CodePath,
|
|
CodeHash: base.CodeHash,
|
|
BuildCmd: base.BuildCmd,
|
|
Namespace: types.StringValue(packageObject.GetNamespace()),
|
|
UID: types.StringValue(string(packageObject.GetUID())),
|
|
BuildStatus: types.StringNull(),
|
|
BuildLog: types.StringNull(),
|
|
}
|
|
|
|
if environmentName != "" {
|
|
state.Environment = types.StringValue(environmentName)
|
|
}
|
|
if buildCommand != "" {
|
|
state.BuildCmd = types.StringValue(buildCommand)
|
|
}
|
|
if buildStatus != "" {
|
|
state.BuildStatus = types.StringValue(buildStatus)
|
|
}
|
|
if buildLog != "" {
|
|
state.BuildLog = types.StringValue(buildLog)
|
|
}
|
|
|
|
return state
|
|
}
|