fix(subresource): password заполняется во ВСЕХ ветках Create, а не только в успешной
Баг (воспроизведён на TEST 2026-10-01): при усыновлении уже существующего пользователя БД Create выходит по раннему return (ветка 'Подресурс уже существует' -> State.Set -> return), оставляя Computed-атрибут password в состоянии unknown. Terraform отказывался: 'Provider returned invalid result object after apply: the provider still indicated an unknown value for nubes_postgres_user.crud_user_0.password'. Исправление: - новая функция resources_core.ResolveUserPasswordFromVault(ctx, client, instanceUID, username, current): читает пароль из Vault родителя, иначе возвращает current, иначе типизированный null (null для Terraform — конкретное, known значение); - в шаблоне подресурса password инициализируется null сразу после получения instanceUID, а перед КАЖДЫМ resp.State.Set в Create проставляется конкретным значением — включая все ветки усыновления; - регрессионный тест усилен: считает сохранения состояния и вызовы заполнения в Create и падает, если хоть в одной ветке password останется unknown. Проверено: generated/test/go/90_postgres_user_resource.go — 4 State.Set и 4 вызова заполнения; go test ./... ok; go build — чисто.
This commit is contained in:
@@ -134,6 +134,12 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
return
|
||||
}
|
||||
adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
|
||||
{{- if .VaultUserPassword }}
|
||||
// password объявлен Computed: в плане он unknown, а Terraform требует КОНКРЕТНОЕ
|
||||
// значение после apply. Каждая ветка выхода ниже (включая усыновление уже
|
||||
// существующего объекта) обязана проставить его — см. ResolveUserPasswordFromVault.
|
||||
plan.Password = types.StringNull()
|
||||
{{- end }}
|
||||
|
||||
idParams := map[string]string{
|
||||
{{- range .IdentityParams }}
|
||||
@@ -168,6 +174,9 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
if known && found {
|
||||
if adoptExistingOnCreate {
|
||||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||
{{- if .VaultUserPassword }}
|
||||
plan.Password = resources_core.ResolveUserPasswordFromVault(ctx, r.client, instanceUID, plan.Username.ValueString(), plan.Password)
|
||||
{{- end }}
|
||||
resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
return
|
||||
@@ -207,6 +216,9 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
}
|
||||
if known && found {
|
||||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||
{{- if .VaultUserPassword }}
|
||||
plan.Password = resources_core.ResolveUserPasswordFromVault(ctx, r.client, instanceUID, plan.Username.ValueString(), plan.Password)
|
||||
{{- end }}
|
||||
resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
return
|
||||
@@ -221,6 +233,9 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
}
|
||||
if found || !known {
|
||||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||
{{- if .VaultUserPassword }}
|
||||
plan.Password = resources_core.ResolveUserPasswordFromVault(ctx, r.client, instanceUID, plan.Username.ValueString(), plan.Password)
|
||||
{{- end }}
|
||||
resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state")
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
return
|
||||
@@ -245,28 +260,11 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
}
|
||||
|
||||
{{- 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Пароль пользователя генерирует платформа: он лежит в секрете Vault родительского
|
||||
// инстанса. Читаем его ЗДЕСЬ и кладём в свой выход, чтобы потребитель получил пароль
|
||||
// в этом же apply. См. TOOLS/ARCHITECTURE.md → «Subresource-born secrets».
|
||||
// Неудачное чтение create не роняет: значение останется null (known для Terraform).
|
||||
plan.Password = resources_core.ResolveUserPasswordFromVault(ctx, r.client, instanceUID, plan.Username.ValueString(), plan.Password)
|
||||
{{- end }}
|
||||
|
||||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||
|
||||
Reference in New Issue
Block a user