feat: sless_service CRD + ServiceReconciler, RBAC fix, split postgres/functions.tf, operator v0.1.41
This commit is contained in:
@@ -547,3 +547,170 @@ func (c *Client) WaitJobDone(ctx context.Context, ns, name string, timeout time.
|
||||
}
|
||||
return nil, fmt.Errorf("timeout waiting for job %s/%s to complete", ns, name)
|
||||
}
|
||||
|
||||
// --- Service CRUD (sless_service — long-running Deployment + URL) ---
|
||||
|
||||
// ServiceRequest — тело POST/PUT /v1/namespaces/{ns}/services[/{name}]
|
||||
type ServiceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Runtime string `json:"runtime"`
|
||||
Entrypoint string `json:"entrypoint,omitempty"`
|
||||
MemoryMB int32 `json:"memory_mb,omitempty"`
|
||||
TimeoutSec int32 `json:"timeout_sec,omitempty"`
|
||||
Env map[string]string `json:"env_vars,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceResponse — ответ GET /v1/namespaces/{ns}/services/{name}
|
||||
// URL — ключевое поле: заполняется оператором после деплоя Deployment+Ingress.
|
||||
type ServiceResponse struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Runtime string `json:"runtime"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
MemoryMB int32 `json:"memory_mb"`
|
||||
TimeoutSec int32 `json:"timeout_sec"`
|
||||
Env map[string]string `json:"env_vars"`
|
||||
S3Bucket string `json:"s3_bucket"`
|
||||
S3Key string `json:"s3_key"`
|
||||
Phase string `json:"phase"`
|
||||
ImageRef string `json:"image_ref"`
|
||||
URL string `json:"url"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// CreateService — POST /v1/namespaces/{ns}/services → 201
|
||||
func (c *Client) CreateService(ctx context.Context, ns string, req ServiceRequest) (*ServiceResponse, error) {
|
||||
url := fmt.Sprintf("%s/v1/namespaces/%s/services", c.endpoint, ns)
|
||||
resp, err := c.doJSON(ctx, http.MethodPost, url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("create service: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
var svc ServiceResponse
|
||||
return &svc, json.NewDecoder(resp.Body).Decode(&svc)
|
||||
}
|
||||
|
||||
// GetService — GET /v1/namespaces/{ns}/services/{name} → nil если 404
|
||||
func (c *Client) GetService(ctx context.Context, ns, name string) (*ServiceResponse, error) {
|
||||
url := fmt.Sprintf("%s/v1/namespaces/%s/services/%s", c.endpoint, ns, name)
|
||||
resp, err := c.doJSON(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("get service: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
var svc ServiceResponse
|
||||
return &svc, json.NewDecoder(resp.Body).Decode(&svc)
|
||||
}
|
||||
|
||||
// UpdateService — PUT /v1/namespaces/{ns}/services/{name} → 200
|
||||
func (c *Client) UpdateService(ctx context.Context, ns, name string, req ServiceRequest) (*ServiceResponse, error) {
|
||||
url := fmt.Sprintf("%s/v1/namespaces/%s/services/%s", c.endpoint, ns, name)
|
||||
resp, err := c.doJSON(ctx, http.MethodPut, url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("update service: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
var svc ServiceResponse
|
||||
return &svc, json.NewDecoder(resp.Body).Decode(&svc)
|
||||
}
|
||||
|
||||
// DeleteService — DELETE /v1/namespaces/{ns}/services/{name} → 204
|
||||
func (c *Client) DeleteService(ctx context.Context, ns, name string) error {
|
||||
url := fmt.Sprintf("%s/v1/namespaces/%s/services/%s", c.endpoint, ns, name)
|
||||
resp, err := c.doJSON(ctx, http.MethodDelete, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("delete service: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadServiceCode — POST /v1/namespaces/{ns}/services/{name}/upload (multipart, field=code)
|
||||
// После вызова оператор начинает kaniko-сборку и затем деплоит Deployment.
|
||||
func (c *Client) UploadServiceCode(ctx context.Context, ns, name, zipPath string) error {
|
||||
f, err := os.Open(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open zip %q: %w", zipPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
return c.UploadServiceCodeReader(ctx, ns, name, filepath.Base(zipPath), f)
|
||||
}
|
||||
|
||||
// UploadServiceCodeReader — загружает код сервиса из произвольного io.Reader.
|
||||
func (c *Client) UploadServiceCodeReader(ctx context.Context, ns, name, filename string, r io.Reader) error {
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
fw, err := mw.CreateFormFile("code", filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create form file: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(fw, r); err != nil {
|
||||
return fmt.Errorf("copy zip: %w", err)
|
||||
}
|
||||
mw.Close()
|
||||
|
||||
url := fmt.Sprintf("%s/v1/namespaces/%s/services/%s/upload", c.endpoint, ns, name)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("upload service code: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitServiceReady опрашивает сервис каждые 5 секунд пока phase != Ready/Failed.
|
||||
// Нужен после UploadServiceCode — kaniko-сборка + деплой занимают ~1-2 минуты.
|
||||
func (c *Client) WaitServiceReady(ctx context.Context, ns, name string, timeout time.Duration) (*ServiceResponse, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
svc, err := c.GetService(ctx, ns, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if svc == nil {
|
||||
return nil, fmt.Errorf("service %s/%s not found while waiting", ns, name)
|
||||
}
|
||||
switch svc.Phase {
|
||||
case "Ready":
|
||||
return svc, nil
|
||||
case "Failed":
|
||||
return nil, fmt.Errorf("service build failed: %s", svc.Message)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("timeout waiting for service %s/%s to become Ready", ns, name)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 2026-03-11
|
||||
// 2026-03-20 (function-service-split: добавлен NewServiceResource)
|
||||
// provider.go — описание провайдера sless для Terraform.
|
||||
//
|
||||
// Архитектура инициализации (Configure):
|
||||
@@ -167,6 +167,7 @@ func (p *SlessProvider) Configure(ctx context.Context, req provider.ConfigureReq
|
||||
func (p *SlessProvider) Resources(_ context.Context) []func() resource.Resource {
|
||||
return []func() resource.Resource{
|
||||
resources.NewFunctionResource,
|
||||
resources.NewServiceResource,
|
||||
resources.NewTriggerResource,
|
||||
resources.NewJobResource,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
// Создано: 2026-03-20 (function-service-split)
|
||||
// service_resource.go — Terraform ресурс sless_service.
|
||||
// sless_service = long-running Deployment + постоянный URL.
|
||||
// Отличие от sless_function: у Service есть url в state (заполняет оператор после деплоя).
|
||||
//
|
||||
// Lifecycle:
|
||||
// Create: POST /services → если code_path/source_dir задан: upload zip → WaitServiceReady
|
||||
// Read: GET /services/{name} → sync state (включая url)
|
||||
// Update: PUT /services/{name} → если code_hash изменился: upload zip → WaitServiceReady
|
||||
// Delete: DELETE /services/{name}
|
||||
//
|
||||
// code_hash, source_dir, build_timeout_sec — логика идентична FunctionResource.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"terraform-provider-sless/internal/client"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"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/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var _ resource.Resource = &ServiceResource{}
|
||||
var _ resource.ResourceWithModifyPlan = &ServiceResource{}
|
||||
|
||||
type ServiceResource struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
func NewServiceResource() resource.Resource {
|
||||
return &ServiceResource{}
|
||||
}
|
||||
|
||||
// ServiceModel — модель состояния terraform для sless_service.
|
||||
// Добавлено поле URL — заполняется оператором, только для чтения.
|
||||
type ServiceModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Runtime types.String `tfsdk:"runtime"`
|
||||
Entrypoint types.String `tfsdk:"entrypoint"`
|
||||
MemoryMB types.Int64 `tfsdk:"memory_mb"`
|
||||
TimeoutSec types.Int64 `tfsdk:"timeout_sec"`
|
||||
EnvVars types.Map `tfsdk:"env_vars"`
|
||||
CodePath types.String `tfsdk:"code_path"`
|
||||
SourceDir types.String `tfsdk:"source_dir"`
|
||||
CodeHash types.String `tfsdk:"code_hash"`
|
||||
BuildTimeoutSec types.Int64 `tfsdk:"build_timeout_sec"`
|
||||
Phase types.String `tfsdk:"phase"`
|
||||
ImageRef types.String `tfsdk:"image_ref"`
|
||||
// URL — публичный URL сервиса. Заполняется оператором после деплоя.
|
||||
// Доступен сразу без sless_trigger (в отличие от sless_function).
|
||||
URL types.String `tfsdk:"url"`
|
||||
}
|
||||
|
||||
func (r *ServiceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_service"
|
||||
}
|
||||
|
||||
func (r *ServiceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: "Long-running serverless service с постоянным URL. Разворачивается как k8s Deployment + Ingress.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"runtime": schema.StringAttribute{
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.OneOf("nodejs20", "python3.11", "go1.23"),
|
||||
},
|
||||
},
|
||||
"entrypoint": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"memory_mb": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Validators: []validator.Int64{
|
||||
int64validator.Between(1, 4096),
|
||||
},
|
||||
},
|
||||
// timeout_sec для сервиса = прокси-таймаут invoke (не таймаут Job).
|
||||
// При длинных операциях (batch) увеличивай.
|
||||
"timeout_sec": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Validators: []validator.Int64{
|
||||
int64validator.Between(1, 900),
|
||||
},
|
||||
},
|
||||
"env_vars": schema.MapAttribute{
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
},
|
||||
"code_path": schema.StringAttribute{
|
||||
Optional: true,
|
||||
},
|
||||
"source_dir": schema.StringAttribute{
|
||||
Optional: true,
|
||||
MarkdownDescription: "Путь к директории с исходным кодом. Провайдер сам упакует в zip.",
|
||||
},
|
||||
"code_hash": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"build_timeout_sec": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
MarkdownDescription: "Таймаут ожидания сборки и деплоя в секундах. По умолчанию 300.",
|
||||
},
|
||||
"phase": schema.StringAttribute{
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"image_ref": schema.StringAttribute{
|
||||
Computed: true,
|
||||
},
|
||||
// url — только для чтения, вычисляется оператором.
|
||||
// Используй в outputs или как зависимость для других ресурсов.
|
||||
"url": schema.StringAttribute{
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ServiceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.Client)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"unexpected provider data",
|
||||
fmt.Sprintf("expected *client.Client, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan ServiceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
ns := r.client.Namespace
|
||||
envVars, d := mapToStringMap(ctx, plan.EnvVars)
|
||||
resp.Diagnostics.Append(d...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
svc, err := r.client.CreateService(ctx, ns, client.ServiceRequest{
|
||||
Name: plan.Name.ValueString(),
|
||||
Runtime: plan.Runtime.ValueString(),
|
||||
Entrypoint: plan.Entrypoint.ValueString(),
|
||||
MemoryMB: int32(plan.MemoryMB.ValueInt64()),
|
||||
TimeoutSec: int32(plan.TimeoutSec.ValueInt64()),
|
||||
Env: envVars,
|
||||
})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("create service", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var codeUploaded bool
|
||||
if !plan.SourceDir.IsNull() && plan.SourceDir.ValueString() != "" {
|
||||
zipData, hash, err := zipDir(plan.SourceDir.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("zip source_dir", err.Error())
|
||||
return
|
||||
}
|
||||
if err := r.client.UploadServiceCodeReader(ctx, ns, svc.Name, "code.zip", bytes.NewReader(zipData)); err != nil {
|
||||
resp.Diagnostics.AddError("upload service code", err.Error())
|
||||
return
|
||||
}
|
||||
plan.CodeHash = types.StringValue(hash)
|
||||
codeUploaded = true
|
||||
} else if !plan.CodePath.IsNull() && plan.CodePath.ValueString() != "" {
|
||||
if err := r.client.UploadServiceCode(ctx, ns, svc.Name, plan.CodePath.ValueString()); err != nil {
|
||||
resp.Diagnostics.AddError("upload service code", err.Error())
|
||||
return
|
||||
}
|
||||
codeUploaded = true
|
||||
}
|
||||
if codeUploaded {
|
||||
buildSec := plan.BuildTimeoutSec.ValueInt64()
|
||||
if buildSec <= 0 {
|
||||
buildSec = defaultBuildTimeoutSec
|
||||
}
|
||||
svc, err = r.client.WaitServiceReady(ctx, ns, svc.Name, time.Duration(buildSec)*time.Second)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("waiting for service ready", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, svcToModel(plan, svc))...)
|
||||
}
|
||||
|
||||
func (r *ServiceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state ServiceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
svc, err := r.client.GetService(ctx, r.client.Namespace, state.Name.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("read service", err.Error())
|
||||
return
|
||||
}
|
||||
if svc == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, svcToModel(state, svc))...)
|
||||
}
|
||||
|
||||
func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan, state ServiceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
ns := r.client.Namespace
|
||||
name := plan.Name.ValueString()
|
||||
|
||||
envVars, d := mapToStringMap(ctx, plan.EnvVars)
|
||||
resp.Diagnostics.Append(d...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
svc, err := r.client.UpdateService(ctx, ns, name, client.ServiceRequest{
|
||||
Name: name,
|
||||
Runtime: plan.Runtime.ValueString(),
|
||||
Entrypoint: plan.Entrypoint.ValueString(),
|
||||
MemoryMB: int32(plan.MemoryMB.ValueInt64()),
|
||||
TimeoutSec: int32(plan.TimeoutSec.ValueInt64()),
|
||||
Env: envVars,
|
||||
})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("update service", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var codeUploaded bool
|
||||
if !plan.SourceDir.IsNull() && plan.SourceDir.ValueString() != "" {
|
||||
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.UploadServiceCodeReader(ctx, ns, name, "code.zip", bytes.NewReader(zipData)); err != nil {
|
||||
resp.Diagnostics.AddError("upload service code", err.Error())
|
||||
return
|
||||
}
|
||||
codeUploaded = true
|
||||
}
|
||||
plan.CodeHash = newHash
|
||||
} else if !plan.CodeHash.Equal(state.CodeHash) && !plan.CodePath.IsNull() && plan.CodePath.ValueString() != "" {
|
||||
if err := r.client.UploadServiceCode(ctx, ns, name, plan.CodePath.ValueString()); err != nil {
|
||||
resp.Diagnostics.AddError("upload service code", err.Error())
|
||||
return
|
||||
}
|
||||
codeUploaded = true
|
||||
}
|
||||
if codeUploaded {
|
||||
buildSec := plan.BuildTimeoutSec.ValueInt64()
|
||||
if buildSec <= 0 {
|
||||
buildSec = defaultBuildTimeoutSec
|
||||
}
|
||||
svc, err = r.client.WaitServiceReady(ctx, ns, name, time.Duration(buildSec)*time.Second)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("waiting for service ready", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, svcToModel(plan, svc))...)
|
||||
}
|
||||
|
||||
func (r *ServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state ServiceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.client.DeleteService(ctx, r.client.Namespace, state.Name.ValueString()); err != nil {
|
||||
resp.Diagnostics.AddError("delete service", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyPlan вычисляет hash директории source_dir на фазе plan.
|
||||
// Идентична FunctionResource.ModifyPlan — без этого terraform не видит изменения кода.
|
||||
func (r *ServiceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||||
if req.Plan.Raw.IsNull() {
|
||||
return
|
||||
}
|
||||
var plan ServiceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if plan.SourceDir.IsNull() || plan.SourceDir.ValueString() == "" {
|
||||
return
|
||||
}
|
||||
_, hash, err := zipDir(plan.SourceDir.ValueString())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
plan.CodeHash = types.StringValue(hash)
|
||||
resp.Diagnostics.Append(resp.Plan.Set(ctx, plan)...)
|
||||
}
|
||||
|
||||
// svcToModel конвертирует API-ответ + plan → state модель.
|
||||
// URL берётся из API (заполняется оператором), остальные read-only поля — тоже.
|
||||
func svcToModel(plan ServiceModel, svc *client.ServiceResponse) ServiceModel {
|
||||
buildTimeoutSec := plan.BuildTimeoutSec
|
||||
if buildTimeoutSec.IsNull() || buildTimeoutSec.IsUnknown() || buildTimeoutSec.ValueInt64() <= 0 {
|
||||
buildTimeoutSec = types.Int64Value(defaultBuildTimeoutSec)
|
||||
}
|
||||
return ServiceModel{
|
||||
Name: types.StringValue(svc.Name),
|
||||
Runtime: types.StringValue(svc.Runtime),
|
||||
Entrypoint: types.StringValue(svc.Entrypoint),
|
||||
MemoryMB: types.Int64Value(int64(svc.MemoryMB)),
|
||||
TimeoutSec: types.Int64Value(int64(svc.TimeoutSec)),
|
||||
EnvVars: plan.EnvVars,
|
||||
CodePath: plan.CodePath,
|
||||
SourceDir: plan.SourceDir,
|
||||
CodeHash: plan.CodeHash,
|
||||
BuildTimeoutSec: buildTimeoutSec,
|
||||
Phase: types.StringValue(svc.Phase),
|
||||
ImageRef: types.StringValue(svc.ImageRef),
|
||||
URL: types.StringValue(svc.URL),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user