Проблема: у части сервисов пароль пользователя генерирует платформа и кладёт его в
секрет Vault РОДИТЕЛЬСКОГО инстанса. vault_secrets родителя — Computed и обновляется
только при его Read, поэтому внутри одного apply после create_user пароль недоступен
(Invalid index). Из-за этого в pg/outputs.tf приходилось читать vault_secrets кластера,
а стенду требовались два apply.
Решение: подресурс-пользователь отдаёт пароль СВОИМ выходом сразу после create_user.
- types.go: GenSubresource.VaultUserPassword (признак из данных спека);
- loader.go: признак = подресурс user + у сервиса есть vault-выходы + create_user
принимает username и НЕ принимает password; имя сервиса нигде не проверяется;
- templates/subresource.go: поле модели + Computed/Sensitive атрибут password,
чтение Vault родителя в Create (GetInstanceStateDetails + GetInstanceVaultSecrets)
и перенос уже полученного пароля в Update (чтобы Computed-атрибут не стал unknown);
- resources_core/subresource_user_password.go: ExtractUserPassword — разбор
{"<username>":{"password":"..."}} с безопасным возвратом пустой строки;
- writers: регрессионный тест «фича включена/выключена».
Проверено генерацией и сборкой test-стенда: выход получили 4 подресурса
(postgres_user, kafka_user, clickhouse_user, mongodb_user); mariadb_user НЕ затронут
(там пароль входной); k8s_*_user и vc_org_user не затронуты (пользователь
адресуется не через username). go test ./... — ok, go build — чисто.
510 lines
23 KiB
Go
510 lines
23 KiB
Go
package templates
|
||
|
||
const Subresource = `package resources_gen
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
|
||
"terraform-provider-nubes/internal/core"
|
||
"terraform-provider-nubes/internal/resources_core"
|
||
|
||
"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/booldefault"
|
||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||
{{- if .NeedsInt64Default }}
|
||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||
{{- end }}
|
||
{{- if .NeedsStringDefault }}
|
||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||
{{- end }}
|
||
{{- if .NeedsBoolPlanMod }}
|
||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
|
||
{{- end }}
|
||
{{- if .NeedsInt64PlanMod }}
|
||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||
{{- end }}
|
||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||
)
|
||
|
||
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
|
||
// Service: {{.ServiceName}}
|
||
// Subresource: {{.SubName}}
|
||
|
||
var _ resource.Resource = &{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource{}
|
||
|
||
// {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource manages a subresource via operations.
|
||
type {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource struct {
|
||
client *core.UniversalClient
|
||
}
|
||
|
||
type {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model struct {
|
||
ID types.String ` + "`" + `tfsdk:"id"` + "`" + `
|
||
{{ToCamel .ServiceName}}ID types.String ` + "`" + `tfsdk:"{{ToSnake .ServiceName}}_id"` + "`" + `
|
||
AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
|
||
SkipMissingOnDelete types.Bool ` + "`" + `tfsdk:"skip_missing_on_delete"` + "`" + `
|
||
KeepOnDestroy types.Bool ` + "`" + `tfsdk:"keep_on_destroy"` + "`" + `
|
||
{{- if .VaultUserPassword }}
|
||
// Password — пароль пользователя, который генерирует платформа и кладёт в секрет
|
||
// Vault родителя. Заполняется в Create сразу после create_user: потребителю не нужно
|
||
// читать vault_secrets родителя (внутри одного apply оно ещё пусто).
|
||
Password types.String ` + "`" + `tfsdk:"password"` + "`" + `
|
||
{{- end }}
|
||
OperationTimeout types.String ` + "`" + `tfsdk:"operation_timeout"` + "`" + `
|
||
LogLevel types.String ` + "`" + `tfsdk:"log_level"` + "`" + `
|
||
{{- range .SchemaParams }}
|
||
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||
{{- end }}
|
||
}
|
||
|
||
func New{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource() resource.Resource {
|
||
return &{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource{}
|
||
}
|
||
|
||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||
resp.TypeName = req.ProviderTypeName + "_{{.ServiceName}}_{{.SubName}}"
|
||
}
|
||
|
||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}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()}},
|
||
"{{ToSnake .ServiceName}}_id": schema.StringAttribute{
|
||
Required: true,
|
||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||
},
|
||
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
|
||
"skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
|
||
"keep_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Режим state_only при destroy: подресурс не удаляется и не меняется в облаке, только убирается из состояния. Нужен там, где родительский инстанс при destroy не удаляется (например suspend_on_destroy = true) и объект должен остаться внутри него. false = обычное удаление."},
|
||
{{- if .VaultUserPassword }}
|
||
"password": schema.StringAttribute{Computed: true, Sensitive: true, MarkdownDescription: "Пароль пользователя, который генерирует платформа. Провайдер читает его из Vault родительского инстанса сразу после создания пользователя."},
|
||
{{- end }}
|
||
"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 }}
|
||
"{{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 .IsJson }}Computed: true,{{- end }}
|
||
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
|
||
{{- if .Sensitive }}Sensitive: true,{{end}}
|
||
{{- if .ForceNew }}PlanModifiers: []planmodifier.{{PlanModifierType .}}{ {{PlanModifierExpr .}} },{{else if .IsJson}}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{end}}
|
||
},
|
||
{{- end }}
|
||
}
|
||
resp.Schema = schema.Schema{Attributes: attrs}
|
||
}
|
||
|
||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||
var plan {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
|
||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
{{- range .SchemaParams }}
|
||
{{- if and .Required (eq (ParamDefaultExpr .) "") }}
|
||
if plan.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() {
|
||
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
|
||
return
|
||
}
|
||
{{- end }}
|
||
{{- end }}
|
||
{{- if .HasRefSvcParams }}
|
||
{{- range .CreateParams }}
|
||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||
// Подресурсный refSvc-параметр резолвим только в локальную переменную.
|
||
// Сам plan не трогаем: UUID нужен лишь для backend-вызова, а в state
|
||
// Terraform должен видеть исходное пользовательское значение.
|
||
resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString()
|
||
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() {
|
||
var err error
|
||
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
}
|
||
{{- end }}
|
||
{{- end }}
|
||
{{- end }}
|
||
|
||
instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString())
|
||
if instanceUID == "" {
|
||
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
|
||
return
|
||
}
|
||
adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
|
||
|
||
idParams := map[string]string{
|
||
{{- range .IdentityParams }}
|
||
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||
{{- end }}
|
||
}
|
||
identityCodes := []string{
|
||
{{- range .IdentityParams }}
|
||
"{{.Code}}",
|
||
{{- end }}
|
||
}
|
||
listKey, idKey := resources_core.ResolveSubresourceStateKeys("{{.SubName}}", identityCodes)
|
||
targetValue := strings.TrimSpace(idParams[idKey])
|
||
if targetValue == "" {
|
||
for _, code := range identityCodes {
|
||
candidate := strings.TrimSpace(idParams[code])
|
||
if candidate == "" {
|
||
continue
|
||
}
|
||
targetValue = candidate
|
||
idKey = code
|
||
break
|
||
}
|
||
}
|
||
|
||
if targetValue != "" {
|
||
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
if known && found {
|
||
if adoptExistingOnCreate {
|
||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||
resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue)
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||
return
|
||
}
|
||
resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue)
|
||
return
|
||
}
|
||
}
|
||
|
||
params := resources_core.CompactParams(map[string]string{
|
||
{{- range .CreateParams }}
|
||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||
"{{.Code}}": resolved{{ToCamel .Code}},
|
||
{{- else }}
|
||
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||
{{- end }}
|
||
{{- end }}
|
||
})
|
||
|
||
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())
|
||
}
|
||
createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.CreateOpName}}", params, operationTimeout)
|
||
if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) {
|
||
createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.CreateOpName}}", params, operationTimeout)
|
||
}
|
||
if createErr != nil {
|
||
if targetValue != "" {
|
||
found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||
if checkErr != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
|
||
return
|
||
}
|
||
if known && found {
|
||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||
resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue)
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||
return
|
||
}
|
||
}
|
||
if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) {
|
||
if targetValue != "" {
|
||
found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||
if checkErr != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
|
||
return
|
||
}
|
||
if found || !known {
|
||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||
resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state")
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||
return
|
||
}
|
||
}
|
||
resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out")
|
||
return
|
||
}
|
||
resp.Diagnostics.AddError("Ошибка клиента", createErr.Error())
|
||
return
|
||
}
|
||
if targetValue != "" {
|
||
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
if known && !found {
|
||
resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue)
|
||
return
|
||
}
|
||
}
|
||
|
||
{{- if .VaultUserPassword }}
|
||
// Пароль пользователя генерирует платформа: после create_user он появляется в секрете
|
||
// Vault родительского инстанса. Читаем его ЗДЕСЬ и кладём в свой выход, чтобы потребитель
|
||
// получил пароль в ЭТОМ же apply (vault_secrets родителя — Computed и обновляется только
|
||
// при Read родителя). См. TOOLS/ARCHITECTURE.md → «Subresource-born secrets».
|
||
// Неудачное чтение create не роняет: поле просто останется пустым.
|
||
plan.Password = types.StringNull()
|
||
if instanceUID != "" {
|
||
if details, derr := r.client.GetInstanceStateDetails(ctx, instanceUID); derr == nil && details != nil && len(details.Vault.Fields) > 0 {
|
||
if secrets, serr := r.client.GetInstanceVaultSecrets(ctx, instanceUID, details.Vault.Fields); serr == nil {
|
||
for _, secretValue := range secrets {
|
||
pw, perr := resources_core.ExtractUserPassword(secretValue, plan.Username.ValueString())
|
||
if perr != nil {
|
||
continue
|
||
}
|
||
if pw != "" {
|
||
plan.Password = types.StringValue(pw)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
{{- end }}
|
||
|
||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||
}
|
||
|
||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||
var state {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
|
||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||
}
|
||
|
||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||
var plan {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
|
||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
{{- if .VaultUserPassword }}
|
||
// Пароль меняет платформа, а не пользователь: при modify его не трогаем и оставляем уже
|
||
// полученное значение из state (иначе Computed-атрибут станет null/unknown и Terraform
|
||
// ругнётся на несовместимый результат после apply).
|
||
if plan.Password.IsNull() || plan.Password.IsUnknown() {
|
||
var priorState {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
|
||
if stateDiags := req.State.Get(ctx, &priorState); !stateDiags.HasError() {
|
||
plan.Password = priorState.Password
|
||
}
|
||
if plan.Password.IsNull() || plan.Password.IsUnknown() {
|
||
plan.Password = types.StringNull()
|
||
}
|
||
}
|
||
{{- end }}
|
||
{{- if .HasRefSvcParams }}
|
||
{{- range .ModifyParams }}
|
||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||
// Update для подресурса следует тому же правилу, что и Create:
|
||
// резолвим значение только в локальную переменную и не мутируем plan.
|
||
resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString()
|
||
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() {
|
||
var err error
|
||
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
}
|
||
{{- end }}
|
||
{{- end }}
|
||
{{- end }}
|
||
|
||
if "{{.ModifyOpName}}" == "" {
|
||
var state {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
|
||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
if plan.ID.IsNull() || plan.ID.IsUnknown() {
|
||
plan.ID = state.ID
|
||
}
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||
return
|
||
}
|
||
|
||
instanceUID := strings.TrimSpace(plan.{{ToCamel .ServiceName}}ID.ValueString())
|
||
if instanceUID == "" {
|
||
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра")
|
||
return
|
||
}
|
||
|
||
params := resources_core.CompactParams(map[string]string{
|
||
{{- range .ModifyParams }}
|
||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||
"{{.Code}}": resolved{{ToCamel .Code}},
|
||
{{- else }}
|
||
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||
{{- end }}
|
||
{{- end }}
|
||
})
|
||
|
||
idParams := map[string]string{
|
||
{{- range .IdentityParams }}
|
||
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||
{{- end }}
|
||
}
|
||
|
||
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 err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.ModifyOpName}}", params, operationTimeout); err != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
|
||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||
}
|
||
|
||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||
var state {{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Model
|
||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||
if resp.Diagnostics.HasError() {
|
||
return
|
||
}
|
||
if !state.KeepOnDestroy.IsNull() && !state.KeepOnDestroy.IsUnknown() && state.KeepOnDestroy.ValueBool() {
|
||
resp.Diagnostics.AddWarning(
|
||
"Подресурс оставлен как есть, а не удалён",
|
||
"destroy: {{.ServiceName}}_{{.SubName}} (service_id={{.ServiceID}}) не изменялся в облаке и только убран из состояния (keep_on_destroy = true). Для полного удаления выставьте keep_on_destroy = false.",
|
||
)
|
||
return
|
||
}
|
||
{{- if .HasRefSvcParams }}
|
||
{{- range .SchemaParams }}
|
||
{{- if and (gt .RefSvcId 0) (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.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() {
|
||
state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||
}
|
||
}
|
||
{{- end }}
|
||
{{- end }}
|
||
{{- end }}
|
||
|
||
if "{{.DeleteOpName}}" == "" {
|
||
return
|
||
}
|
||
|
||
instanceUID := strings.TrimSpace(state.{{ToCamel .ServiceName}}ID.ValueString())
|
||
if instanceUID == "" {
|
||
return
|
||
}
|
||
skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool()
|
||
|
||
idParams := map[string]string{
|
||
{{- range .IdentityParams }}
|
||
"{{.Code}}": {{ParamFormat . (printf "state.%s" (ToCamel .Code))}},
|
||
{{- end }}
|
||
}
|
||
identityCodes := []string{
|
||
{{- range .IdentityParams }}
|
||
"{{.Code}}",
|
||
{{- end }}
|
||
}
|
||
listKey, idKey := resources_core.ResolveSubresourceStateKeys("{{.SubName}}", identityCodes)
|
||
targetValue := strings.TrimSpace(idParams[idKey])
|
||
if targetValue == "" {
|
||
for _, code := range identityCodes {
|
||
candidate := strings.TrimSpace(idParams[code])
|
||
if candidate == "" {
|
||
continue
|
||
}
|
||
targetValue = candidate
|
||
idKey = code
|
||
break
|
||
}
|
||
}
|
||
|
||
if targetValue != "" {
|
||
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
if known && !found {
|
||
resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue)
|
||
return
|
||
}
|
||
}
|
||
|
||
params := resources_core.CompactParams(map[string]string{
|
||
{{- range .DeleteParams }}
|
||
"{{.Code}}": {{ParamFormat . (printf "state.%s" (ToCamel .Code))}},
|
||
{{- 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.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.DeleteOpName}}", params, operationTimeout); err != nil {
|
||
if resources_core.IsSubresourceMissingError(err) {
|
||
if targetValue != "" {
|
||
found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||
if checkErr != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
|
||
return
|
||
}
|
||
if known && found {
|
||
resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue)
|
||
return
|
||
}
|
||
}
|
||
if skipMissingOnDelete {
|
||
resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено")
|
||
return
|
||
}
|
||
}
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
|
||
if targetValue != "" {
|
||
found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||
if err != nil {
|
||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||
return
|
||
}
|
||
if known && found {
|
||
resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}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
|
||
}
|
||
`
|