feat: add fission_simple_function composite resource
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
# 2026-04-20 — fission_simple_function
|
||||||
|
|
||||||
|
## Задача
|
||||||
|
|
||||||
|
Добавить составной ресурс `fission_simple_function`, который позволяет описать serverless-функцию одним HCL-блоком вместо четырёх отдельных ресурсов.
|
||||||
|
|
||||||
|
## Мотивация
|
||||||
|
|
||||||
|
До: пользователь обязан вручную объявлять `fission_environment` + `fission_package` + `fission_function` + `fission_http_trigger`.
|
||||||
|
|
||||||
|
После: достаточно одного блока:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
resource "fission_simple_function" "health" {
|
||||||
|
name = "health"
|
||||||
|
runtime = "nodejs"
|
||||||
|
code_dir = "./code/health"
|
||||||
|
url = "/ecom/health"
|
||||||
|
methods = ["GET"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Реализация
|
||||||
|
|
||||||
|
**Файл:** `terraform/provider/internal/resources/simple_function_resource.go`
|
||||||
|
|
||||||
|
### Схема ресурса
|
||||||
|
|
||||||
|
| Атрибут | Тип | Поведение |
|
||||||
|
|---------|-----|-----------|
|
||||||
|
| `name` | required string | RequiresReplace |
|
||||||
|
| `runtime` | required string | RequiresReplace; допустимые: nodejs, python, go, ruby, php, perl |
|
||||||
|
| `code_dir` | required string | путь к директории с исходниками |
|
||||||
|
| `url` | optional string | если задан — создаётся HTTPTrigger |
|
||||||
|
| `methods` | optional+computed list(string) | дефолт `["GET"]` |
|
||||||
|
| `namespace` | optional+computed string | дефолт из провайдера |
|
||||||
|
| `environment_name` | computed string | имя созданного/найденного env |
|
||||||
|
| `package_name` | computed string | `<name>-pkg` |
|
||||||
|
| `trigger_name` | computed string | `<name>-trigger` или "" |
|
||||||
|
| `code_hash` | computed string | SHA-256 кода из code_dir |
|
||||||
|
|
||||||
|
### Логика Create
|
||||||
|
|
||||||
|
1. `findOrCreateSimpleEnv` — ищет `simple-<runtime>-env`; если нет — создаёт с параметрами из `runtimeRegistry`.
|
||||||
|
2. `loadPackageContent(code_dir, "", deployType)` — загружает код (JS получает ESM zip, Go — source zip, остальные — literal bytes).
|
||||||
|
3. `CreatePackage` → `CreateFunction` → `CreateHTTPTrigger` (опционально).
|
||||||
|
4. Rollback при ошибке: если Function не создалась — удаляет Package; если Trigger — удаляет Function + Package.
|
||||||
|
|
||||||
|
### runtimeRegistry
|
||||||
|
|
||||||
|
| Runtime | deployType | entrypoint | builderImage |
|
||||||
|
|---------|-----------|------------|-------------|
|
||||||
|
| nodejs | literal | "" (default export) | — |
|
||||||
|
| python | literal | main.main | — |
|
||||||
|
| go | source | Handler | ghcr.io/fission/go-builder:latest |
|
||||||
|
| ruby | literal | main.main | — |
|
||||||
|
| php | literal | main.main | — |
|
||||||
|
| perl | literal | main.main | — |
|
||||||
|
|
||||||
|
### ModifyPlan
|
||||||
|
|
||||||
|
Пересчитывает `code_hash` при каждом plan, чтобы Terraform обнаруживал изменения кода без force-replace.
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
- Если `code_hash` изменился → `UpdatePackage` (patch content, preserve resourceVersion).
|
||||||
|
- URL добавлен/удалён → `CreateHTTPTrigger` / `DeleteHTTPTrigger`.
|
||||||
|
- URL изменился → `UpdateHTTPTrigger`.
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
Порядок: HTTPTrigger → Function → Package. Environment намеренно НЕ удаляется (shared).
|
||||||
|
|
||||||
|
### Import
|
||||||
|
|
||||||
|
Формат: `namespace/name`. После import читается из Kubernetes через `Read`.
|
||||||
|
|
||||||
|
## Регистрация
|
||||||
|
|
||||||
|
`terraform/provider/internal/provider/provider.go` — добавлено `resources.NewSimpleFunctionResource` в `Resources()`.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
- [x] Реализация написана и скомпилирована (`go build ./...` — OK)
|
||||||
|
- [ ] Тест на real cluster
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler — точка входа для fission go-env.
|
||||||
|
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{"status":"processed"}`))
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
def main():
|
||||||
|
return {"status": "ok", "metrics": {}}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# simple_functions.tf — примеры fission_simple_function (добавлено 2026-04-20)
|
||||||
|
#
|
||||||
|
# Демонстрирует использование нового составного ресурса, который заменяет
|
||||||
|
# отдельные fission_environment + fission_package + fission_function + fission_http_trigger
|
||||||
|
# одним блоком.
|
||||||
|
#
|
||||||
|
# Environment simple-<runtime>-env создаётся автоматически при первом apply.
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ─── health-check (nodejs, с HTTPTrigger) ────────────────────────────────────
|
||||||
|
resource "fission_simple_function" "health" {
|
||||||
|
name = "ecom-health"
|
||||||
|
runtime = "nodejs"
|
||||||
|
code_dir = "${path.module}/code/node-health"
|
||||||
|
url = "/ecom/health"
|
||||||
|
methods = ["GET"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── metrics (python, без HTTPTrigger) ───────────────────────────────────────
|
||||||
|
resource "fission_simple_function" "metrics" {
|
||||||
|
name = "ecom-metrics"
|
||||||
|
runtime = "python"
|
||||||
|
code_dir = "${path.module}/code/python-metrics"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── go-processor (go, с HTTPTrigger, PUT/POST) ───────────────────────────────
|
||||||
|
resource "fission_simple_function" "processor" {
|
||||||
|
name = "ecom-processor"
|
||||||
|
runtime = "go"
|
||||||
|
code_dir = "${path.module}/code/go-processor"
|
||||||
|
url = "/ecom/process"
|
||||||
|
methods = ["POST", "PUT"]
|
||||||
|
}
|
||||||
@@ -98,6 +98,7 @@ func (p *FissionProvider) Resources(_ context.Context) []func() resource.Resourc
|
|||||||
resources.NewPackageResource,
|
resources.NewPackageResource,
|
||||||
resources.NewFunctionResource,
|
resources.NewFunctionResource,
|
||||||
resources.NewHTTPTriggerResource,
|
resources.NewHTTPTriggerResource,
|
||||||
|
resources.NewSimpleFunctionResource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,660 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
// simple_function_resource.go — составной ресурс fission_simple_function.
|
||||||
|
//
|
||||||
|
// Позволяет объявить serverless-функцию одним блоком HCL:
|
||||||
|
//
|
||||||
|
// resource "fission_simple_function" "health" {
|
||||||
|
// name = "health"
|
||||||
|
// runtime = "nodejs"
|
||||||
|
// code_dir = "./code/health"
|
||||||
|
// url = "/ecom/health"
|
||||||
|
// methods = ["GET"]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Провайдер сам:
|
||||||
|
// 1. Ищет environment simple-<runtime>-env, если не найден — создаёт.
|
||||||
|
// 2. Создаёт Package (<name>-pkg) с кодом из code_dir.
|
||||||
|
// 3. Создаёт Function (<name>).
|
||||||
|
// 4. Создаёт HTTPTrigger (<name>-trigger), если указан url.
|
||||||
|
//
|
||||||
|
// Добавлено: 2026-04-20
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"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/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
|
||||||
|
"terraform-provider-fission/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ resource.Resource = &SimpleFunctionResource{}
|
||||||
|
var _ resource.ResourceWithImportState = &SimpleFunctionResource{}
|
||||||
|
var _ resource.ResourceWithModifyPlan = &SimpleFunctionResource{}
|
||||||
|
|
||||||
|
// runtimeMeta содержит параметры окружения для каждого поддерживаемого runtime.
|
||||||
|
type runtimeMeta struct {
|
||||||
|
image string // образ среды выполнения
|
||||||
|
builderImage string // образ сборщика (пустая строка если не нужен)
|
||||||
|
deployType string // "literal" или "source"
|
||||||
|
entrypoint string // точка входа функции
|
||||||
|
poolSize int64 // размер пула
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtimeRegistry — маппинг имени runtime → параметры среды выполнения.
|
||||||
|
var runtimeRegistry = map[string]runtimeMeta{
|
||||||
|
"nodejs": {
|
||||||
|
image: "ghcr.io/fission/node-env:latest",
|
||||||
|
builderImage: "",
|
||||||
|
deployType: "literal",
|
||||||
|
entrypoint: "", // JS runtime использует default export (пустая строка)
|
||||||
|
poolSize: 3,
|
||||||
|
},
|
||||||
|
"python": {
|
||||||
|
image: "ghcr.io/fission/python-env:latest",
|
||||||
|
builderImage: "",
|
||||||
|
deployType: "literal",
|
||||||
|
entrypoint: "main.main",
|
||||||
|
poolSize: 3,
|
||||||
|
},
|
||||||
|
"go": {
|
||||||
|
image: "ghcr.io/fission/go-env:latest",
|
||||||
|
builderImage: "ghcr.io/fission/go-builder:latest",
|
||||||
|
deployType: "source",
|
||||||
|
entrypoint: "Handler",
|
||||||
|
poolSize: 3,
|
||||||
|
},
|
||||||
|
"ruby": {
|
||||||
|
image: "ghcr.io/fission/ruby-env:latest",
|
||||||
|
builderImage: "",
|
||||||
|
deployType: "literal",
|
||||||
|
entrypoint: "main.main",
|
||||||
|
poolSize: 3,
|
||||||
|
},
|
||||||
|
"php": {
|
||||||
|
image: "ghcr.io/fission/php-env:latest",
|
||||||
|
builderImage: "",
|
||||||
|
deployType: "literal",
|
||||||
|
entrypoint: "main.main",
|
||||||
|
poolSize: 3,
|
||||||
|
},
|
||||||
|
"perl": {
|
||||||
|
image: "ghcr.io/fission/perl-env:latest",
|
||||||
|
builderImage: "",
|
||||||
|
deployType: "literal",
|
||||||
|
entrypoint: "main.main",
|
||||||
|
poolSize: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleFunctionResource — составной ресурс: Environment + Package + Function + HTTPTrigger.
|
||||||
|
type SimpleFunctionResource struct {
|
||||||
|
client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleFunctionModel описывает Terraform state ресурса fission_simple_function.
|
||||||
|
type simpleFunctionModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
Name types.String `tfsdk:"name"`
|
||||||
|
Runtime types.String `tfsdk:"runtime"`
|
||||||
|
CodeDir types.String `tfsdk:"code_dir"`
|
||||||
|
URL types.String `tfsdk:"url"`
|
||||||
|
Methods types.List `tfsdk:"methods"`
|
||||||
|
Namespace types.String `tfsdk:"namespace"`
|
||||||
|
EnvironmentName types.String `tfsdk:"environment_name"`
|
||||||
|
PackageName types.String `tfsdk:"package_name"`
|
||||||
|
TriggerName types.String `tfsdk:"trigger_name"`
|
||||||
|
CodeHash types.String `tfsdk:"code_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSimpleFunctionResource создаёт инстанс составного ресурса.
|
||||||
|
func NewSimpleFunctionResource() resource.Resource {
|
||||||
|
return &SimpleFunctionResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SimpleFunctionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_simple_function"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SimpleFunctionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
Description: "Составной ресурс: создаёт Environment (при необходимости), Package, Function и HTTPTrigger одним блоком.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
Computed: true,
|
||||||
|
Description: "Идентификатор ресурса (namespace/name).",
|
||||||
|
},
|
||||||
|
"name": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
Description: "Имя функции. Используется как база для имён Package и HTTPTrigger.",
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"runtime": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
Description: "Runtime функции: nodejs, python, go, ruby, php, perl.",
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"code_dir": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
Description: "Путь к директории с исходным кодом (main.js, main.py, main.go и т.д.).",
|
||||||
|
},
|
||||||
|
"url": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
Description: "Относительный URL для HTTPTrigger. Если не задан — HTTPTrigger не создаётся.",
|
||||||
|
},
|
||||||
|
"methods": schema.ListAttribute{
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
ElementType: types.StringType,
|
||||||
|
Description: "HTTP методы для HTTPTrigger. По умолчанию [\"GET\"].",
|
||||||
|
},
|
||||||
|
"namespace": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Description: "Namespace для ресурсов. По умолчанию используется namespace провайдера.",
|
||||||
|
},
|
||||||
|
"environment_name": schema.StringAttribute{
|
||||||
|
Computed: true,
|
||||||
|
Description: "Имя Environment, который был использован или создан (simple-<runtime>-env).",
|
||||||
|
},
|
||||||
|
"package_name": schema.StringAttribute{
|
||||||
|
Computed: true,
|
||||||
|
Description: "Имя созданного Package (<name>-pkg).",
|
||||||
|
},
|
||||||
|
"trigger_name": schema.StringAttribute{
|
||||||
|
Computed: true,
|
||||||
|
Description: "Имя созданного HTTPTrigger (<name>-trigger). Пусто, если url не задан.",
|
||||||
|
},
|
||||||
|
"code_hash": schema.StringAttribute{
|
||||||
|
Computed: true,
|
||||||
|
Description: "SHA-256 хеш кода из code_dir. Terraform обнаруживает изменения кода по этому полю.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyPlan пересчитывает code_hash из code_dir, чтобы Terraform видел изменения кода.
|
||||||
|
func (r *SimpleFunctionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||||||
|
if req.Plan.Raw.IsNull() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var plan simpleFunctionModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if plan.CodeDir.IsNull() || plan.CodeDir.IsUnknown() || plan.CodeDir.ValueString() == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deployType := "literal"
|
||||||
|
if !plan.Runtime.IsNull() && !plan.Runtime.IsUnknown() {
|
||||||
|
if meta, ok := runtimeRegistry[plan.Runtime.ValueString()]; ok {
|
||||||
|
deployType = meta.deployType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeBytes, err := loadPackageContent(plan.CodeDir.ValueString(), "", deployType)
|
||||||
|
if err != nil {
|
||||||
|
// Файл может отсутствовать при первом plan — не ошибка.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256(codeBytes)
|
||||||
|
plan.CodeHash = types.StringValue(fmt.Sprintf("%x", sum))
|
||||||
|
resp.Diagnostics.Append(resp.Plan.Set(ctx, &plan)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure получает клиент из provider.Configure().
|
||||||
|
func (r *SimpleFunctionResource) 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 (при необходимости), Package, Function и HTTPTrigger.
|
||||||
|
func (r *SimpleFunctionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var plan simpleFunctionModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := resolveNamespace(plan.Namespace, r.client.Namespace)
|
||||||
|
|
||||||
|
meta, ok := runtimeRegistry[plan.Runtime.ValueString()]
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Неподдерживаемый runtime",
|
||||||
|
fmt.Sprintf("Runtime %q не поддерживается. Допустимые значения: nodejs, python, go, ruby, php, perl.", plan.Runtime.ValueString()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Найти или создать environment.
|
||||||
|
envName, err := findOrCreateSimpleEnv(ctx, r.client, namespace, plan.Runtime.ValueString(), meta)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка подготовки Environment", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Загрузить код из code_dir.
|
||||||
|
codeBytes, err := loadPackageContent(plan.CodeDir.ValueString(), "", meta.deployType)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка чтения исходного кода", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(codeBytes)
|
||||||
|
codeHash := fmt.Sprintf("%x", sum)
|
||||||
|
|
||||||
|
// 3. Создать Package.
|
||||||
|
pkgName := plan.Name.ValueString() + "-pkg"
|
||||||
|
pkgObj := buildSimplePackage(pkgName, envName, namespace, codeBytes, meta.deployType)
|
||||||
|
if _, err = r.client.CreatePackage(ctx, pkgObj); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка создания Package", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Создать Function.
|
||||||
|
fnObj := buildSimpleFunction(plan.Name.ValueString(), envName, pkgName, meta.entrypoint, namespace)
|
||||||
|
if _, err = r.client.CreateFunction(ctx, fnObj); err != nil {
|
||||||
|
_ = r.client.DeletePackage(ctx, namespace, pkgName)
|
||||||
|
resp.Diagnostics.AddError("Ошибка создания Function", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Создать HTTPTrigger (только если задан url).
|
||||||
|
triggerName := ""
|
||||||
|
if url := plan.URL.ValueString(); url != "" {
|
||||||
|
triggerName = plan.Name.ValueString() + "-trigger"
|
||||||
|
methods := simpleMethods(ctx, plan.Methods)
|
||||||
|
triggerObj := buildSimpleTrigger(triggerName, plan.Name.ValueString(), url, methods, namespace)
|
||||||
|
if _, err = r.client.CreateHTTPTrigger(ctx, triggerObj); err != nil {
|
||||||
|
_ = r.client.DeleteFunction(ctx, namespace, plan.Name.ValueString())
|
||||||
|
_ = r.client.DeletePackage(ctx, namespace, pkgName)
|
||||||
|
resp.Diagnostics.AddError("Ошибка создания HTTPTrigger", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMethods, _ := types.ListValueFrom(ctx, types.StringType, []string{"GET"})
|
||||||
|
methods := plan.Methods
|
||||||
|
if methods.IsNull() || methods.IsUnknown() {
|
||||||
|
methods = defaultMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
state := simpleFunctionModel{
|
||||||
|
ID: types.StringValue(fmt.Sprintf("%s/%s", namespace, plan.Name.ValueString())),
|
||||||
|
Name: plan.Name,
|
||||||
|
Runtime: plan.Runtime,
|
||||||
|
CodeDir: plan.CodeDir,
|
||||||
|
URL: plan.URL,
|
||||||
|
Methods: methods,
|
||||||
|
Namespace: types.StringValue(namespace),
|
||||||
|
EnvironmentName: types.StringValue(envName),
|
||||||
|
PackageName: types.StringValue(pkgName),
|
||||||
|
TriggerName: types.StringValue(triggerName),
|
||||||
|
CodeHash: types.StringValue(codeHash),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read синхронизирует state с Kubernetes.
|
||||||
|
func (r *SimpleFunctionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var state simpleFunctionModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := resolveNamespace(state.Namespace, r.client.Namespace)
|
||||||
|
|
||||||
|
// Основной ресурс — Function.
|
||||||
|
if _, err := r.client.GetFunction(ctx, namespace, state.Name.ValueString()); err != nil {
|
||||||
|
if client.IsNotFound(err) {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Ошибка чтения Function", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Синхронизировать url/methods из реального состояния HTTPTrigger.
|
||||||
|
if triggerName := state.TriggerName.ValueString(); triggerName != "" {
|
||||||
|
triggerObj, err := r.client.GetHTTPTrigger(ctx, namespace, triggerName)
|
||||||
|
if err != nil && !client.IsNotFound(err) {
|
||||||
|
resp.Diagnostics.AddError("Ошибка чтения HTTPTrigger", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if urlVal, _, _ := unstructured.NestedString(triggerObj.Object, "spec", "relativeurl"); urlVal != "" {
|
||||||
|
state.URL = types.StringValue(urlVal)
|
||||||
|
}
|
||||||
|
if methodsVal, _, _ := unstructured.NestedStringSlice(triggerObj.Object, "spec", "methods"); len(methodsVal) > 0 {
|
||||||
|
methodsList, _ := types.ListValueFrom(ctx, types.StringType, methodsVal)
|
||||||
|
state.Methods = methodsList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update обновляет Package (если код изменился) и HTTPTrigger (если url/methods изменились).
|
||||||
|
func (r *SimpleFunctionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var plan simpleFunctionModel
|
||||||
|
var state simpleFunctionModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := resolveNamespace(plan.Namespace, r.client.Namespace)
|
||||||
|
meta := runtimeRegistry[plan.Runtime.ValueString()]
|
||||||
|
|
||||||
|
// Обновить Package, если изменился код.
|
||||||
|
newCodeBytes, err := loadPackageContent(plan.CodeDir.ValueString(), "", meta.deployType)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка чтения исходного кода", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(newCodeBytes)
|
||||||
|
newHash := fmt.Sprintf("%x", sum)
|
||||||
|
|
||||||
|
if newHash != state.CodeHash.ValueString() {
|
||||||
|
pkgName := state.PackageName.ValueString()
|
||||||
|
existingPkg, err := r.client.GetPackage(ctx, namespace, pkgName)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка получения Package перед обновлением", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newPkg := buildSimplePackage(pkgName, state.EnvironmentName.ValueString(), namespace, newCodeBytes, meta.deployType)
|
||||||
|
newPkg.SetResourceVersion(existingPkg.GetResourceVersion())
|
||||||
|
if _, err = r.client.UpdatePackage(ctx, newPkg); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка обновления Package", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновить HTTPTrigger при изменении url или methods.
|
||||||
|
oldURL := state.URL.ValueString()
|
||||||
|
newURL := plan.URL.ValueString()
|
||||||
|
triggerName := state.TriggerName.ValueString()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case oldURL == "" && newURL != "":
|
||||||
|
triggerName = plan.Name.ValueString() + "-trigger"
|
||||||
|
methods := simpleMethods(ctx, plan.Methods)
|
||||||
|
triggerObj := buildSimpleTrigger(triggerName, plan.Name.ValueString(), newURL, methods, namespace)
|
||||||
|
if _, err = r.client.CreateHTTPTrigger(ctx, triggerObj); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка создания HTTPTrigger", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case oldURL != "" && newURL == "":
|
||||||
|
if triggerName != "" {
|
||||||
|
if err := r.client.DeleteHTTPTrigger(ctx, namespace, triggerName); err != nil && !client.IsNotFound(err) {
|
||||||
|
resp.Diagnostics.AddError("Ошибка удаления HTTPTrigger", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
triggerName = ""
|
||||||
|
|
||||||
|
case oldURL != "" && newURL != "":
|
||||||
|
if triggerName == "" {
|
||||||
|
triggerName = plan.Name.ValueString() + "-trigger"
|
||||||
|
}
|
||||||
|
methods := simpleMethods(ctx, plan.Methods)
|
||||||
|
existing, err := r.client.GetHTTPTrigger(ctx, namespace, triggerName)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка получения HTTPTrigger перед обновлением", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newTrigger := buildSimpleTrigger(triggerName, plan.Name.ValueString(), newURL, methods, namespace)
|
||||||
|
newTrigger.SetResourceVersion(existing.GetResourceVersion())
|
||||||
|
if _, err = r.client.UpdateHTTPTrigger(ctx, newTrigger); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка обновления HTTPTrigger", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMethods, _ := types.ListValueFrom(ctx, types.StringType, []string{"GET"})
|
||||||
|
methods := plan.Methods
|
||||||
|
if methods.IsNull() || methods.IsUnknown() {
|
||||||
|
methods = defaultMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := state
|
||||||
|
newState.CodeDir = plan.CodeDir
|
||||||
|
newState.URL = plan.URL
|
||||||
|
newState.Methods = methods
|
||||||
|
newState.CodeHash = types.StringValue(newHash)
|
||||||
|
newState.TriggerName = types.StringValue(triggerName)
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete удаляет HTTPTrigger, Function и Package (Environment не удаляется).
|
||||||
|
func (r *SimpleFunctionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var state simpleFunctionModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := resolveNamespace(state.Namespace, r.client.Namespace)
|
||||||
|
|
||||||
|
if triggerName := state.TriggerName.ValueString(); triggerName != "" {
|
||||||
|
if err := r.client.DeleteHTTPTrigger(ctx, namespace, triggerName); err != nil && !client.IsNotFound(err) {
|
||||||
|
resp.Diagnostics.AddError("Ошибка удаления HTTPTrigger", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.client.DeleteFunction(ctx, namespace, state.Name.ValueString()); err != nil && !client.IsNotFound(err) {
|
||||||
|
resp.Diagnostics.AddError("Ошибка удаления Function", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkgName := state.PackageName.ValueString(); pkgName != "" {
|
||||||
|
if err := r.client.DeletePackage(ctx, namespace, pkgName); err != nil && !client.IsNotFound(err) {
|
||||||
|
resp.Diagnostics.AddError("Ошибка удаления Package", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportState поддерживает импорт в формате namespace/name.
|
||||||
|
func (r *SimpleFunctionResource) 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)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Вспомогательные функции
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// findOrCreateSimpleEnv ищет environment simple-<runtime>-env; при отсутствии создаёт.
|
||||||
|
func findOrCreateSimpleEnv(ctx context.Context, fissionClient *client.Client, namespace, runtime string, meta runtimeMeta) (string, error) {
|
||||||
|
envName := fmt.Sprintf("simple-%s-env", runtime)
|
||||||
|
|
||||||
|
_, err := fissionClient.GetEnvironment(ctx, namespace, envName)
|
||||||
|
if err == nil {
|
||||||
|
return envName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !client.IsNotFound(err) {
|
||||||
|
return "", fmt.Errorf("проверка environment %q: %w", envName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := map[string]interface{}{
|
||||||
|
"version": int64(2),
|
||||||
|
"runtime": map[string]interface{}{
|
||||||
|
"image": meta.image,
|
||||||
|
},
|
||||||
|
"poolsize": meta.poolSize,
|
||||||
|
}
|
||||||
|
if meta.builderImage != "" {
|
||||||
|
spec["builder"] = map[string]interface{}{
|
||||||
|
"image": meta.builderImage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
envObj := &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "fission.io/v1",
|
||||||
|
"kind": "Environment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": envName,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
"spec": spec,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if _, err = fissionClient.CreateEnvironment(ctx, envObj); err != nil {
|
||||||
|
return "", fmt.Errorf("создание environment %q: %w", envName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return envName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSimplePackage формирует CRD объект Package с кодом в base64.
|
||||||
|
func buildSimplePackage(pkgName, envName, namespace string, codeBytes []byte, deployType string) *unstructured.Unstructured {
|
||||||
|
literalSource := base64.StdEncoding.EncodeToString(codeBytes)
|
||||||
|
|
||||||
|
spec := map[string]interface{}{
|
||||||
|
"environment": map[string]interface{}{
|
||||||
|
"name": envName,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployType == "source" {
|
||||||
|
spec["source"] = map[string]interface{}{
|
||||||
|
"type": "literal",
|
||||||
|
"literal": literalSource,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spec["deployment"] = map[string]interface{}{
|
||||||
|
"type": "literal",
|
||||||
|
"literal": literalSource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "fission.io/v1",
|
||||||
|
"kind": "Package",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": pkgName,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
"spec": spec,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSimpleFunction формирует CRD объект Function.
|
||||||
|
func buildSimpleFunction(name, envName, pkgName, entrypoint, namespace string) *unstructured.Unstructured {
|
||||||
|
return &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "fission.io/v1",
|
||||||
|
"kind": "Function",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": name,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"environment": map[string]interface{}{
|
||||||
|
"name": envName,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
"InvokeStrategy": map[string]interface{}{
|
||||||
|
"ExecutionStrategy": map[string]interface{}{
|
||||||
|
"ExecutorType": "poolmgr",
|
||||||
|
},
|
||||||
|
"StrategyType": "execution",
|
||||||
|
},
|
||||||
|
"package": map[string]interface{}{
|
||||||
|
"packageref": map[string]interface{}{
|
||||||
|
"name": pkgName,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
"functionName": entrypoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSimpleTrigger формирует CRD объект HTTPTrigger.
|
||||||
|
func buildSimpleTrigger(triggerName, functionName, url string, methods []string, namespace string) *unstructured.Unstructured {
|
||||||
|
if len(methods) == 0 {
|
||||||
|
methods = []string{"GET"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "fission.io/v1",
|
||||||
|
"kind": "HTTPTrigger",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": triggerName,
|
||||||
|
"namespace": namespace,
|
||||||
|
},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"relativeurl": url,
|
||||||
|
"methods": methods,
|
||||||
|
"functionref": map[string]interface{}{
|
||||||
|
"type": "name",
|
||||||
|
"name": functionName,
|
||||||
|
},
|
||||||
|
"createingress": false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleMethods извлекает []string из types.List, возвращая ["GET"] по умолчанию.
|
||||||
|
func simpleMethods(ctx context.Context, methodsList types.List) []string {
|
||||||
|
if methodsList.IsNull() || methodsList.IsUnknown() || len(methodsList.Elements()) == 0 {
|
||||||
|
return []string{"GET"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var methods []string
|
||||||
|
if diags := methodsList.ElementsAs(ctx, &methods, false); diags.HasError() {
|
||||||
|
return []string{"GET"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(methods) == 0 {
|
||||||
|
return []string{"GET"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return methods
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user