fix(subresource): диагностика чтения пароля + одна точка заполнения в Create
По итогам код-ревью (коммит 64328ab):
- ResolveUserPasswordFromVault теперь возвращает и текст предупреждения: если пароль
прочитать не удалось (недоступен API/Vault, пустое имя, нет записи), в выводе apply
появляется Warning. Раньше поле молча становилось null и причина была невидима.
Отсутствие секретов у родителя ошибкой не считается (для части сервисов это норма).
- В шаблоне подресурса заполнение вынесено в одно замыкание applyPassword(),
вызываемое перед каждым resp.State.Set в Create (4 сохранения — 4 вызова).
Убирает четыре одинаковые строки и снижает риск забыть новую ветку выхода.
- Тест проверяет: замыкание есть, предупреждение есть, и вызовов applyPassword()
не меньше, чем сохранений состояния в Create.
Проверено: generated/test/go/90_postgres_user_resource.go — 4 State.Set, 4 вызова
(плюс одно упоминание в комментарии); go test ./... ok; go build ./internal/... и
полная сборка провайдера во временной копии с новым generated — чисто.
This commit is contained in:
@@ -135,10 +135,20 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
}
|
||||
adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool()
|
||||
{{- if .VaultUserPassword }}
|
||||
// password объявлен Computed: в плане он unknown, а Terraform требует КОНКРЕТНОЕ
|
||||
// значение после apply. Каждая ветка выхода ниже (включая усыновление уже
|
||||
// существующего объекта) обязана проставить его — см. ResolveUserPasswordFromVault.
|
||||
// password — Computed: в плане он unknown, а Terraform требует КОНКРЕТНОЕ значение после
|
||||
// apply, иначе «Provider returned invalid result object after apply». Поэтому:
|
||||
// 1) сразу задаём конкретный null — покрывает ветки, где читать нечего;
|
||||
// 2) applyPassword() обязана вызываться перед КАЖДЫМ сохранением состояния в Create
|
||||
// (обычный путь и все ветки усыновления) — она подставляет реальный пароль.
|
||||
// Замыкание одно, чтобы новая ветка выхода не осталась без заполнения.
|
||||
plan.Password = types.StringNull()
|
||||
applyPassword := func() {
|
||||
var warn string
|
||||
plan.Password, warn = resources_core.ResolveUserPasswordFromVault(ctx, r.client, instanceUID, plan.Username.ValueString(), plan.Password)
|
||||
if warn != "" {
|
||||
resp.Diagnostics.AddWarning("Пароль пользователя не прочитан из Vault", warn)
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
idParams := map[string]string{
|
||||
@@ -175,7 +185,7 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
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)
|
||||
applyPassword()
|
||||
{{- end }}
|
||||
resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
@@ -217,7 +227,7 @@ 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)
|
||||
applyPassword()
|
||||
{{- end }}
|
||||
resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
@@ -234,7 +244,7 @@ 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)
|
||||
applyPassword()
|
||||
{{- end }}
|
||||
resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state")
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
@@ -264,7 +274,7 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
// инстанса. Читаем его ЗДЕСЬ и кладём в свой выход, чтобы потребитель получил пароль
|
||||
// в этом же 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)
|
||||
applyPassword()
|
||||
{{- end }}
|
||||
|
||||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||
|
||||
@@ -89,6 +89,12 @@ func TestWriteSubresource_VaultUserPassword(t *testing.T) {
|
||||
if !strings.Contains(src, "ResolveUserPasswordFromVault(") {
|
||||
t.Errorf("нет чтения пароля из Vault родителя (ResolveUserPasswordFromVault)")
|
||||
}
|
||||
if !strings.Contains(src, "applyPassword := func()") {
|
||||
t.Errorf("нет единого замыкания applyPassword")
|
||||
}
|
||||
if !strings.Contains(src, "Пароль пользователя не прочитан из Vault") {
|
||||
t.Errorf("нет предупреждения при неудачном чтении пароля — ошибка останется невидимой")
|
||||
}
|
||||
if !strings.Contains(src, "priorState.Password") {
|
||||
t.Errorf("в Update нет сохранения уже полученного пароля")
|
||||
}
|
||||
@@ -96,7 +102,7 @@ func TestWriteSubresource_VaultUserPassword(t *testing.T) {
|
||||
// КРИТИЧНО: password — Computed-атрибут. Если хотя бы одна ветка выхода из
|
||||
// Create сохранит состояние, не проставив его, Terraform упадёт с
|
||||
// "Provider returned invalid result object after apply ... unknown value".
|
||||
// Поэтому: инициализация + вызов перед каждым сохранением состояния.
|
||||
// Поэтому: инициализация + вызов заполнения перед каждым сохранением состояния.
|
||||
if !strings.Contains(src, "plan.Password = types.StringNull()") {
|
||||
t.Errorf("password не инициализируется конкретным значением до ветвлений")
|
||||
}
|
||||
@@ -108,12 +114,12 @@ func TestWriteSubresource_VaultUserPassword(t *testing.T) {
|
||||
createBody = createBody[:end]
|
||||
}
|
||||
setCount := strings.Count(createBody, "resp.State.Set(ctx, &plan)")
|
||||
resolveCount := strings.Count(createBody, "ResolveUserPasswordFromVault(")
|
||||
callCount := strings.Count(createBody, "applyPassword()")
|
||||
if setCount == 0 {
|
||||
t.Fatalf("в Create не найдено сохранение состояния")
|
||||
}
|
||||
if resolveCount < setCount {
|
||||
t.Errorf("в Create %d сохранений состояния, но только %d вызовов заполнения пароля — в какой-то ветке password останется unknown", setCount, resolveCount)
|
||||
if callCount < setCount {
|
||||
t.Errorf("в Create %d сохранений состояния, но только %d вызовов applyPassword() — в какой-то ветке password останется unknown", setCount, callCount)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user