feat: provider v0.1.10 — source_dir (zip внутри провайдера), убран archive провайдер из всех примеров
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user