fix auth model
This commit is contained in:
1 parent
f81285b151
commit
5e3800e9cb
10 files changed
+901
-70
No files matched your search
@@ -0,0 +1,301 @@
|
||||
# План доработки: API управления конфигурацией
|
||||
|
||||
> Статус: **план на будущее, не реализовано**. Документ фиксирует
|
||||
> согласованный дизайн доработки Control API, дающей возможность
|
||||
> управлять `check_types`, `targets`, `validators` и `sites` через HTTP
|
||||
> API вместо правки YAML + рестарта. Реализация — отдельная задача.
|
||||
|
||||
## Context
|
||||
|
||||
Сейчас `control-api` полностью read-only в части конфигурации: типы
|
||||
проверок (`check_types`), список целей (`targets`), состав валидаторов
|
||||
(`validators`) и внешних площадок (`sites`) читаются один раз из
|
||||
`control-api.yaml` при старте процесса и живут дальше только в памяти
|
||||
(`Orchestrator.Checks`, `Orchestrator.Sites`) либо (для валидаторов) в
|
||||
таблице `validators`, куда при каждом рестарте они переупорядочиваются из
|
||||
YAML. Единственный способ что-то поменять — отредактировать YAML и
|
||||
выполнить `systemctl restart control-api`. Это задокументированное
|
||||
ограничение (см. `docs/USAGE.md`).
|
||||
|
||||
Согласованные решения (зафиксированы для будущей реализации):
|
||||
- **Область доработки** — ровно четыре сущности: `check_types` (типы
|
||||
проверок + их привязка к группам целей), `targets` (группы целей),
|
||||
`validators` (состав ВМ-валидаторов), `sites` (состав из ≤3 внешних
|
||||
площадок). Очередь `ip_addresses` и тайминги оркестратора
|
||||
(`orchestrator.*`, `aggregation.*`, `inbound_checks.*`) — вне scope,
|
||||
остаются YAML-only как сейчас.
|
||||
- **Источник истины после первого изменения — БД.** YAML используется
|
||||
только для одноразового bootstrap при пустой базе; после первого
|
||||
запуска (или после первого API-изменения) YAML для этих 4 секций
|
||||
больше не перечитывается и не переприменяется при рестартах.
|
||||
- **Добавляется базовая аутентификация** — bearer-токен администратора,
|
||||
которым закрывается весь namespace `/api/v1/admin/*` (не только новые
|
||||
write-методы, но и существующие read-методы `status/ips/validators` —
|
||||
единая политика для всего admin-namespace проще и логичнее половинчатой
|
||||
защиты). Протокол `/api/v1/agents/*` и `/api/v1/probers/*`
|
||||
(agent/prober) — вне scope, остаётся как есть.
|
||||
|
||||
## Важное архитектурное ограничение: площадки жёстко капнуты на 3
|
||||
|
||||
Схема `ip_queue` хранит завершённость площадок как три отдельные колонки
|
||||
(`site1_complete`, `site2_complete`, `site3_complete`) — это не список
|
||||
произвольной длины. Поэтому API для `sites` не может быть обычным
|
||||
CRUD-списком: это управление максимум тремя пронумерованными слотами
|
||||
(`index` ∈ {1,2,3}), где `site_id` можно назначить, переименовать или
|
||||
снять со слота. Это ограничение уже описано в `docs/USAGE.md` и явно
|
||||
закладывается в дизайн API ниже, а не игнорируется.
|
||||
|
||||
## Общий план реализации
|
||||
|
||||
### 1. Новая схема БД — `internal/db/migrations/0002_dynamic_config.sql`
|
||||
|
||||
```sql
|
||||
CREATE TABLE sites (
|
||||
idx INTEGER PRIMARY KEY, -- 1, 2 или 3 — фиксированный слот
|
||||
site_id TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE target_groups (
|
||||
group_name TEXT PRIMARY KEY,
|
||||
targets TEXT NOT NULL, -- JSON-массив строк
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE check_types (
|
||||
name TEXT PRIMARY KEY,
|
||||
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||
target_groups TEXT NOT NULL, -- JSON-массив имён групп
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Список целей внутри группы и список групп внутри типа проверки хранятся
|
||||
как JSON-массив в TEXT-колонке (тот же паттерн, что уже используется для
|
||||
`events.payload`) — они всегда читаются/пишутся целиком, отдельная
|
||||
реляционная таблица тут не нужна (не переусложняем).
|
||||
|
||||
`validators` — существующая таблица, новых колонок не требует.
|
||||
|
||||
`internal/db/db.go`: функцию `migrate()` обобщить со списка из одной
|
||||
миграции (`version >= 1 → return`) на упорядоченный список
|
||||
`{version, sql}` и применение всех версий выше текущего
|
||||
`PRAGMA user_version` — понадобится и для этой, и для будущих миграций.
|
||||
|
||||
### 2. Bootstrap-логика — новый файл `internal/db/bootstrap.go`
|
||||
|
||||
```go
|
||||
func (d *DB) BootstrapFromConfig(ctx context.Context, cfg *config.ControlAPI) error
|
||||
```
|
||||
|
||||
Переносит и обобщает то, что сейчас разбросано по
|
||||
`cmd/control-api/main.go` (`RegisterValidator` в цикле + `SeedQueue`):
|
||||
|
||||
- `ip_addresses` → `SeedQueue` — **без изменений**, как сейчас (всегда
|
||||
доливает новые адреса, это уже вне scope доработки).
|
||||
- `validators`, `sites`, `target_groups`, `check_types` — **новая
|
||||
семантика**: применяется, **только если соответствующая таблица
|
||||
сейчас пуста** (`SELECT COUNT(*) ... == 0`). Если в таблице уже есть
|
||||
строки — YAML для этой секции полностью игнорируется, ничего не
|
||||
трогаем. Это и есть «bootstrap один раз, дальше БД главная».
|
||||
|
||||
`internal/db` уже не будет зависеть от `internal/orchestrator` — только
|
||||
новая зависимость `internal/db → internal/config` (обратной зависимости
|
||||
`config → db` нет, циклов не возникает).
|
||||
|
||||
`cmd/control-api/main.go`: заменить текущий цикл `RegisterValidator` +
|
||||
`SeedQueue` одним вызовом `database.BootstrapFromConfig(ctx, cfg)`. Это
|
||||
же делает функцию тестируемой напрямую (используется в обновлённых
|
||||
`orchestrator_test.go`/`httpapi_test.go` вместо ручного построения
|
||||
`Orchestrator.Checks`/`.Sites`).
|
||||
|
||||
**Важное следствие смены семантики валидаторов**: сейчас при каждом
|
||||
рестарте `control-api` валидаторы из YAML переприменяются (в частности,
|
||||
может тихо откатить `os_port_id`, изменённый через API/вручную в БД).
|
||||
После доработки — только на пустой таблице. Это осознанное поведенческое
|
||||
изменение, требует апдейта `docs/SETUP.md`/`docs/USAGE.md` (шаг 6 плана).
|
||||
|
||||
### 3. Запросы к БД для новых сущностей
|
||||
|
||||
Новые файлы, по аналогии с существующими `queries_*.go`:
|
||||
|
||||
- **`internal/db/queries_sites.go`**: `ListSites`, `UpsertSite(idx, siteID)`
|
||||
(проверяет допустимость `idx` 1..3 и уникальность `site_id` до записи,
|
||||
чтобы вернуть чистую типизированную ошибку, а не сырую SQL), `DeleteSite(idx)`,
|
||||
`GetSiteIndex(siteID) (int, error)` — заменяет текущий
|
||||
`Orchestrator.SiteIndexForID`, который сканирует статический слайс.
|
||||
- **`internal/db/queries_targetgroups.go`**: `ListTargetGroups`,
|
||||
`UpsertTargetGroup(name, targets)`, `DeleteTargetGroup(name)` —
|
||||
**перед удалением проверяет**, что ни один `check_types` не ссылается
|
||||
на эту группу (иначе `ErrInUse`), `GetTargetGroup(name)`.
|
||||
- **`internal/db/queries_checktypes.go`**: `ListCheckTypes`,
|
||||
`ListResolvedCheckTypes` (сразу разворачивает имена групп в плоский
|
||||
список URL — то, что раньше строил `orchestrator.New()` один раз при
|
||||
старте), `UpsertCheckType(name, enabled, targetGroups)` (**проверяет**,
|
||||
что все переданные `targetGroups` существуют — иначе `ErrValidation`),
|
||||
`DeleteCheckType(name)`.
|
||||
- **`internal/db/queries_validators.go`** (дополнить существующий файл):
|
||||
`AdminCreateValidator(id, osPortID)` (409/`ErrConflict`, если уже
|
||||
есть), `AdminUpdateValidatorPort(id, osPortID)` (404/`ErrNotFound`,
|
||||
если нет), `DeleteValidator(id)` (409/`ErrBusy`, если
|
||||
`current_ip_id IS NOT NULL` — валидатор сейчас владеет IP). Не путать
|
||||
с существующим `RegisterValidator` — тот остаётся as-is и продолжает
|
||||
использоваться только агентом при самостоятельной регистрации
|
||||
(`handleAgentRegister`), полей `os_port_id` не трогает при
|
||||
self-registration (это уже так в текущем коде).
|
||||
|
||||
Новый файл **`internal/db/errors.go`** с типизированными сентинелами
|
||||
(`ErrNotFound`, `ErrConflict`, `ErrBusy`, `ErrValidation`, через `errors.New`
|
||||
+ `%w`-обёртку в местах возврата) — чтобы `httpapi`-хендлеры мапили их в
|
||||
404/409/400 через `errors.Is`, а не всё подряд в 500 (как сейчас местами
|
||||
получается по умолчанию).
|
||||
|
||||
### 4. Оркестратор — переход на динамическое чтение конфигурации
|
||||
|
||||
`internal/orchestrator/orchestrator.go`:
|
||||
- Убрать поля `Checks []CheckConfig` и `Sites []config.SiteConfig` из
|
||||
`Orchestrator` (сейчас вычисляются один раз в `New()` и застывают на
|
||||
весь жизненный цикл процесса — это и есть корень проблемы). `Inbound`
|
||||
остаётся статическим полем как сейчас (вне scope).
|
||||
- `AssignmentForValidator` — вместо `return item, o.Checks, nil` вызывает
|
||||
`o.DB.ListResolvedCheckTypes(ctx)` и возвращает актуальный на данный
|
||||
момент список.
|
||||
- `SiteIndexForID` — удаляется, вызовы (`handleProberRegister`,
|
||||
`handleProberAssignments`, `handleProberResults`) переходят на
|
||||
`o.DB.GetSiteIndex(ctx, siteID)`.
|
||||
- `expectedCheckCount()` — читает актуальные `ListResolvedCheckTypes` и
|
||||
`ListSites` из БД на момент агрегации, а не статические поля.
|
||||
|
||||
**Принятый компромисс (осознанно, без over-engineering):** если
|
||||
`check_types`/`targets`/`sites` меняются API-запросом ровно в момент,
|
||||
когда чей-то IP уже находится в `checking` (self-check уже пройден,
|
||||
проверки уже назначены агенту), агрегация этой конкретной попытки
|
||||
посчитает *текущую* (уже изменённую) конфигурацию, а не ту, что была на
|
||||
момент выдачи задания. На практике это узкое окно в несколько секунд
|
||||
между админ-изменением и завершением проверки; деградирует безопасно —
|
||||
через существующий механизм `missing_counts_as_fail` результат в худшем
|
||||
случае будет `partial` вместо `pass` для одной попытки, самоисправляется
|
||||
на следующей (после retry/requeue). Полный snapshot-per-attempt (доп.
|
||||
колонки в `ip_queue` с зафиксированным ожидаемым числом проверок) —
|
||||
возможное будущее усиление, не требуется для этой доработки.
|
||||
|
||||
### 5. HTTP API
|
||||
|
||||
Новый файл **`internal/httpapi/handlers_config.go`** и DTO в
|
||||
`dto.go`. Все — под префиксом `/api/v1/admin/config/*`, JSON в
|
||||
snake_case (в отличие от существующих `/admin/status|ips|validators`,
|
||||
которые отдают сырые Go-поля в PascalCase — для новых, «настоящих»
|
||||
management-эндпоинтов сразу делаем нормальный контракт, старые не
|
||||
трогаем, чтобы не ломать уже задокументированное поведение).
|
||||
|
||||
| Метод | Путь | Тело | Успех | Ошибки |
|
||||
|---|---|---|---|---|
|
||||
| GET | `/api/v1/admin/config/validators` | — | `[{validator_id, os_port_id, state}]` | |
|
||||
| POST | `/api/v1/admin/config/validators` | `{validator_id, os_port_id}` | 201 | 409 если уже есть |
|
||||
| PUT | `/api/v1/admin/config/validators/{id}` | `{os_port_id}` | 200 | 404 |
|
||||
| DELETE | `/api/v1/admin/config/validators/{id}` | — | 200 | 404, 409 если владеет IP |
|
||||
| GET | `/api/v1/admin/config/sites` | — | `[{index, site_id}]` (до 3 строк) | |
|
||||
| PUT | `/api/v1/admin/config/sites/{index}` | `{site_id}` | 200 | 400 если `index` не 1..3, 409 если `site_id` занят другим слотом |
|
||||
| DELETE | `/api/v1/admin/config/sites/{index}` | — | 200 | 404 |
|
||||
| GET | `/api/v1/admin/config/targets` | — | `[{name, targets}]` | |
|
||||
| PUT | `/api/v1/admin/config/targets/{group}` | `{targets:[...]}` | 200 | 400 пустой список |
|
||||
| DELETE | `/api/v1/admin/config/targets/{group}` | — | 200 | 404, 409 если используется check_type'ом |
|
||||
| GET | `/api/v1/admin/config/check-types` | — | `[{name, enabled, targets}]` | |
|
||||
| PUT | `/api/v1/admin/config/check-types/{name}` | `{enabled, targets:[group,...]}` | 200 | 400 если группа не существует |
|
||||
| DELETE | `/api/v1/admin/config/check-types/{name}` | — | 200 | 404 |
|
||||
|
||||
`routes.go`: все существующие и новые `/api/v1/admin/*`-маршруты
|
||||
оборачиваются `s.requireAdmin(...)`.
|
||||
|
||||
### 6. Аутентификация
|
||||
|
||||
- `internal/config/config.go`: в `ServerConfig` добавить
|
||||
`AdminTokenEnv string \`yaml:"admin_token_env"\`` — по аналогии с
|
||||
`openstack.*_env` полями (в YAML — только *имя* переменной, не сам
|
||||
токен).
|
||||
- `internal/httpapi/server.go`: `Server.AdminToken string` +
|
||||
`func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc`
|
||||
— сверяет `Authorization: Bearer <token>` через
|
||||
`crypto/subtle.ConstantTimeCompare`. Если `s.AdminToken == ""` —
|
||||
пропускает без проверки (обратная совместимость).
|
||||
- `cmd/control-api/main.go`: если `cfg.Server.AdminTokenEnv` задан, но
|
||||
`os.Getenv(...)` пуст — **отказ запуска** с понятной ошибкой
|
||||
(fail-safe, не запускаемся с «пустым паролем»). Если
|
||||
`AdminTokenEnv` вообще не задан — запускаемся как сейчас, но пишем
|
||||
явный `log.Warn` про незащищённый admin API.
|
||||
- `configs/control-api.example.yaml`, `deploy/systemd/control-api.service`
|
||||
(добавить пример переменной в `EnvironmentFile`) — обновить.
|
||||
|
||||
### 7. Обновление существующих тестов и добавление новых
|
||||
|
||||
- `internal/orchestrator/orchestrator_test.go`,
|
||||
`internal/httpapi/httpapi_test.go`: заменить ручное построение
|
||||
`cfg.CheckTypes/.Targets/.Sites` + прямые поля `Orchestrator{Checks:...}`
|
||||
на `db.BootstrapFromConfig(ctx, cfg)` перед `orchestrator.New(...)` —
|
||||
сами тестовые сценарии (happy path, partial, lease reclaim) не меняются
|
||||
по сути, меняется только способ засеять конфигурацию.
|
||||
- Новые unit-тесты: `internal/db/queries_dynconfig_test.go` (CRUD +
|
||||
граничные случаи: удаление занятого валидатора → `ErrBusy`, удаление
|
||||
группы целей, на которую ссылается check_type → `ErrInUse`,
|
||||
upsert check_type с несуществующей группой → `ErrValidation`, upsert
|
||||
сайта с чужим `site_id` → `ErrConflict`, bootstrap на непустой таблице
|
||||
→ YAML игнорируется).
|
||||
- Новый `internal/httpapi/handlers_config_test.go` (или расширение
|
||||
`httpapi_test.go`): сквозной сценарий — создать валидатора и сайт через
|
||||
API вместо конфига, убедиться, что IP реально дошёл до `done`; смена
|
||||
`check_types` между запусками влияет на следующий назначенный IP.
|
||||
- Обновить `scripts/run-local-e2e.sh` не требуется по сути (bootstrap
|
||||
из YAML при пустой БД работает как раньше), но стоит добавить один шаг
|
||||
с `curl -X PUT .../config/check-types/ssh` как живую демонстрацию.
|
||||
|
||||
### 8. Документация (после реализации)
|
||||
|
||||
- `docs/API.md`: новый раздел «Методы управления конфигурацией» с
|
||||
таблицей выше + примеры curl (создание валидатора, отключение ssh,
|
||||
добавление цели, назначение площадки на слот) + раздел про
|
||||
`Authorization: Bearer`.
|
||||
- `docs/SETUP.md`: шаг про `server.admin_token_env` в
|
||||
«Переменные окружения для OpenStack» (переименовать раздел или
|
||||
добавить рядом «и для admin-токена»); явно описать новую
|
||||
bootstrap-once семантику `validators`/`sites`/`check_types`/`targets`.
|
||||
- `docs/USAGE.md`: заменить текущие разделы «Управление валидаторами» /
|
||||
«Управление площадками» (сейчас там «только через YAML + restart») на
|
||||
актуальные — через API; убрать утверждение «нет API-метода» там, где
|
||||
оно перестало быть верным.
|
||||
- `docs/DIAGRAMS.md`: в диаграмму control plane (раздел 1) добавить
|
||||
новую стрелку «Оператор → HTTP API → БД (config CRUD)» вместо текущей
|
||||
«CFG → читается при старте (инициализация)» как единственного пути.
|
||||
|
||||
## Критичные файлы
|
||||
|
||||
- `internal/db/migrations/0002_dynamic_config.sql` (новый)
|
||||
- `internal/db/db.go` (обобщить `migrate()`)
|
||||
- `internal/db/bootstrap.go` (новый)
|
||||
- `internal/db/errors.go` (новый)
|
||||
- `internal/db/queries_sites.go`, `queries_targetgroups.go`,
|
||||
`queries_checktypes.go` (новые), `queries_validators.go` (дополнить)
|
||||
- `internal/orchestrator/orchestrator.go` (убрать статические
|
||||
`Checks`/`Sites`, читать из БД)
|
||||
- `internal/httpapi/handlers_config.go` (новый), `dto.go`, `routes.go`,
|
||||
`server.go` (`requireAdmin`)
|
||||
- `internal/config/config.go` (`AdminTokenEnv`)
|
||||
- `cmd/control-api/main.go` (bootstrap-вызов, проверка токена при старте)
|
||||
|
||||
## Проверка (когда план будет реализовываться)
|
||||
|
||||
1. `go build ./... && go test ./...` — все существующие + новые unit- и
|
||||
httpapi-тесты проходят.
|
||||
2. `scripts/run-local-e2e.sh` — офлайн-сценарий по-прежнему проходит от
|
||||
начала до конца без ручного вмешательства (bootstrap из YAML при
|
||||
пустой БД работает как раньше).
|
||||
3. Ручная проверка нового контракта: поднять `control-api` с пустой БД и
|
||||
`admin_token_env` без токена → админ-запрос без заголовка проходит;
|
||||
задать токен → запрос без `Authorization` получает 401; создать
|
||||
валидатора/площадку/группу целей/тип проверки через API без
|
||||
единой строчки в YAML, убедиться, что IP реально проходит полный цикл
|
||||
проверки на этой конфигурации; попытаться удалить валидатора, пока он
|
||||
владеет IP → 409; перезапустить `control-api` и убедиться, что
|
||||
API-изменения пережили рестарт, а YAML их не затёр.
|
||||
@@ -0,0 +1,206 @@
|
||||
# План доработки: механизм аутентификации OpenStack-клиента
|
||||
|
||||
> Статус: реализуется (см. коммиты после этого документа). Документ
|
||||
> фиксирует согласованный дизайн доработки `internal/openstack` —
|
||||
> поддержка двух режимов аутентификации (token passthrough / password
|
||||
> auto-obtain) и приведение имён переменных окружения к реальному набору
|
||||
> пользователя.
|
||||
|
||||
## Context
|
||||
|
||||
В ходе разбора выяснилось, что переменные окружения OpenStack нужны не
|
||||
только «токену действовать», а именно **чтобы получить токен** —
|
||||
пользователь предоставляет стандартный набор credentials, принятый в
|
||||
python-openstackclient/RC-файлах:
|
||||
|
||||
```
|
||||
OS_AUTH_URL=https://infra.mail.ru:35357/v3/
|
||||
OS_PROJECT_ID=366fdd4249984226a023f13ac94e226e
|
||||
OS_REGION_NAME=RegionOne
|
||||
OS_USERNAME=a.yurishchev
|
||||
OS_USER_DOMAIN_NAME=users
|
||||
OS_PASSWORD=<секрет>
|
||||
OS_INTERFACE=public
|
||||
OS_IDENTITY_API_VERSION=3
|
||||
```
|
||||
|
||||
Текущий код (`internal/openstack/client.go`) поддерживает только один
|
||||
режим: обменять переданный `OS_TOKEN` на **новый** scoped-токен через
|
||||
`POST /auth/tokens` (identity method `"token"`), потому что всегда
|
||||
выставляет `authOpts.Scope`. Разбор исходников `gophercloud` показал, что
|
||||
библиотека уже умеет и второй, более простой режим: если `Scope` **не**
|
||||
задан, а `TokenID` задан — включается «passthrough»-путь (`v3auth` в
|
||||
`openstack/client.go` пакета gophercloud): токен валидируется через
|
||||
`GET /v3/auth/tokens` (`X-Subject-Token: <тот же токен>`) и **используется
|
||||
как есть** для всех последующих вызовов Neutron — новый токен не
|
||||
выпускается. Это ровно то поведение, которое просит пользователь:
|
||||
«для выполнения API-вызовов следует использовать токен, который передал
|
||||
администратор», без скрытого обмена.
|
||||
|
||||
Итоговое требование: **два режима аутентификации**, выбираемые
|
||||
конфигурацией:
|
||||
1. **`token` (по умолчанию)** — админ передаёт уже готовый, заранее
|
||||
scoped на нужный проект токен через `OS_TOKEN`; код использует его как
|
||||
есть (passthrough), никогда не переобменивает. Автообновление здесь
|
||||
невозможно в принципе (нечем) — если токен истёк, `control-api` упадёт
|
||||
с ошибкой аутентификации, токен нужно перевыпустить и обновить env
|
||||
вручную (это принимается как компромисс данного режима, не баг).
|
||||
2. **`password` (опционально)** — админ передаёт
|
||||
`OS_USERNAME`/`OS_PASSWORD`/`OS_USER_DOMAIN_NAME`; код сам получает
|
||||
токен через обычную password-аутентификацию Keystone v3, с
|
||||
`AllowReauth: true` — токен самообновляется автоматически при 401 в
|
||||
течение всего времени жизни процесса (не ограничен TTL одного токена,
|
||||
в отличие от режима `token`).
|
||||
|
||||
Набор *имён* переменных окружения также приводится в соответствие с
|
||||
реальным набором пользователя (стандартные `OS_*`-имена
|
||||
python-openstackclient), включая ранее не читавшиеся код `OS_USERNAME`,
|
||||
`OS_USER_DOMAIN_NAME`, `OS_PASSWORD`, `OS_INTERFACE`. `OS_PROJECT_NAME` и
|
||||
`OS_PROJECT_DOMAIN_NAME` из текущего кода убираются как неиспользуемые —
|
||||
у пользователя их нет, и они не нужны: в Keystone v3 scope по одному
|
||||
`OS_PROJECT_ID` достаточен, доменной привязки проекта не требует.
|
||||
`OS_IDENTITY_API_VERSION` не потребляется кодом — он и так всегда ходит в
|
||||
Keystone v3 (`AuthenticateV3`/`v3auth`), читать/валидировать эту
|
||||
переменную незачем.
|
||||
|
||||
## Дизайн
|
||||
|
||||
### 1. `internal/openstack/client.go` — новый `ClientConfig` и `NewClient`
|
||||
|
||||
```go
|
||||
type AuthMethod string
|
||||
|
||||
const (
|
||||
AuthMethodToken AuthMethod = "token" // passthrough, по умолчанию
|
||||
AuthMethodPassword AuthMethod = "password" // авто-получение, опционально
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
AuthURL string
|
||||
Method AuthMethod // "" трактуется как AuthMethodToken
|
||||
Token string // для Method == token
|
||||
Username string // для Method == password
|
||||
Password string
|
||||
UserDomainName string
|
||||
ProjectID string
|
||||
Region string
|
||||
Interface string // "public" | "internal" | "admin" — совпадает по значениям с gophercloud.Availability, маппинг не нужен
|
||||
}
|
||||
```
|
||||
|
||||
Вынести сборку `gophercloud.AuthOptions` в отдельную чистую функцию
|
||||
**`buildAuthOptions(cfg ClientConfig) (gophercloud.AuthOptions, error)`**
|
||||
(без сети) — специально, чтобы её можно было покрыть юнит-тестами без
|
||||
реального OpenStack:
|
||||
|
||||
- `Method == AuthMethodPassword`: требует `Username`+`Password`
|
||||
(иначе ошибка); `Username`, `Password`,
|
||||
`DomainName: cfg.UserDomainName`, `Scope: &gophercloud.AuthScope{ProjectID: cfg.ProjectID}`,
|
||||
`AllowReauth: true`.
|
||||
- `Method == AuthMethodToken` (или `""`, по умолчанию): требует `Token`
|
||||
(иначе ошибка); `TokenID: cfg.Token`. **`Scope` намеренно не
|
||||
выставляется** — это и есть переключатель на passthrough-путь в
|
||||
`gophercloud`. `AllowReauth` не выставляется (`gophercloud` сам вернёт
|
||||
ошибку, если включить `AllowReauth` без `Scope` — см. явную проверку в
|
||||
`v3auth`), с комментарием почему.
|
||||
- Неизвестный `Method` — явная ошибка на старте, а не тихий фоллбэк.
|
||||
|
||||
`NewClient` дальше: `AuthenticatedClient(ctx, authOpts)` (без изменений в
|
||||
логике вызова) → резолвинг `networking`-клиента с
|
||||
`gophercloud.EndpointOpts{Region: cfg.Region, Availability: gophercloud.Availability(cfg.Interface)}`
|
||||
(пустая строка `Interface` → явно дефолтить `"public"`).
|
||||
|
||||
### 2. `internal/config/config.go` — `OpenStackConfig`
|
||||
|
||||
Обновить набор *_env-полей до реального использования (индирекция
|
||||
«имя переменной → значение» сохраняется, но список и дефолты приводятся
|
||||
в соответствие):
|
||||
|
||||
```go
|
||||
type OpenStackConfig struct {
|
||||
Mode string `yaml:"mode"` // "mock" | "real"
|
||||
AuthMethod string `yaml:"auth_method"` // "token" (default) | "password"
|
||||
|
||||
AuthURLEnv string `yaml:"auth_url_env"` // default OS_AUTH_URL
|
||||
ProjectIDEnv string `yaml:"project_id_env"` // default OS_PROJECT_ID
|
||||
RegionEnv string `yaml:"region_env"` // default OS_REGION_NAME
|
||||
InterfaceEnv string `yaml:"interface_env"` // default OS_INTERFACE
|
||||
|
||||
TokenEnv string `yaml:"token_env"` // default OS_TOKEN — auth_method: token
|
||||
|
||||
UsernameEnv string `yaml:"username_env"` // default OS_USERNAME — auth_method: password
|
||||
UserDomainNameEnv string `yaml:"user_domain_name_env"` // default OS_USER_DOMAIN_NAME
|
||||
PasswordEnv string `yaml:"password_env"` // default OS_PASSWORD
|
||||
}
|
||||
```
|
||||
|
||||
Убрать `ProjectNameEnv`/`ProjectDomainEnv` (не используются в реальном
|
||||
наборе, Keystone v3 scope по ID их не требует — упрощение, а не потеря
|
||||
функциональности).
|
||||
|
||||
### 3. `cmd/control-api/main.go` — `newOpenStackClient`
|
||||
|
||||
- Читает `cfg.OpenStack.AuthMethod` (`""`/`"token"` → `openstack.AuthMethodToken`,
|
||||
`"password"` → `openstack.AuthMethodPassword`, иначе — отказ старта).
|
||||
- Для `token`: требует непустой `os.Getenv(TokenEnv)` — иначе явная
|
||||
ошибка старта с именем недостающей переменной.
|
||||
- Для `password`: требует непустые `Username`/`Password` — иначе явная
|
||||
ошибка старта.
|
||||
- `AuthURL`/`ProjectID`/`Region` — обязательны в обоих режимах, как и
|
||||
сейчас.
|
||||
- `Interface` — необязателен (пустая строка = `public` по умолчанию, см.
|
||||
выше).
|
||||
|
||||
### 4. Конфиг и документация
|
||||
|
||||
- `configs/control-api.example.yaml`: секция `openstack` переписывается
|
||||
под новый набор полей и под пример из реального использования (два
|
||||
примера в комментариях — `auth_method: token` и `auth_method: password`).
|
||||
- `deploy/systemd/control-api.service` / `docs/SETUP.md`: обновить
|
||||
пример `control-api.env` под оба режима, объяснить компромисс режима
|
||||
`token` (нет автообновления, нужен ручной перевыпуск при истечении) vs
|
||||
`password` (самообновляется, но требует хранить пароль в env-файле).
|
||||
- `docs/API.md`/`docs/DIAGRAMS.md` не затрагиваются — это внутренний
|
||||
механизм клиента, наружу в HTTP API не выходит.
|
||||
|
||||
### 5. Тесты
|
||||
|
||||
- Новый `internal/openstack/client_test.go`: юнит-тесты **чистой**
|
||||
функции `buildAuthOptions` — без сети, без `OPENSTACK_LIVE_TEST`:
|
||||
- `token`: `Scope == nil`, `TokenID` равен переданному, `AllowReauth == false`.
|
||||
- `token` без `Token` → ошибка.
|
||||
- `password`: `Scope != nil` и `Scope.ProjectID` верный, `AllowReauth == true`, `Username`/`Password`/`DomainName` прокинуты.
|
||||
- `password` без `Username`/`Password` → ошибка.
|
||||
- неизвестный `Method` → ошибка.
|
||||
- `internal/openstack/client_live_test.go` (уже существует, гейтится
|
||||
`OPENSTACK_LIVE_TEST=1`): расширить, чтобы читал `OS_AUTH_METHOD`
|
||||
(`token`/`password`) и гонял `GetFloatingIPByAddress` в обоих режимах,
|
||||
если для них заданы переменные — ручная проверка соответствия дизайна
|
||||
реальному Keystone (Mail.ru/VK Cloud), не часть обычного `go test ./...`.
|
||||
|
||||
## Критичные файлы
|
||||
|
||||
- `internal/openstack/client.go` (основная переработка)
|
||||
- `internal/openstack/client_test.go` (новый)
|
||||
- `internal/openstack/client_live_test.go` (расширение)
|
||||
- `internal/config/config.go` (`OpenStackConfig`)
|
||||
- `cmd/control-api/main.go` (`newOpenStackClient`)
|
||||
- `configs/control-api.example.yaml`, `docs/SETUP.md`,
|
||||
`deploy/systemd/control-api.service`
|
||||
|
||||
## Проверка
|
||||
|
||||
1. `go build ./... && go test ./...` — новые юнит-тесты
|
||||
`buildAuthOptions` проходят офлайн вместе со всем остальным.
|
||||
2. `scripts/run-local-e2e.sh` не затрагивается (использует
|
||||
`openstack.mode: mock`, минует `NewClient` целиком) — должен
|
||||
по-прежнему проходить без изменений в поведении.
|
||||
3. Ручная проверка с реальными данными пользователя (вне автоматических
|
||||
тестов, наружу секреты не публикуются): поднять `control-api` с
|
||||
`openstack.mode: real`, `auth_method: token`, реальным `OS_TOKEN` —
|
||||
убедиться, что `GetFloatingIPByAddress`/associate/disassociate проходят
|
||||
без 401; затем повторить с `auth_method: password` и
|
||||
`OS_USERNAME`/`OS_PASSWORD`/`OS_USER_DOMAIN_NAME` — сверить, что оба
|
||||
режима реально авторизуются в `https://infra.mail.ru:35357/v3/` и
|
||||
работают с проектом `366fdd4249984226a023f13ac94e226e` в
|
||||
`RegionOne`.
|
||||
+39
-5
@@ -184,23 +184,57 @@ cp configs/control-api.example.yaml /etc/cloud-ip-validator/control-api.yaml
|
||||
### 2. Переменные окружения для OpenStack
|
||||
|
||||
Учётные данные передаются **только** через переменные окружения — никогда
|
||||
через YAML. Создайте файл (доступный на чтение только сервисному
|
||||
пользователю):
|
||||
через YAML. Есть два режима аутентификации, выбираются полем
|
||||
`openstack.auth_method` в `control-api.yaml`:
|
||||
|
||||
**`auth_method: "token"` (по умолчанию)** — вы предоставляете уже
|
||||
готовый, заранее scoped на нужный проект токен (например, полученный
|
||||
через `openstack --os-project-id=<id> token issue`). Control-api
|
||||
использует его как есть, ни на что не обменивает. Просто, но токен не
|
||||
самообновляется: когда он истечёт, `control-api` начнёт получать ошибки
|
||||
от OpenStack API, пока вы вручную не перевыпустите токен, не обновите
|
||||
переменную и не перезапустите процесс.
|
||||
|
||||
```bash
|
||||
install -m 0600 -o cloud-ip-validator -g cloud-ip-validator /dev/null /etc/cloud-ip-validator/control-api.env
|
||||
cat >> /etc/cloud-ip-validator/control-api.env <<'EOF'
|
||||
OS_AUTH_URL=https://keystone.example.com:5000/v3
|
||||
OS_TOKEN=<токен администратора с правами на управление floating IP>
|
||||
OS_TOKEN=<токен администратора, уже scoped на сервисный проект>
|
||||
OS_PROJECT_ID=<id сервисного проекта>
|
||||
OS_REGION_NAME=<регион>
|
||||
EOF
|
||||
```
|
||||
|
||||
**`auth_method: "password"`** — вы предоставляете обычные логин/пароль;
|
||||
control-api сам получает токен через Keystone и автоматически
|
||||
переполучает новый при истечении текущего (весь срок жизни процесса, без
|
||||
ручного вмешательства). Компромисс — в файле окружения лежит долгоживущий
|
||||
пароль, а не токен.
|
||||
|
||||
```bash
|
||||
install -m 0600 -o cloud-ip-validator -g cloud-ip-validator /dev/null /etc/cloud-ip-validator/control-api.env
|
||||
cat >> /etc/cloud-ip-validator/control-api.env <<'EOF'
|
||||
OS_AUTH_URL=https://keystone.example.com:5000/v3
|
||||
OS_PROJECT_ID=<id сервисного проекта>
|
||||
OS_REGION_NAME=<регион>
|
||||
OS_USERNAME=<логин>
|
||||
OS_USER_DOMAIN_NAME=<домен пользователя>
|
||||
OS_PASSWORD=<пароль>
|
||||
EOF
|
||||
```
|
||||
и в `control-api.yaml`: `openstack.auth_method: "password"`.
|
||||
|
||||
Опционально в обоих режимах — `OS_INTERFACE` (`public`/`internal`/`admin`,
|
||||
по умолчанию `public`): выбирает, какой из адресов Neutron в каталоге
|
||||
сервисов использовать, если у эндпоинта их несколько.
|
||||
|
||||
Имена переменных должны совпадать с тем, что указано в
|
||||
`control-api.yaml` в секции `openstack` (`auth_url_env`, `token_env` и
|
||||
т.д.) — в шаблоне это ровно `OS_AUTH_URL`, `OS_TOKEN`, `OS_PROJECT_ID`,
|
||||
`OS_PROJECT_NAME`, `OS_PROJECT_DOMAIN_NAME`, `OS_REGION_NAME`.
|
||||
т.д.) — в шаблоне это ровно `OS_AUTH_URL`, `OS_PROJECT_ID`,
|
||||
`OS_REGION_NAME`, `OS_INTERFACE`, и, в зависимости от режима, либо
|
||||
`OS_TOKEN`, либо `OS_USERNAME`/`OS_USER_DOMAIN_NAME`/`OS_PASSWORD` — это
|
||||
стандартные имена, принятые в python-openstackclient/RC-файлах, менять их
|
||||
обычно не требуется.
|
||||
|
||||
### 3. `validator-agent.yaml` (свой на каждом валидаторе)
|
||||
|
||||
|
||||
Reference in new issue
Block a user