Перейти к содержимому

Destiny

Destiny — декларативное описание желаемого состояния одного хоста: набор идемпотентных шагов вида core.<module>.<state>. Это атомарный кирпичик Soul Stack — «как привести один хост в нужное состояние», без оркестрации (на каких именно хостах и в каком порядке — это уже Scenario).

Destiny изолирован: он видит только свой объявленный вход (input:) и факты собственного хоста. Он не знает про базу, про вызвавший его сценарий и не подсматривает чужой контекст.

Destiny — это папка в git-репозитории:

destiny-<name>/
├── destiny.yml # манифест: input/output-контракты
├── tasks/
│ ├── main.yml # точка входа: список задач
│ └── <sub>.yml # подключаемые соседи (include)
├── templates/ # шаблоны файлов (.tmpl)
└── vars.yml # локальные значения автора destiny

tasks/main.yml — это список задач, исполняемых сверху вниз. Никакой обёртки на верхнем уровне нет: путь к файлу сам сообщает контекст.

Минимальная задача адресует один модуль в форме core.<module>.<state> и передаёт параметры:

- name: Install redis-server package
module: core.pkg.installed
params:
name: redis-server

Имя модуля трёхуровневое: core.pkg.installed читается как «пакет установлен» — это желаемое состояние, а не команда «установи». Доступные модули и их состояния — в каталоге core-модулей.

Каждый шаг идемпотентен: модуль сначала проверяет текущее состояние хоста и меняет его, только если оно отличается от желаемого. Результат шага — changed=true (состояние поменялось) или changed=false (уже было таким). При повторном прогоне достигнутое состояние ничего не меняет.

changed-статус — это сигнал для зависимостей между шагами (например, «перезапусти сервис, только если изменился конфиг»).

Помимо module: и params:, задача поддерживает блоки управления. Наиболее употребимые:

БлокНазначение
name:Человекочитаемое имя шага (идёт в лог применения).
when:Условие (CEL): задача выполняется, только если выражение истинно.
register:Имя, под которым сохраняется результат шага для последующих задач.
vars:Локальные значения, доступные внутри задачи.
loop:Повтор шага по элементам коллекции.
changed_when:Переопределяет, считать ли шаг изменившим состояние.
failed_when:Переопределяет, считать ли шаг упавшим.
retry: / timeout:Повторные попытки и лимит времени на попытку.
no_log:Скрыть параметры/результат шага из логов (для секретов).

Зависимости выражаются через register:-имена других задач:

БлокСемантика
require:Не стартовать до завершения упомянутых задач (барьер).
onchanges:Выполнить, только если упомянутая задача изменила состояние.
onfail:Выполнить, только если упомянутая задача упала (rescue/cleanup).

Классический пример — перезапуск сервиса по изменению конфига:

- name: Render redis.conf
module: core.file.present
register: redis_conf
params:
path: /etc/redis/redis.conf
content: "..."
- name: Restart redis-server because config changed
module: core.service.restarted
onchanges: [redis_conf]
params:
name: redis-server

Перезапуск произойдёт, только если рендер конфига сообщил changed=true.

  • block: — inline-группа из нескольких задач с общим условием when:: если оно ложно, пропускается вся группа разом, без повтора условия на каждой задаче.
  • include: — подключение соседнего файла из той же папки tasks/: его задачи вклеиваются на место include-задачи. Для переиспользуемых или больших групп.

Файл, который собирается из шаблона, рендерит шаг core.file.rendered. Значения для шаблона явно поднимаются в vars: шага — это единственный канал «прокинуть данные в файл»:

- name: Render redis.conf from template
module: core.file.rendered
params:
path: /etc/redis/redis.conf
template: templates/redis.conf.tmpl
vars:
maxmemory: "${ essence.redis.maxmemory }"
mode: "0640"

Внутри templates/redis.conf.tmpl поднятое значение доступно как {{ .vars.maxmemory }}. Шаблон видит только то, что автор явно поднял, плюс узкий набор фактов хоста — не весь контекст. Подробнее про границу движков — в обзоре шаблонизатора.

Destiny объявляет два опциональных контракта в destiny.yml — симметричных по форме:

  • input: — что Destiny принимает снаружи. Те, кто вызывает Destiny, обязаны передать значения по этому контракту; они валидируются (типы, обязательность, pattern/enum) до применения. В задачах вход доступен как input.<имя>.
  • output: — что Destiny публикует наружу как свой результат. Top-level output: объявляет схему результата, а задачи заполняют объявленные поля через свой task-level output:. Destiny публикует свой результат, но никогда не читает чужой контекст — изоляция при этом не нарушается.
destiny.yml
input:
version:
type: string
required: true
maxmemory:
type: string
output:
config_path:
type: string

Если Destiny ничего не возвращает, блок output: опускается — вызвавший всё равно получает стандартные changed / failed.

В выражениях Destiny доступны:

ИмяСодержание
input.<имя>Значения объявленного входного контракта.
vars.<имя>Локальные значения автора Destiny (vars.yml + task-level).
soulprint.self.<путь>Факты собственного хоста: soulprint.self.os.family, soulprint.self.network.primary_ip, …
register.<имя>.*Результаты предыдущих шагов: .changed, .failed и поля из их output:.

Чего в контексте Destiny нет (это уровень сценария): прямого доступа к essence.*, к атрибутам вызывающего и кросс-хостовых запросов к фактам других хостов. Эти значения Destiny получает только через свой input:.

tasks/main.yml
- name: Install redis-server package
module: core.pkg.installed
retry: { count: 3, delay: 10s } # сеть может моргнуть
params:
name: redis-server
version: "${ input.version }"
- name: Render redis.conf from template
module: core.file.rendered
register: redis_conf
params:
path: /etc/redis/redis.conf
template: templates/redis.conf.tmpl
vars:
maxmemory: "${ input.maxmemory }"
mode: "0640"
- name: Ensure redis-server is running and enabled at boot
module: core.service.running
params:
name: redis-server
enabled: true
- name: Restart redis-server because config changed
module: core.service.restarted
onchanges: [redis_conf]
timeout: 30s
params:
name: redis-server