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:
@@ -0,0 +1,106 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"resource-generator/internal/types"
|
||||
)
|
||||
|
||||
// TestWriteSubresource_VaultUserPassword — регрессионный тест выхода password у
|
||||
// подресурса-пользователя.
|
||||
//
|
||||
// Зачем: у части сервисов пароль пользователя ГЕНЕРИРУЕТ платформа и кладёт его в
|
||||
// секрет Vault родительского инстанса. Подресурс обязан вернуть пароль СВОИМ выходом
|
||||
// сразу после create_user — иначе потребитель не получит его в том же apply
|
||||
// (vault_secrets родителя — Computed и обновляется только при Read родителя).
|
||||
// См. TOOLS/ARCHITECTURE.md → «Subresource-born secrets».
|
||||
//
|
||||
// Проверяем три части фичи и то, что она НЕ включается у чужих подресурсов:
|
||||
// 1) поле модели Password с тэгом tfsdk:"password";
|
||||
// 2) атрибут схемы Computed + Sensitive;
|
||||
// 3) чтение секрета Vault в Create и перенос пароля в Update;
|
||||
// 4) при VaultUserPassword=false ничего этого нет (чтобы mariadb и подресурсы без
|
||||
// секретов не получили лишний атрибут).
|
||||
func TestWriteSubresource_VaultUserPassword(t *testing.T) {
|
||||
base := types.GenSubresource{
|
||||
ServiceName: "postgres",
|
||||
ServiceID: 90,
|
||||
SubName: "user",
|
||||
CreateOpName: "create_user",
|
||||
DeleteOpName: "delete_user",
|
||||
CreateParams: []types.Param{
|
||||
{Code: "username", Type: "string", Required: true, Descr: "Имя пользователя"},
|
||||
{Code: "role", Type: "string", Required: true, Descr: "Роль"},
|
||||
},
|
||||
SchemaParams: []types.Param{
|
||||
{Code: "username", Type: "string", Required: true, Descr: "Имя пользователя"},
|
||||
{Code: "role", Type: "string", Required: true, Descr: "Роль"},
|
||||
},
|
||||
IdentityParams: []types.Param{
|
||||
{Code: "username", Type: "string", Required: true, Descr: "Имя пользователя"},
|
||||
},
|
||||
UsesString: true,
|
||||
}
|
||||
|
||||
render := func(t *testing.T, sr types.GenSubresource) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if err := WriteSubresource(dir, sr); err != nil {
|
||||
t.Fatalf("WriteSubresource вернул ошибку: %v", err)
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("не прочитал каталог генерации: %v", err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("ожидался один сгенерированный файл, получено %d", len(entries))
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(dir, entries[0].Name()))
|
||||
if err != nil {
|
||||
t.Fatalf("не прочитал %s: %v", entries[0].Name(), err)
|
||||
}
|
||||
return squashSpaces(string(raw))
|
||||
}
|
||||
|
||||
t.Run("фича включена: выход password есть", func(t *testing.T) {
|
||||
sr := base
|
||||
sr.VaultUserPassword = true
|
||||
src := render(t, sr)
|
||||
|
||||
if !strings.Contains(src, `Password types.String`) || !strings.Contains(src, `tfsdk:"password"`) {
|
||||
t.Errorf("в модели нет поля Password с тэгом tfsdk:\"password\"")
|
||||
}
|
||||
|
||||
idx := strings.Index(src, `"password": schema.StringAttribute{`)
|
||||
if idx < 0 {
|
||||
t.Fatalf("в схеме нет атрибута password")
|
||||
}
|
||||
block := src[idx:]
|
||||
if end := strings.Index(block, " },"); end > 0 {
|
||||
block = block[:end]
|
||||
}
|
||||
if !strings.Contains(block, "Computed: true") || !strings.Contains(block, "Sensitive: true") {
|
||||
t.Errorf("password должен быть Computed + Sensitive, блок:\n%s", block)
|
||||
}
|
||||
|
||||
if !strings.Contains(src, "ExtractUserPassword(") || !strings.Contains(src, "GetInstanceVaultSecrets(") {
|
||||
t.Errorf("в Create нет чтения пароля из Vault родителя")
|
||||
}
|
||||
if !strings.Contains(src, "priorState.Password") {
|
||||
t.Errorf("в Update нет сохранения уже полученного пароля")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("фича выключена: атрибута password нет", func(t *testing.T) {
|
||||
src := render(t, base)
|
||||
if strings.Contains(src, `tfsdk:"password"`) {
|
||||
t.Errorf("атрибут password не должен появляться без VaultUserPassword")
|
||||
}
|
||||
if strings.Contains(src, "ExtractUserPassword(") {
|
||||
t.Errorf("чтение Vault не должно появляться без VaultUserPassword")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user