gen(subresource): выход password для авто-генерируемого пароля пользователя

Проблема: у части сервисов пароль пользователя генерирует платформа и кладёт его в
секрет 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 — чисто.
This commit is contained in:
Repinoid
2026-10-01 17:15:14 +03:00
parent 1a049efa44
commit 58c519e712
5 changed files with 271 additions and 2 deletions
@@ -0,0 +1,63 @@
package resources_core
import (
"encoding/json"
"fmt"
"strings"
)
// ExtractUserPassword вытаскивает пароль пользователя из JSON-значения секрета Vault
// родительского инстанса.
//
// ЗАЧЕМ: у части сервисов (проверено на postgres) платформа САМА генерирует пароль
// в операции create_user и кладёт его в секрет Vault родителя. Формат значения
// (проверено через GET /instances/<uid>/vault/users):
//
// {"<username>": {"password": "<пароль>"}}
//
// Подресурс обязан вернуть этот пароль СВОИМ выходом сразу после create_user, иначе
// потребитель не сможет получить его в том же apply (vault_secrets родителя — Computed
// и обновляется только при Read родителя). См. TOOLS/ARCHITECTURE.md,
// раздел «Subresource-born secrets».
//
// Возвращает "" без ошибки, если пользователя в секрете нет или пароль отсутствует:
// это не фатально (пароль остаётся неизвестным до следующего чтения) и не должно
// ломать create.
func ExtractUserPassword(secretJSON string, username string) (string, error) {
raw := strings.TrimSpace(secretJSON)
if raw == "" {
return "", nil
}
name := strings.TrimSpace(username)
if name == "" {
return "", fmt.Errorf("пустое имя пользователя для поиска пароля в Vault")
}
// Основной формат: {"<username>": {"password": "..."}}.
var byUser map[string]struct {
Password string `json:"password"`
}
if err := json.Unmarshal([]byte(raw), &byUser); err == nil {
if entry, ok := byUser[name]; ok && strings.TrimSpace(entry.Password) != "" {
return strings.TrimSpace(entry.Password), nil
}
// Регистр имени может отличаться (платформа нормализует его сама).
for key, entry := range byUser {
if strings.EqualFold(strings.TrimSpace(key), name) && strings.TrimSpace(entry.Password) != "" {
return strings.TrimSpace(entry.Password), nil
}
}
}
// Упрощённый формат: {"password": "..."} — единственный пользователь в секрете.
var flat struct {
Password string `json:"password"`
}
if err := json.Unmarshal([]byte(raw), &flat); err == nil {
if strings.TrimSpace(flat.Password) != "" {
return strings.TrimSpace(flat.Password), nil
}
}
return "", nil
}