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/resource-generator. 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) --- // 1. Modify — только если изменились параметры сервиса if hasServiceParamChanges { {{- 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 }} } // 2. Redeploy — только если изменился git_revision if redeployRequested { 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 { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } } {{- 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 } `