feat: complete MVP resources and validate end-to-end terraform flow
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# Terraform Provider Fission (MVP)
|
||||
|
||||
Текущий MVP провайдера управляет Fission CRD через Kubernetes API:
|
||||
- `fission_environment`
|
||||
- `fission_package`
|
||||
- `fission_function`
|
||||
- `fission_http_trigger`
|
||||
|
||||
## Быстрый запуск (dev override)
|
||||
|
||||
1. Собрать бинарник провайдера:
|
||||
|
||||
```bash
|
||||
cd terraform/provider
|
||||
go build -o bin/terraform-provider-fission .
|
||||
```
|
||||
|
||||
2. Положить бинарник в локальный каталог dev plugins:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.terraform.d/dev-plugins/Nail/fission
|
||||
cp terraform/provider/bin/terraform-provider-fission ~/.terraform.d/dev-plugins/Nail/fission/
|
||||
```
|
||||
|
||||
3. Создать файл Terraform CLI config (пример `/tmp/terraformrc-fission`):
|
||||
|
||||
```hcl
|
||||
provider_installation {
|
||||
dev_overrides {
|
||||
"nail/fission" = "/home/naeel/.terraform.d/dev-plugins/Nail/fission"
|
||||
}
|
||||
direct {}
|
||||
}
|
||||
```
|
||||
|
||||
4. Запустить пример:
|
||||
|
||||
```bash
|
||||
cd examples/hello-python
|
||||
TF_CLI_CONFIG_FILE=/tmp/terraformrc-fission terraform plan
|
||||
TF_CLI_CONFIG_FILE=/tmp/terraformrc-fission terraform apply -auto-approve
|
||||
```
|
||||
|
||||
## Ограничения MVP
|
||||
|
||||
- `fission_package.source_dir` в текущей реализации ожидает файл `main.py` в корне указанной директории.
|
||||
- Реализация ориентирована на Python flow через `spec.deployment.literal`.
|
||||
- Дополнительные сценарии сборки (`build_command`, многофайловые архивы и т.д.) будут расширены в следующих этапах.
|
||||
@@ -68,3 +68,43 @@
|
||||
|
||||
### Следующий шаг
|
||||
- Добавить пример `examples/hello-python/main.tf` и выполнить smoke Terraform apply.
|
||||
|
||||
### Обновление этапа (пример Terraform)
|
||||
- Создан пример `examples/hello-python/main.tf` с использованием ресурсов:
|
||||
- `fission_environment`
|
||||
- `fission_package`
|
||||
- `fission_function`
|
||||
- `fission_http_trigger`
|
||||
- Добавлен код функции `examples/hello-python/code/main.py`.
|
||||
|
||||
### Следующий шаг
|
||||
- Подготовить локальную установку бинарника провайдера для `terraform init` и выполнить smoke `terraform apply`.
|
||||
|
||||
### Обновление этапа (smoke Terraform apply)
|
||||
- Собран бинарник провайдера и подключен через Terraform `dev_overrides`.
|
||||
- Прогнан `terraform plan` для `examples/hello-python`: план успешен, 4 ресурса к созданию.
|
||||
- Прогнан `terraform apply` и выполнена функциональная проверка.
|
||||
|
||||
Исправленные баги по факту smoke:
|
||||
- `fission_package`: после Create computed-поля (`build_status`, `build_log`) могли оставаться `unknown`.
|
||||
- Исправлено формирование state в `unstructuredToPackageModel`.
|
||||
- `fission_function`: CRD требовал обязательный `spec.InvokeStrategy`.
|
||||
- Добавлен `InvokeStrategy` в payload функции.
|
||||
- `fission_http_trigger`: несогласованность state (`create_ingress` null -> false).
|
||||
- В schema добавлены `Computed` + `Default(false)` для `create_ingress`.
|
||||
- Для `methods` добавлен `Computed`.
|
||||
|
||||
Дополнительно:
|
||||
- Для текущего python MVP `fission_package` переведен на `spec.deployment.literal` (как у рабочего CLI-пакета).
|
||||
- Проверка endpoint после apply:
|
||||
- `GET /tf-hello` -> `hello from fission via terraform`.
|
||||
|
||||
### Следующий шаг
|
||||
- Добавить README по запуску dev override и оговорить ограничения текущего MVP (`source_dir` ожидает `main.py`).
|
||||
|
||||
### Обновление этапа (README)
|
||||
- Добавлен `README.md` в корень репозитория.
|
||||
- Описаны:
|
||||
- список реализованных ресурсов MVP
|
||||
- запуск через Terraform dev override
|
||||
- текущие ограничения MVP
|
||||
|
||||
@@ -100,3 +100,40 @@
|
||||
- преобразование terraform model <-> CRD payload
|
||||
- Сборка `go build ./...` успешна.
|
||||
- Следующий этап: пример Terraform и smoke apply.
|
||||
|
||||
---
|
||||
|
||||
Агент: GitHub Copilot
|
||||
Модель: GPT-5.3-Codex
|
||||
|
||||
## План (до действий, этап 6)
|
||||
1. Добавить пример Terraform под текущий набор реализованных ресурсов.
|
||||
2. Добавить простой код функции для примера.
|
||||
3. Зафиксировать изменения в документации.
|
||||
|
||||
## Результат (после действий, этап 6)
|
||||
- Создан `examples/hello-python/main.tf`.
|
||||
- Создан `examples/hello-python/code/main.py`.
|
||||
- Пример покрывает цепочку Environment -> Package -> Function -> HTTPTrigger.
|
||||
- Следующий этап: установить бинарник провайдера в локальный plugin cache и прогнать smoke `terraform init/plan/apply`.
|
||||
|
||||
---
|
||||
|
||||
Агент: GitHub Copilot
|
||||
Модель: GPT-5.3-Codex
|
||||
|
||||
## План (до действий, этап 7)
|
||||
1. Выполнить smoke `terraform plan/apply` с dev override.
|
||||
2. Проверить рабочий HTTP endpoint функции.
|
||||
3. Зафиксировать найденные дефекты провайдера и исправить их.
|
||||
|
||||
## Результат (после действий, этап 7)
|
||||
- Выполнен `terraform plan` для `examples/hello-python`.
|
||||
- Выполнен `terraform apply`.
|
||||
- Найдены и исправлены дефекты:
|
||||
- `fission_package`: computed-поля могли оставаться unknown после Create.
|
||||
- `fission_function`: отсутствовал обязательный `spec.InvokeStrategy`.
|
||||
- `fission_http_trigger`: несогласованность состояния для `create_ingress`.
|
||||
- После фиксов выполнен повторный apply и проверка endpoint:
|
||||
- `https://fission.kube5s.ru/tf-hello` -> `hello from fission via terraform`.
|
||||
- MVP-цепочка ресурсов работает end-to-end на кластере.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
def main():
|
||||
return "hello from fission via terraform"
|
||||
@@ -0,0 +1,43 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
fission = {
|
||||
source = "nail/fission"
|
||||
version = "~> 0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "fission" {
|
||||
kubeconfig_path = "/home/naeel/.kube/config"
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
resource "fission_environment" "python" {
|
||||
name = "tf-python-env"
|
||||
image = "ghcr.io/fission/python-env"
|
||||
version = 3
|
||||
}
|
||||
|
||||
resource "fission_package" "hello" {
|
||||
name = "tf-hello-pkg"
|
||||
environment = fission_environment.python.name
|
||||
source_dir = "${path.module}/code"
|
||||
}
|
||||
|
||||
resource "fission_function" "hello" {
|
||||
name = "tf-hello-fn"
|
||||
environment = fission_environment.python.name
|
||||
package_name = fission_package.hello.name
|
||||
entrypoint = "main.main"
|
||||
}
|
||||
|
||||
resource "fission_http_trigger" "hello" {
|
||||
name = "tf-hello-route"
|
||||
function = fission_function.hello.name
|
||||
url = "/tf-hello"
|
||||
methods = ["GET"]
|
||||
}
|
||||
|
||||
output "hello_url" {
|
||||
value = "https://fission.kube5s.ru/tf-hello"
|
||||
}
|
||||
@@ -198,6 +198,12 @@ func functionToUnstructured(model functionResourceModel, namespace string) *unst
|
||||
"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(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
@@ -65,11 +66,14 @@ func (r *HTTPTriggerResource) Schema(_ context.Context, _ resource.SchemaRequest
|
||||
},
|
||||
"methods": schema.ListAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
Description: "HTTP методы trigger, например [\"GET\", \"POST\"].",
|
||||
},
|
||||
"create_ingress": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
Description: "Включает создание ingress правила в Fission.",
|
||||
},
|
||||
"host": schema.StringAttribute{
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -137,13 +133,13 @@ func (r *PackageResource) Create(ctx context.Context, req resource.CreateRequest
|
||||
return
|
||||
}
|
||||
|
||||
archiveBytes, err := loadPackageArchive(plan.SourceDir.ValueString(), plan.CodePath.ValueString())
|
||||
literalBytes, err := loadPackageLiteral(plan.SourceDir.ValueString(), plan.CodePath.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка чтения исходного кода пакета", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
packageObject := packageToUnstructured(plan, namespace, archiveBytes)
|
||||
packageObject := packageToUnstructured(plan, namespace, literalBytes)
|
||||
createdPackage, err := r.client.CreatePackage(ctx, packageObject)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка создания Fission Package", err.Error())
|
||||
@@ -191,7 +187,7 @@ func (r *PackageResource) Update(ctx context.Context, req resource.UpdateRequest
|
||||
return
|
||||
}
|
||||
|
||||
archiveBytes, err := loadPackageArchive(plan.SourceDir.ValueString(), plan.CodePath.ValueString())
|
||||
literalBytes, err := loadPackageLiteral(plan.SourceDir.ValueString(), plan.CodePath.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка чтения исходного кода пакета", err.Error())
|
||||
return
|
||||
@@ -203,7 +199,7 @@ func (r *PackageResource) Update(ctx context.Context, req resource.UpdateRequest
|
||||
return
|
||||
}
|
||||
|
||||
packageObject := packageToUnstructured(plan, namespace, archiveBytes)
|
||||
packageObject := packageToUnstructured(plan, namespace, literalBytes)
|
||||
packageObject.SetResourceVersion(existingPackage.GetResourceVersion())
|
||||
|
||||
updatedPackage, err := r.client.UpdatePackage(ctx, packageObject)
|
||||
@@ -257,75 +253,29 @@ func resolveNamespace(resourceNamespace types.String, providerNamespace string)
|
||||
return namespace
|
||||
}
|
||||
|
||||
// loadPackageArchive читает zip из code_path или собирает zip из source_dir.
|
||||
func loadPackageArchive(sourceDir, codePath string) ([]byte, error) {
|
||||
// loadPackageLiteral читает bytes для spec.deployment.literal.
|
||||
func loadPackageLiteral(sourceDir, codePath string) ([]byte, error) {
|
||||
if sourceDir != "" {
|
||||
return zipDirectory(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)
|
||||
}
|
||||
|
||||
archiveBytes, err := os.ReadFile(codePath)
|
||||
return mainFileBytes, nil
|
||||
}
|
||||
|
||||
literalBytes, err := os.ReadFile(codePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read code_path %q: %w", codePath, err)
|
||||
}
|
||||
|
||||
return archiveBytes, nil
|
||||
}
|
||||
|
||||
// zipDirectory собирает zip-архив из директории для передачи в spec.source.literal.
|
||||
func zipDirectory(sourceDir string) ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
zipWriter := zip.NewWriter(buffer)
|
||||
|
||||
walkError := filepath.WalkDir(sourceDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
relativePath, err := filepath.Rel(sourceDir, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build relative path for %q: %w", path, err)
|
||||
}
|
||||
|
||||
zipPath := filepath.ToSlash(relativePath)
|
||||
fileWriter, err := zipWriter.Create(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create zip entry %q: %w", zipPath, err)
|
||||
}
|
||||
|
||||
fileHandle, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source file %q: %w", path, err)
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fileWriter, fileHandle); err != nil {
|
||||
_ = fileHandle.Close()
|
||||
return fmt.Errorf("write zip entry %q: %w", zipPath, err)
|
||||
}
|
||||
|
||||
if err = fileHandle.Close(); err != nil {
|
||||
return fmt.Errorf("close source file %q: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if walkError != nil {
|
||||
_ = zipWriter.Close()
|
||||
return nil, fmt.Errorf("zip source_dir %q: %w", sourceDir, walkError)
|
||||
}
|
||||
|
||||
if err := zipWriter.Close(); err != nil {
|
||||
return nil, fmt.Errorf("finalize zip archive: %w", err)
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
return literalBytes, nil
|
||||
}
|
||||
|
||||
// packageToUnstructured преобразует Terraform model в Kubernetes CRD payload.
|
||||
func packageToUnstructured(model packageResourceModel, namespace string, archiveBytes []byte) *unstructured.Unstructured {
|
||||
literalSource := base64.StdEncoding.EncodeToString(archiveBytes)
|
||||
func packageToUnstructured(model packageResourceModel, namespace string, literalBytes []byte) *unstructured.Unstructured {
|
||||
literalSource := base64.StdEncoding.EncodeToString(literalBytes)
|
||||
|
||||
object := map[string]interface{}{
|
||||
"apiVersion": "fission.io/v1",
|
||||
@@ -335,14 +285,15 @@ func packageToUnstructured(model packageResourceModel, namespace string, archive
|
||||
"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{}{
|
||||
"type": "literal",
|
||||
"literal": literalSource,
|
||||
},
|
||||
"source": map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -360,11 +311,19 @@ func unstructuredToPackageModel(packageObject *unstructured.Unstructured, base p
|
||||
buildStatus, _, _ := unstructured.NestedString(packageObject.Object, "status", "buildstatus")
|
||||
buildLog, _, _ := unstructured.NestedString(packageObject.Object, "status", "buildlog")
|
||||
|
||||
state := base
|
||||
state.Name = types.StringValue(packageObject.GetName())
|
||||
state.Namespace = types.StringValue(packageObject.GetNamespace())
|
||||
state.ID = types.StringValue(fmt.Sprintf("%s/%s", packageObject.GetNamespace(), packageObject.GetName()))
|
||||
state.UID = types.StringValue(string(packageObject.GetUID()))
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user