Skip to content

Commit 0a4adc1

Browse files
committed
Add useful develop docs for humans and AIs
1 parent 6864815 commit 0a4adc1

9 files changed

Lines changed: 388 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ All source files must include Apache 2.0 license header:
179179

180180
### Documentation
181181

182+
- Before working on element/manifest/reconciliation internals, read
183+
`docs/core-developer-guide/index.md` (element/resource model, `ElementEngine`,
184+
reconciliation loop, manifest value rendering).
185+
- Before debugging a stuck element, node, or config delivery, read
186+
`docs/usage/troubleshooting.md`.
182187
- Update `docs/` for CLI changes
183188
- Run `tox -e cli_docs` to regenerate CLI docs
184189
- Run `make mdlint` for Markdown linting
@@ -188,5 +193,6 @@ All source files must include Apache 2.0 license header:
188193

189194
- **Source**: `exordos_core/`
190195
- **Tests**: `exordos_core/tests/`
191-
- **Documentation**: `docs/`
196+
- **Documentation**: `docs/` — see `docs/core-developer-guide/index.md` (architecture)
197+
and `docs/usage/troubleshooting.md` (debugging) first
192198
- **Build output**: `output/`

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

docs/core-developer-guide/index.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
title: Core Developer Guide
3+
---
4+
5+
This guide describes the internal architecture of Exordos Core: how elements, manifests, and resources are
6+
stored and reconciled onto compute nodes. It's aimed at people working on Exordos Core itself. If you want
7+
to build an element on top of the platform instead, see the
8+
[Application Developer Guide](../app-developer-guide/index.md).
9+
10+
## Elements, manifests, and resources
11+
12+
Installing an element (`exordos em elements install <manifest.yaml>`) creates a handful of related
13+
records, all defined in `exordos_core/elements/dm/models.py`:
14+
15+
| Model | Table | Purpose |
16+
|---|---|---|
17+
| `Manifest` | `em_manifests` | The parsed YAML manifest (resources, imports, exports, requirements) |
18+
| `Element` | `em_elements` | The installed element (name, version, status) |
19+
| `Resource` | `em_resources` | A single declared resource inside an element |
20+
| `Import` | `em_imports` | Which resource of which element is imported |
21+
| `Export` | `em_exports` | A resource published for other elements to import |
22+
23+
## The element engine
24+
25+
`ElementEngine` is an in-memory registry of every installed element and its resources, loaded from the
26+
database via `load_from_database()`. Each element is a `Namespace`, and resources inside it are addressed
27+
by their link string (for example `$my_element.compute.nodes.$my_node`). The registry is reloaded whenever
28+
a manifest is installed, upgraded, or uninstalled, and lazily on the reconciliation loop's first iteration.
29+
30+
## Reconciliation: from declared resource to running node
31+
32+
Reconciliation is driven by `ElementManagerBuilder` (`elements/services/builders.py`), a service loop that
33+
ticks roughly every 3 seconds:
34+
35+
1. Iterate every resource known to the element engine.
36+
2. Render the resource's `value` into a `target_state`, resolving `$link` references and `f"..."`
37+
interpolations (see [Manifest value rendering](#manifest-value-rendering)).
38+
3. Create or update a `TargetResource` row (table `ua_target_resources`, from `gcl_sdk`) — this is the
39+
contract with the node-side agent.
40+
4. A Universal Agent running on the target compute node watches `ua_target_resources` for records of its
41+
own `kind`, applies them to the real system (systemd unit, VM, disk, config file, and so on), and
42+
writes back an `actual_resource`.
43+
5. `ElementManagerBuilder` compares the hash of `actual_resource` against `target_state` and updates
44+
`Resource.status` accordingly.
45+
6. An element's overall status is derived from the status of all its resources.
46+
47+
## Status lifecycle
48+
49+
`Element` and `Resource` share a `Status` enum with three values: `NEW → IN_PROGRESS → ACTIVE`. There is no
50+
`ERROR` state at this level — a resource that can't converge simply stays `IN_PROGRESS`.
51+
52+
Some resource kinds track a richer lifecycle of their own. `Service` (table `em_services`) and `Config`
53+
(table `config_configs`) both add an `ERROR` status alongside `NEW`/`IN_PROGRESS`/`ACTIVE`, since those
54+
subsystems can detect and report a failed apply.
55+
56+
## Manifest value rendering
57+
58+
`_render_value` (`elements/dm/models.py`) turns a manifest string into a concrete value:
59+
60+
- A string starting with `$` is resolved as a resource link.
61+
- A string starting with `f"` is treated as an inline template: `{$element.type.$name:field}`
62+
placeholders inside it are substituted.
63+
- Any other string is returned unchanged.
64+
65+
A manifest author who forgets the `f"` prefix does not get a silently empty value — the literal `{$...}`
66+
text is left in the rendered output, wherever that value ends up (a config file, a service command line,
67+
and so on). See [Troubleshooting](../usage/troubleshooting.md) for the symptoms this produces.
68+
69+
## Core resource types
70+
71+
- `$core.compute.nodes` / `$core.compute.sets` — virtual machines and node groups (KVM/QEMU)
72+
- `$core.em.services` — systemd services on a node or node set
73+
- `$core.vs.variables` — the Variable Store, used for platform-wide defaults
74+
- `$core.config.configs` — files delivered to a node; requires `project_id` and `body.kind`, see
75+
[Troubleshooting](../usage/troubleshooting.md)
76+
77+
## See also
78+
79+
- [Manifest reference](../em/manifest.md)
80+
- [Service as a Service API](../em/service.md)
81+
- [Troubleshooting](../usage/troubleshooting.md)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
title: Руководство для разработчиков ядра
3+
---
4+
5+
Это руководство описывает внутреннюю архитектуру Exordos Core: как элементы, манифесты и ресурсы
6+
хранятся и приводятся в соответствие (reconcile) на вычислительных нодах. Оно предназначено для тех, кто
7+
работает над самим Exordos Core. Если вы хотите разработать элемент поверх платформы, см.
8+
[Руководство для разработчиков приложений](../app-developer-guide/index.ru.md).
9+
10+
## Элементы, манифесты и ресурсы
11+
12+
Установка элемента (`exordos em elements install <manifest.yaml>`) создаёт набор связанных записей,
13+
описанных в `exordos_core/elements/dm/models.py`:
14+
15+
| Модель | Таблица | Назначение |
16+
|---|---|---|
17+
| `Manifest` | `em_manifests` | Разобранный YAML-манифест (resources, imports, exports, requirements) |
18+
| `Element` | `em_elements` | Установленный элемент (имя, версия, статус) |
19+
| `Resource` | `em_resources` | Отдельный задекларированный ресурс внутри элемента |
20+
| `Import` | `em_imports` | Какой ресурс какого элемента импортирован |
21+
| `Export` | `em_exports` | Ресурс, опубликованный для импорта другими элементами |
22+
23+
## Element engine
24+
25+
`ElementEngine` — реестр в памяти со всеми установленными элементами и их ресурсами, загружаемый из базы
26+
данных через `load_from_database()`. Каждый элемент представлен как `Namespace`, а ресурсы внутри него
27+
адресуются по строке-ссылке (например, `$my_element.compute.nodes.$my_node`). Реестр перезагружается при
28+
установке, обновлении или удалении манифеста, а также лениво — на первой итерации цикла reconciliation.
29+
30+
## Reconciliation: от задекларированного ресурса до работающей ноды
31+
32+
Reconciliation выполняет `ElementManagerBuilder` (`elements/services/builders.py`) — сервисный цикл,
33+
тикающий примерно раз в 3 секунды:
34+
35+
1. Перебрать все ресурсы, известные element engine.
36+
2. Отрендерить `value` ресурса в `target_state`, разрешив ссылки `$link` и интерполяции `f"..."` (см.
37+
[Рендеринг значений манифеста](#рендеринг-значений-манифеста)).
38+
3. Создать или обновить запись `TargetResource` (таблица `ua_target_resources` из `gcl_sdk`) — это
39+
контракт с агентом на стороне ноды.
40+
4. Universal Agent, работающий на целевой вычислительной ноде, следит за `ua_target_resources` на предмет
41+
записей своего `kind`, применяет их к реальной системе (systemd-юнит, VM, диск, конфигурационный файл
42+
и т.д.) и записывает обратно `actual_resource`.
43+
5. `ElementManagerBuilder` сравнивает хэш `actual_resource` с `target_state` и обновляет
44+
`Resource.status` соответственно.
45+
6. Итоговый статус элемента выводится из статусов всех его ресурсов.
46+
47+
## Жизненный цикл статусов
48+
49+
`Element` и `Resource` используют общий enum `Status` с тремя значениями: `NEW → IN_PROGRESS → ACTIVE`.
50+
На этом уровне нет статуса `ERROR` — ресурс, который не может сойтись, просто остаётся в `IN_PROGRESS`.
51+
52+
У некоторых типов ресурсов есть собственный, более богатый жизненный цикл. `Service` (таблица
53+
`em_services`) и `Config` (таблица `config_configs`) добавляют статус `ERROR` к `NEW`/`IN_PROGRESS`/
54+
`ACTIVE`, поскольку эти подсистемы способны обнаружить и сообщить о неудачном применении.
55+
56+
## Рендеринг значений манифеста
57+
58+
`_render_value` (`elements/dm/models.py`) превращает строку из манифеста в конкретное значение:
59+
60+
- Строка, начинающаяся с `$`, разрешается как ссылка на ресурс.
61+
- Строка, начинающаяся с `f"`, трактуется как inline-шаблон: плейсхолдеры `{$element.type.$name:field}`
62+
внутри неё подставляются.
63+
- Любая другая строка возвращается без изменений.
64+
65+
Автор манифеста, забывший префикс `f"`, не получает молча пустое значение — буквальный текст `{$...}`
66+
остаётся в результирующем выводе, где бы это значение ни оказалось (в конфигурационном файле, в командной
67+
строке сервиса и т.д.). См. [Диагностику проблем](../usage/troubleshooting.ru.md) — там описаны симптомы,
68+
к которым это приводит.
69+
70+
## Основные типы ресурсов ядра
71+
72+
- `$core.compute.nodes` / `$core.compute.sets` — виртуальные машины и группы нод (KVM/QEMU)
73+
- `$core.em.services` — systemd-сервисы на ноде или группе нод
74+
- `$core.vs.variables` — Variable Store, используется для платформенных значений по умолчанию
75+
- `$core.config.configs` — файлы, доставляемые на ноду; требует `project_id` и `body.kind`, см.
76+
[Диагностику проблем](../usage/troubleshooting.ru.md)
77+
78+
## См. также
79+
80+
- [Справочник по манифесту](../em/manifest.ru.md)
81+
- [Service as a Service API](../em/service.md)
82+
- [Диагностика проблем](../usage/troubleshooting.ru.md)

docs/usage/local_deployment.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,19 @@ ssh ubuntu@10.20.0.2
107107
Use the admin credentials to obtain an access token from the IAM service:
108108

109109
```bash
110-
curl --location 'http://10.20.0.2:11010/v1/iam/clients/00000000-0000-0000-0000-000000000000/actions/get_token/invoke' \
110+
curl --location 'http://10.20.0.2:80/api/core/v1/iam/clients/default/actions/get_token/invoke' \
111111
--header 'Content-Type: application/x-www-form-urlencoded' \
112112
--data-urlencode 'grant_type=password' \
113113
--data-urlencode 'username=<ADMIN_USERNAME>' \
114114
--data-urlencode 'password=<ADMIN_PASSWORD>' \
115-
--data-urlencode 'client_id=ExordosCoreClientId' \
116-
--data-urlencode 'client_secret=ExordosCoreSecret' \
117115
--data-urlencode 'scope=' \
118116
--data-urlencode 'ttl=86400'
119117
```
120118

121-
The response contains an `access_token` field. Use this token as a `Bearer` token in all subsequent API requests.
119+
The `clients/default` path is rewritten by Core's load balancer (port 80) to the real IAM client and has
120+
`X-Client-Id`/`X-Client-Secret` injected automatically, so no client credentials need to be passed here —
121+
see the equivalent setup in `exordos_ecosystem/web/README.md`. The response contains an `access_token`
122+
field. Use this token as a `Bearer` token in all subsequent API requests.
122123

123124
### CLI access
124125

docs/usage/local_deployment.ru.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,19 @@ ssh ubuntu@10.20.0.2
107107
Используйте учётные данные администратора для получения токена доступа от сервиса IAM:
108108

109109
```bash
110-
curl --location 'http://10.20.0.2:11010/v1/iam/clients/00000000-0000-0000-0000-000000000000/actions/get_token/invoke' \
110+
curl --location 'http://10.20.0.2:80/api/core/v1/iam/clients/default/actions/get_token/invoke' \
111111
--header 'Content-Type: application/x-www-form-urlencoded' \
112112
--data-urlencode 'grant_type=password' \
113113
--data-urlencode 'username=<ADMIN_USERNAME>' \
114114
--data-urlencode 'password=<ADMIN_PASSWORD>' \
115-
--data-urlencode 'client_id=ExordosCoreClientId' \
116-
--data-urlencode 'client_secret=ExordosCoreSecret' \
117115
--data-urlencode 'scope=' \
118116
--data-urlencode 'ttl=86400'
119117
```
120118

121-
В ответе содержится поле `access_token`. Используйте этот токен как `Bearer`-токен во всех последующих запросах к API.
119+
Путь `clients/default` переписывается балансировщиком Core (порт 80) на реальный IAM-клиент, а
120+
`X-Client-Id`/`X-Client-Secret` подставляются автоматически — учётные данные клиента передавать не нужно,
121+
см. аналогичную настройку в `exordos_ecosystem/web/README.md`. В ответе содержится поле `access_token`.
122+
Используйте этот токен как `Bearer`-токен во всех последующих запросах к API.
122123

123124
### Доступ через CLI
124125

docs/usage/troubleshooting.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
title: Troubleshooting
3+
---
4+
5+
This page collects common issues when developing and deploying elements on Exordos Core, and how to diagnose them.
6+
7+
## A resource is stuck in `IN_PROGRESS`
8+
9+
An element's resources go through the lifecycle `NEW → IN_PROGRESS → ACTIVE`. If a resource stays in
10+
`IN_PROGRESS`, the platform has not been able to reconcile the declared state with reality yet.
11+
12+
Check the resource state:
13+
14+
```bash
15+
exordos em resources list
16+
exordos em resources show <uuid>
17+
```
18+
19+
If the resource never leaves `IN_PROGRESS`, the compute node it targets may never have come up. Check the
20+
node and hypervisor status:
21+
22+
```bash
23+
exordos compute nodes list
24+
exordos compute hypervisors list
25+
```
26+
27+
See [Local Deployment](local_deployment.md) for hypervisor setup requirements.
28+
29+
## `$core.config.configs` fails instead of rendering
30+
31+
Both `project_id` and `body.kind` are required fields on a `Config` resource — neither has a default.
32+
Omitting `body.kind` (for example `text`) makes the manifest fail with an `UnknownType: Unknown kind for
33+
value: ...` error instead of producing an empty or partial file.
34+
35+
```yaml
36+
$core.config.configs:
37+
my_config:
38+
project_id: "12345678-c625-4fee-81d5-f691897b8142"
39+
path: /etc/my_element_init.txt
40+
target:
41+
kind: node
42+
node: $core.compute.nodes.$my_node:uuid
43+
body:
44+
kind: text # required, no default
45+
content: |
46+
...
47+
```
48+
49+
## Values inside a config body are not substituted
50+
51+
Manifest string values are only interpolated in two cases:
52+
53+
- A value starting with `$` is treated as a full resource link, for example
54+
`"$core.vs.variables.$default_cores:value"`.
55+
- A value starting with `f"` is treated as an inline template, where `{$element.type.$name:field}`
56+
placeholders are substituted inside a larger string.
57+
58+
Any other string — including a multi-line `content:` block — is passed through verbatim. If you forget the
59+
`f"` prefix, the `{$...}` placeholders are **not** silently dropped: they stay in the rendered output as
60+
literal text. If a delivered file ends up containing `{$core.secret.passwords.$my_password:value}` instead
61+
of an actual password, this is almost always a missing `f"` prefix.
62+
63+
```yaml
64+
body:
65+
kind: text
66+
content: |
67+
f"MY_PASS={$core.secret.passwords.$my_password:value}"
68+
```
69+
70+
## Bootstrap script reads an empty config file
71+
72+
Content delivered through `$core.config.configs` is written to the target node as a plain file write: the
73+
file is created (truncated) first, and the content is written afterwards — this is not an atomic
74+
operation. A bootstrap script that starts before the content has arrived reads an empty file.
75+
76+
Always wait for the file to be **non-empty**, not just for it to exist:
77+
78+
```bash
79+
while [ ! -s /etc/my_element_init.txt ]; do
80+
echo "Waiting for config..."
81+
sleep 2
82+
done
83+
source /etc/my_element_init.txt
84+
```
85+
86+
If a script already checks `-s` and still reads an empty file, compare the content stored in the database
87+
with what is on disk:
88+
89+
```bash
90+
psql exordos_core -c "SELECT body FROM config_configs WHERE path = '/etc/my_element_init.txt';"
91+
ssh ubuntu@<node-ip> "sudo cat /etc/my_element_init.txt"
92+
```
93+
94+
If the database value is empty, the manifest itself has a rendering problem — see the previous section. If
95+
the database has content but the file on disk doesn't, the node hasn't received it yet — wait, or re-run
96+
the bootstrap script manually once it has.
97+
98+
## See also
99+
100+
- [Manifest reference](../em/manifest.md)
101+
- [Core Developer Guide](../core-developer-guide/index.md)

0 commit comments

Comments
 (0)