688 lines
29 KiB
Go
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
|
|
}
|
|
`
|