feat: provider v0.1.10 — source_dir (zip внутри провайдера), убран archive провайдер из всех примеров

This commit is contained in:
“Naeel”
2026-03-09 17:45:45 +04:00
parent 6e236e5998
commit 5f913e9f25
16 changed files with 148 additions and 170 deletions
+7 -2
View File
@@ -194,14 +194,19 @@ func (c *Client) UploadCode(ctx context.Context, ns, name, zipPath string) error
return fmt.Errorf("open zip %q: %w", zipPath, err)
}
defer f.Close()
return c.UploadCodeReader(ctx, ns, name, filepath.Base(zipPath), f)
}
// UploadCodeReader — загружает код из произвольного io.Reader (например in-memory zip).
// filename используется только как имя файла в multipart-форме.
func (c *Client) UploadCodeReader(ctx context.Context, ns, name, filename string, r io.Reader) error {
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
fw, err := mw.CreateFormFile("code", filepath.Base(zipPath))
fw, err := mw.CreateFormFile("code", filename)
if err != nil {
return fmt.Errorf("create form file: %w", err)
}
if _, err := io.Copy(fw, f); err != nil {
if _, err := io.Copy(fw, r); err != nil {
return fmt.Errorf("copy zip: %w", err)
}
mw.Close()
@@ -17,8 +17,16 @@
package resources
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"time"
"terraform-provider-sless/internal/client"
@@ -57,8 +65,11 @@ type FunctionModel struct {
TimeoutSec types.Int64 `tfsdk:"timeout_sec"`
EnvVars types.Map `tfsdk:"env_vars"`
CodePath types.String `tfsdk:"code_path"`
// code_hash — sha256/md5 zip-файла, пользователь задаёт через filemd5().
// Изменение hash → провайдер перезагружает код и запускает пересборку.
// source_dir — директория с исходниками. Провайдер сам упакует в zip и загрузит.
// Взаимоисключающе с code_path. Не требует hashicorp/archive.
SourceDir types.String `tfsdk:"source_dir"`
// code_hash — sha256 содержимого. При code_path: задаётся вручную.
// При source_dir: провайдер вычисляет сам и хранит в state для детекции изменений.
CodeHash types.String `tfsdk:"code_hash"`
// build_timeout_sec — максимальное ожидание kaniko-сборки. Дефолт 300 сек.
BuildTimeoutSec types.Int64 `tfsdk:"build_timeout_sec"`
@@ -117,9 +128,15 @@ func (r *FunctionResource) Schema(_ context.Context, _ resource.SchemaRequest, r
"code_path": schema.StringAttribute{
Optional: true,
},
// code_hash — задаётся через filemd5("./handler.zip") в .tf
// source_dir — путь к директории с исходниками. Провайдер сам соберёт zip без archive provider.
"source_dir": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Путь к директории с исходным кодом. Провайдер сам упакует в zip. Не нужен archive provider.",
},
// code_hash — при code_path: задаётся вручную; при source_dir: вычисляется автоматически.
"code_hash": schema.StringAttribute{
Optional: true,
Computed: true,
},
// build_timeout_sec — таймаут ожидания kaniko-сборки. Дефолт 300 сек (5 мин).
// Увеличь если функция с тяжёлыми зависимостями (например torch).
@@ -187,11 +204,29 @@ func (r *FunctionResource) Create(ctx context.Context, req resource.CreateReques
return
}
if !plan.CodePath.IsNull() && plan.CodePath.ValueString() != "" {
var codeUploaded bool
if !plan.SourceDir.IsNull() && plan.SourceDir.ValueString() != "" {
// source_dir: провайдер сам собирает zip и вычисляет hash
zipData, hash, err := zipDir(plan.SourceDir.ValueString())
if err != nil {
resp.Diagnostics.AddError("zip source_dir", err.Error())
return
}
if err := r.client.UploadCodeReader(ctx, ns, fn.Name, "code.zip", bytes.NewReader(zipData)); err != nil {
resp.Diagnostics.AddError("upload code", err.Error())
return
}
plan.CodeHash = types.StringValue(hash)
codeUploaded = true
} else if !plan.CodePath.IsNull() && plan.CodePath.ValueString() != "" {
// code_path: старый путь, для обратной совместимости
if err := r.client.UploadCode(ctx, ns, fn.Name, plan.CodePath.ValueString()); err != nil {
resp.Diagnostics.AddError("upload code", err.Error())
return
}
codeUploaded = true
}
if codeUploaded {
buildSec := plan.BuildTimeoutSec.ValueInt64()
if buildSec <= 0 {
buildSec = defaultBuildTimeoutSec
@@ -257,13 +292,32 @@ func (r *FunctionResource) Update(ctx context.Context, req resource.UpdateReques
return
}
// Перезагружаем код только если code_hash изменился
// Это предотвращает ненужные пересборки при apply без изменений кода
if !plan.CodeHash.Equal(state.CodeHash) && !plan.CodePath.IsNull() && plan.CodePath.ValueString() != "" {
var codeUploaded bool
if !plan.SourceDir.IsNull() && plan.SourceDir.ValueString() != "" {
// source_dir: пересчитываем hash и загружаем только если изменился
zipData, hash, err := zipDir(plan.SourceDir.ValueString())
if err != nil {
resp.Diagnostics.AddError("zip source_dir", err.Error())
return
}
newHash := types.StringValue(hash)
if !newHash.Equal(state.CodeHash) {
if err := r.client.UploadCodeReader(ctx, ns, name, "code.zip", bytes.NewReader(zipData)); err != nil {
resp.Diagnostics.AddError("upload code", err.Error())
return
}
codeUploaded = true
}
plan.CodeHash = newHash
} else if !plan.CodeHash.Equal(state.CodeHash) && !plan.CodePath.IsNull() && plan.CodePath.ValueString() != "" {
// code_path: загружаем только если code_hash изменился вручную
if err := r.client.UploadCode(ctx, ns, name, plan.CodePath.ValueString()); err != nil {
resp.Diagnostics.AddError("upload code", err.Error())
return
}
codeUploaded = true
}
if codeUploaded {
buildSec := plan.BuildTimeoutSec.ValueInt64()
if buildSec <= 0 {
buildSec = defaultBuildTimeoutSec
@@ -306,6 +360,7 @@ func fnToModel(plan FunctionModel, fn *client.FunctionResponse) FunctionModel {
TimeoutSec: types.Int64Value(int64(fn.TimeoutSec)),
EnvVars: plan.EnvVars, // API возвращает null для пустого map — берём из plan
CodePath: plan.CodePath,
SourceDir: plan.SourceDir,
CodeHash: plan.CodeHash,
BuildTimeoutSec: buildTimeoutSec,
Phase: types.StringValue(fn.Phase),
@@ -313,6 +368,53 @@ func fnToModel(plan FunctionModel, fn *client.FunctionResponse) FunctionModel {
}
}
// zipDir упаковывает директорию dir в zip-архив (in-memory) и возвращает байты + SHA256 содержимого.
// Сортирует файлы по пути для детерминированного хеша — одинаковый код даёт одинаковый hash.
func zipDir(dir string) ([]byte, string, error) {
var paths []string
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
paths = append(paths, path)
}
return nil
})
if err != nil {
return nil, "", fmt.Errorf("walk dir %q: %w", dir, err)
}
sort.Strings(paths)
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
h := sha256.New()
for _, path := range paths {
rel, err := filepath.Rel(dir, path)
if err != nil {
return nil, "", err
}
data, err := os.ReadFile(path)
if err != nil {
return nil, "", fmt.Errorf("read %q: %w", path, err)
}
fw, err := zw.Create(rel)
if err != nil {
return nil, "", fmt.Errorf("zip create %q: %w", rel, err)
}
if _, err := fw.Write(data); err != nil {
return nil, "", fmt.Errorf("zip write %q: %w", rel, err)
}
// хешируем путь + содержимое для надёжной детекции изменений
h.Write([]byte(rel))
h.Write(data)
}
if err := zw.Close(); err != nil {
return nil, "", fmt.Errorf("zip close: %w", err)
}
return buf.Bytes(), hex.EncodeToString(h.Sum(nil)), nil
}
// mapToStringMap конвертирует types.Map → map[string]string.
func mapToStringMap(ctx context.Context, m types.Map) (map[string]string, diag.Diagnostics) {
if m.IsNull() || m.IsUnknown() {