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
@@ -223,6 +223,23 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
params.AnalyzePlanModifiers(&sr.NeedsBoolPlanMod, &sr.NeedsInt64PlanMod, &sr.NeedsStringPlanMod, sr.SchemaParams)
sr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(sr.SchemaParams)
sr.NeedsStringsImport = params.AnalyzeNeedsStrings(sr.ModifyParams)
// VaultUserPassword: пароль пользователя ГЕНЕРИРУЕТ платформа — в create-параметрах
// подресурса нет кода password, — а сервис отдаёт vault-выходы. Значит пароль
// появится в Vault родителя после create_user, и подресурс обязан вернуть его сам
// (выход password), иначе потребителю придётся читать vault_secrets родителя, что
// внутри одного apply невозможно. См. TOOLS/ARCHITECTURE.md.
//
// Признак берётся из ДАННЫХ спека, без сверки с именем сервиса: поэтому mariadb
// (там пароль задаётся на входе create_user) сюда не попадает.
//
// Отдельно требуется параметр username: именно по нему провайдер находит пароль
// в секрете Vault. У сервисов, где пользователь адресуется иначе (k8s_*_user,
// vc_org_user), выход пока не генерируется.
if sr.SubName == "user" && serviceHasVaultOutputs(spec) &&
hasParamCode(sr.CreateParams, "username") && !hasParamCode(sr.CreateParams, "password") {
sr.VaultUserPassword = true
}
subs = append(subs, *sr)
}
@@ -460,8 +477,35 @@ func validateModifierOperation(i int, op *lib.OperationSpec) error {
return nil
}
// analyzeUseStateForUnknown определяет, нужны ли импорты boolplanmodifier/int64planmodifier:
// есть ли среди SchemaParams скалярный Optional+Computed (без Default) параметр
// serviceHasVaultOutputs сообщает, отдаёт ли сервис секреты через Vault
// (в outputs объявлены vault_secrets / vault_fields).
//
// Зачем: это признак «сервис держит секреты в Vault родителя». Используется вместе
// с отсутствием входного password у create-подресурса, чтобы включить выход password
// у подресурса-пользователя (см. VaultUserPassword в types.GenSubresource).
// Признак берётся из данных спека — имя сервиса нигде не проверяется.
func serviceHasVaultOutputs(spec types.ServiceSpec) bool {
for _, p := range spec.Outputs.Params {
switch strings.ToLower(strings.TrimSpace(p.Code)) {
case "vault_secrets", "vault_fields":
return true
}
}
return false
}
// hasParamCode сообщает, есть ли среди параметров параметр с таким кодом
// (регистр и пробелы не важны).
func hasParamCode(params []types.Param, code string) bool {
for _, p := range params {
if strings.EqualFold(strings.TrimSpace(p.Code), code) {
return true
}
}
return false
}
// analyzeUseStateForUnknown определяет, нужны ли импорты boolplanmodifier/int64planmodifier:// есть ли среди SchemaParams скалярный Optional+Computed (без Default) параметр
// соответствующего типа, для которого генерируется UseStateForUnknown().
func analyzeUseStateForUnknown(schemaParams []types.Param) (needsBool, needsInt64 bool) {
for _, p := range schemaParams {