Files
tf_provider/TOOLS/resource-generator/internal/templates/instance.go
T

688 lines
29 KiB
Go

package templates
const Instance = `package resources_gen
import (
"context"
{{- if .NeedsStringsImport }}
"strings"
{{- end }}
{{- if .NeedsFmtImport }}
"fmt"
{{- end }}
"terraform-provider-nubes/internal/core"
"terraform-provider-nubes/internal/resources_core"
"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/booldefault"
{{- if .NeedsInt64Default }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
{{- end }}
{{- if .NeedsStringDefault }}
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
{{- end }}
"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"
)
// Code generated by tools/gen_v2. DO NOT EDIT.
// Service: {{.Name}}
// Service ID: {{.ServiceID}}
var _ resource.Resource = &{{ToCamel .Name}}Resource{}
var _ resource.ResourceWithModifyPlan = &{{ToCamel .Name}}Resource{}
var _ resource.ResourceWithImportState = &{{ToCamel .Name}}Resource{}
type {{ToCamel .Name}}Resource struct {
client *core.UniversalClient
}
{{- range .SchemaParams }}
{{- if (IsNested .) }}
// {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}.
type {{NestedModelName $.Name .Code}} struct {
{{- range .SubParams }}
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}" json:"{{.Code}}"` + "`" + `
{{- end }}
}
{{- end }}
{{- end }}
type {{ToCamel .Name}}Model struct {
ID types.String ` + "`" + `tfsdk:"id"` + "`" + `
ResourceName types.String ` + "`" + `tfsdk:"resource_name"` + "`" + `
OperationTimeout types.String ` + "`" + `tfsdk:"operation_timeout"` + "`" + `
LogLevel types.String ` + "`" + `tfsdk:"log_level"` + "`" + `
{{- if .HasRedeploy }}
// --- Redeploy support (ARCHITECTURE.md) ---
// git_revision: при изменении вызывает redeploy вместо modify.
// Опциональное поле — если не задано, modify работает как обычно.
GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + `
{{- end }}
{{- range .SchemaParams }}
{{- if (IsNested .) }}
{{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
{{- else }}
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
{{- end }}
{{- end }}
SuspendOnDestroy types.Bool ` + "`" + `tfsdk:"suspend_on_destroy"` + "`" + `
AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
{{- range .OutputParams }}
{{ToCamel .Code}} {{OutputType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
{{- end }}
}
func New{{ToCamel .Name}}Resource() resource.Resource {
return &{{ToCamel .Name}}Resource{}
}
func (r *{{ToCamel .Name}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_{{.Name}}"
}
func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
attrs := map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
"resource_name": schema.StringAttribute{Required: true},
"operation_timeout": schema.StringAttribute{Optional: true},
"log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
{{- range .SchemaParams }}
{{- if (IsNested .) }}
"{{ToSnake .Code}}": {{NestedSchemaBlock .}}
{{- range .SubParams }}
"{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{
{{- if .Default }}Optional: true, Computed: true, Default: {{SubDefaultExpr .}},{{else if .Required}}Required: true,{{else}}Optional: true,{{end}}
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
},
{{- end }}
{{NestedSchemaEnd .}}
{{- else }}
"{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{
{{- if and .Required (eq (ParamDefaultExpr .) "") (eq .RefSvcId 0) }}Required: true,{{else}}Optional: true,{{end}}
{{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if or .IsJson (gt .RefSvcId 0) }}Computed: true,{{- end }}
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
{{- if .Sensitive }}Sensitive: true,{{end}}
{{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{- end }}
},
{{- end }}
{{- end }}
{{- if .HasRedeploy }}
// --- Redeploy support (ARCHITECTURE.md) ---
// При изменении git_revision вызывается redeploy вместо modify.
// Если не задано — modify работает без изменений.
"git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
{{- end }}
"suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.SuspendOnDestroy}})},
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.AdoptExistingOnCreate}})},
{{- range .OutputParams }}
{{- if or (OutputIsMap .) (OutputIsList .) }}
"{{ToSnake .Code}}": schema.{{if OutputIsMap .}}Map{{else}}List{{end}}Attribute{Computed: true, ElementType: types.StringType{{if OutputSensitive .}}, Sensitive: true{{end}}},
{{- else }}
"{{ToSnake .Code}}": schema.StringAttribute{Computed: true{{if OutputSensitive .}}, Sensitive: true{{end}}},
{{- end }}
{{- end }}
}
resp.Schema = schema.Schema{Attributes: attrs}
}
func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
if r.client == nil {
return
}
var config *{{ToCamel .Name}}Model
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
if config == nil {
return
}
var state *{{ToCamel .Name}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
return
}
if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
var plan {{ToCamel .Name}}Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
{{- if .HasRefSvcParams }}
// FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
// Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
// Resolve (displayName → UUID или uppercase → lowercase) нужен только для
// API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
// "Provider produced invalid plan: planned value does not match config value".
{{- end }}
if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
return
}
}
{{- range .CreateOnlyParams }}
{{- if not (IsNested .) }}
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
{{- if eq (ParamType .) "types.Bool" }}
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
return
}
{{- else if eq (ParamType .) "types.Int64" }}
if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
return
}
{{- else }}
// FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
// в lowercase, пользователь мог написать upper/mixed. Это одно и то же
// значение, менять его нельзя только если оно реально другое.
if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
return
}
{{- end }}
}
{{- end }}
{{- end }}
return
}
{{- range .SchemaParams }}
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
if config.{{ToCamel .Code}}.IsNull() {
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
return
}
if config.{{ToCamel .Code}}.IsUnknown() {
return
}
{{- end }}
{{- end }}
if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
return
}
adoptExistingOnCreate := false
if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
}
{{- if .HasRefSvcParams }}
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
if !config.{{ToCamel .Code}}.IsNull() && !config.{{ToCamel .Code}}.IsUnknown() {
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, config.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != config.{{ToCamel .Code}}.ValueString() {
config.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
}
{{- end }}
{{- end }}
{{- end }}
params := map[int]string{
{{- range .CreateParams }}
{{- if not (IsNested .) }}
{{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}},
{{- end }}
{{- end }}
}
{{- range .CreateParams }}
{{- if (IsNested .) }}
if config.{{ToCamel .Code}} != nil {
params[{{.ID}}] = {{NestedJSONExpr . "config"}}
}
{{- end }}
{{- end }}
desiredDomain := ""
{{- if .HasDomainParam }}
if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
desiredDomain = config.Domain.ValueString()
}
{{- end }}
domainServiceIDs := []int{ {{- range .DomainServiceIDs }}{{.}}, {{- end }} }
resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, {{.ServiceID}}, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
}
func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data {{ToCamel .Name}}Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
{{- range .SchemaParams }}
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
if data.{{ToCamel .Code}}.IsNull() || data.{{ToCamel .Code}}.IsUnknown() {
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
return
}
{{- end }}
{{- end }}
{{- if .HasRefSvcParams }}
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
// ══════════════════════════════════════════════════════════════════════════
// ПРАВИЛО TERRAFORM (официальная документация):
// «If an attribute value is configured, it is NEVER valid to change that
// value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
// написал пользователь). Менять plan запрещено на уровне фреймворка.
//
// ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
// Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
// После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
// «Provider produced inconsistent result after apply».
//
// РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
// Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
// ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
// Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
// регистр в state через strings.EqualFold (сравниваем без учёта регистра).
// ══════════════════════════════════════════════════════════════════════════
original{{ToCamel .Code}} := data.{{ToCamel .Code}}
{{- end }}
{{- end }}
{{- end }}
{{- if .HasRefSvcParams }}
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
if !data.{{ToCamel .Code}}.IsNull() && !data.{{ToCamel .Code}}.IsUnknown() {
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, data.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != data.{{ToCamel .Code}}.ValueString() {
data.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
}
{{- end }}
{{- end }}
{{- end }}
resourceName := data.ResourceName.ValueString()
desiredDomain := ""
{{- if .HasDomainParam }}
if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
desiredDomain = data.Domain.ValueString()
}
{{- end }}
domainServiceIDs := []int{ {{- range .DomainServiceIDs }}{{.}}, {{- end }} }
resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
// ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
// not created, конфликт) сайд-эффект create не должен выполняться.
if resp.Diagnostics.HasError() {
return
}
params := map[int]string{
{{- range .CreateParams }}
{{- if not (IsNested .) }}
{{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}},
{{- end }}
{{- end }}
}
{{- range .CreateParams }}
{{- if (IsNested .) }}
if data.{{ToCamel .Code}} != nil {
params[{{.ID}}] = {{NestedJSONExpr . "data"}}
}
{{- end }}
{{- end }}
operationTimeout := ""
if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
operationTimeout = data.OperationTimeout.ValueString()
}
if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
}
id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
data.ID = types.StringValue(id)
state, diags := resources_core.RefreshResourceState(ctx, r.client, id, {{.ServiceID}}, data, []resources_core.StateField{
{{- range .OutputParams }}
{Code: "{{.Code}}"},
{{- end }}
}, []resources_core.InputField{
{{- range .SchemaParams }}
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }}
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
{{- end }}
{{- end }}
})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
{{- if .HasRefSvcParams }}
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, state.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() {
state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
}
{{- end }}
{{- end }}
// Restore user-provided casing в state (Create).
// EqualFold = «совпадают ли значения без учёта регистра?»
// Если да — значит API вернул «туже» строку, только в другом регистре.
// Заменяем state на original (то что было в plan/config пользователя).
// Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
if !original{{ToCamel .Code}}.IsNull() && !original{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
if strings.EqualFold(state.{{ToCamel .Code}}.ValueString(), original{{ToCamel .Code}}.ValueString()) {
state.{{ToCamel .Code}} = original{{ToCamel .Code}}
}
}
{{- end }}
{{- end }}
{{- end }}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state {{ToCamel .Name}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if state.ID.IsNull() || state.ID.IsUnknown() {
return
}
if r.client != nil {
remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if remove {
resp.State.RemoveResource(ctx)
return
}
}
newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), {{.ServiceID}}, state, []resources_core.StateField{
{{- range .OutputParams }}
{Code: "{{.Code}}"},
{{- end }}
}, []resources_core.InputField{
{{- range .SchemaParams }}
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }}
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
{{- end }}
{{- end }}
})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
{{- if .HasRefSvcParams }}
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
if !newState.{{ToCamel .Code}}.IsNull() && !newState.{{ToCamel .Code}}.IsUnknown() {
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, newState.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != newState.{{ToCamel .Code}}.ValueString() {
newState.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
}
{{- end }}
{{- end }}
// Restore user-provided casing в state (Read).
// При чтении у нас нет plan — но предыдущий state уже хранит значение
// в регистре пользователя (после первого Create оно было восстановлено).
// Берём prior state (переменная state) как эталон регистра.
// Если API вернул то же UUID только строчными буквами — восстанавливаем.
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() && !newState.{{ToCamel .Code}}.IsNull() && !newState.{{ToCamel .Code}}.IsUnknown() {
if strings.EqualFold(newState.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
newState.{{ToCamel .Code}} = state.{{ToCamel .Code}}
}
}
{{- end }}
{{- end }}
{{- end }}
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
}
func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan {{ToCamel .Name}}Model
var state {{ToCamel .Name}}Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
instanceID := state.ID
if instanceID.IsNull() || instanceID.IsUnknown() {
instanceID = plan.ID
}
if instanceID.IsNull() || instanceID.IsUnknown() {
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
return
}
hasServiceParamChanges := false
{{- range .ModifyParams }}
{{- if not (IsNested .) }}
if !hasServiceParamChanges {
if plan.{{ToCamel .Code}}.IsNull() != state.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() != state.{{ToCamel .Code}}.IsUnknown() {
hasServiceParamChanges = true
} else if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
{{- if eq (ParamType .) "types.Bool" }}
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
hasServiceParamChanges = true
}
{{- else if eq (ParamType .) "types.Int64" }}
if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
hasServiceParamChanges = true
}
{{- else }}
if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
hasServiceParamChanges = true
}
{{- end }}
}
}
{{- end }}
{{- end }}
{{- if .HasRedeploy }}
// --- Redeploy support (ARCHITECTURE.md) ---
// Проверяем изменился ли git_revision.
// Если да — вызываем redeploy вместо modify.
// git_revision = "" (не задано) → обычный modify.
redeployRequested := false
if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
redeployRequested = true
}
}
{{- end }}
if !hasServiceParamChanges{{if .HasRedeploy}} && !redeployRequested{{end}} {
plan.ID = instanceID
{{- range .OutputParams }}
plan.{{ToCamel .Code}} = state.{{ToCamel .Code}}
{{- end }}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
return
}
operationTimeout := ""
if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() {
operationTimeout = plan.OperationTimeout.ValueString()
}
if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() {
ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString())
}
{{- if .HasRedeploy }}
// --- Redeploy support (ARCHITECTURE.md) ---
if redeployRequested {
{{- if .RedeployParams }}
redeployParams := map[int]string{}
{{- range .RedeployParams }}
{{- if (IsNested .) }}
if plan.{{ToCamel .Code}} != nil {
redeployParams[{{.ID}}] = {{NestedJSONExpr . "plan"}}
}
{{- else }}
redeployParams[{{.ID}}] = {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}
{{- end }}
{{- end }}
if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout, redeployParams); err != nil {
{{- else }}
if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout, nil); err != nil {
{{- end }}
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
} else {
{{- end }}
params := map[int]string{
{{- range .ModifyParams }}
{{- if not (IsNested .) }}
{{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }}
{{- end }}
}
{{- range .ModifyParams }}
{{- if (IsNested .) }}
if plan.{{ToCamel .Code}} != nil {
params[{{.ID}}] = {{NestedJSONExpr . "plan"}}
}
{{- end }}
{{- end }}
if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
{{- if .HasRedeploy }}
}
{{- end }}
plan.ID = instanceID
state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), {{.ServiceID}}, plan, []resources_core.StateField{
{{- range .OutputParams }}
{Code: "{{.Code}}"},
{{- end }}
}, []resources_core.InputField{
{{- range .SchemaParams }}
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }}
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
{{- end }}
{{- end }}
})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
{{- if .HasRefSvcParams }}
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, state.{{ToCamel .Code}}.ValueString())
if err != nil {
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() {
state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
}
{{- end }}
{{- end }}
// Restore user-provided casing в state (Update).
// После API-вызова modify state содержит значения в lower-case от API.
// Plan == config == то что написал пользователь (регистр неизменён).
// EqualFold: если UUID совпадает без учёта регистра — берём из plan.
{{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
if strings.EqualFold(state.{{ToCamel .Code}}.ValueString(), plan.{{ToCamel .Code}}.ValueString()) {
state.{{ToCamel .Code}} = plan.{{ToCamel .Code}}
}
}
{{- end }}
{{- end }}
{{- end }}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state {{ToCamel .Name}}Model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if state.ID.IsNull() || state.ID.IsUnknown() {
return
}
{{- if .SupportsSuspendDestroy }}
deleteMode := "state_only"
if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
deleteMode = "suspend"
}
{{- else }}
deleteMode := "delete"
{{- end }}
operationTimeout := ""
if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() {
operationTimeout = state.OperationTimeout.ValueString()
}
if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() {
ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString())
}
if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
}
func (r *{{ToCamel .Name}}Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *{{ToCamel .Name}}Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*core.UniversalClient)
if !ok {
resp.Diagnostics.AddError("Error", "Invalid client type")
return
}
r.client = client
}
`