core.file
Управление файлами и каталогами: содержимое/права/владелец, рендер из шаблона, создание каталога, удаление.
core.file приводит файл или каталог к нужному виду четырьмя состояниями: present (содержимое inline через content или копией с хоста через src), rendered (рендер text/template-шаблона), directory (каталог с атрибутами) и absent. Идемпотентно по содержимому (сверка SHA-256) и атрибутам mode/owner/group — совпало всё, changed=false. Заменяет отдельные core.copy и core.template (ADR-015). Soul-side: запись за пределы /var/lib/soul-stack/ требует fs_write_root, для системных путей на практике root.
Требования
- Rootтребуется
- Сторона
soul-side - Коллекция
soulstack.files - Категория
files
fs_write_rootСостояния
core.file.present — Файл существует с заданным содержимым (inline content или копия src) плюс mode/owner/group.
Файла не было, либо отличается содержимое (сверка по SHA-256; для src — sha256 байт src), mode или владелец/группа.
Содержимое, mode и владелец/группа уже совпадают.
Параметры
| Параметр | Тип | Обяз. / дефолт | Описание |
|---|---|---|---|
path | string | required | Целевой путь файла. |
content | string | optional | Содержимое файла (inline). Взаимоисключается с src; ни одного — пустой файл. |
src | string | optional | Абсолютный путь regular-файла на хосте, содержимое копируется в path (типично результат core.archive.extracted). Задаёт только содержимое, не атрибуты источника. Взаимоисключается с content. |
mode | string | optional | Права в восьмеричной форме, напр. "0640". |
owner | string | optional | Владелец (имя пользователя). |
group | string | optional | Группа-владелец (имя группы). |
Пример — present с inline-content (роль core.copy)
- name: Drop a static marker file module: core.file.present params: path: /etc/soul-stack/marker content: "managed by soul-stack" mode: "0644" owner: root group: rootOutput
| Поле | Тип | Описание |
|---|---|---|
path | string | |
sha256 | string | |
mode | string | |
installed | true |
core.file.absent — Файл удалён.
Файл был и удалён.
Файла нет.
Параметры
| Параметр | Тип | Обяз. / дефолт | Описание |
|---|---|---|---|
path | string | required | Путь удаляемого файла. |
Пример — Удалить файл
- name: Remove stale marker module: core.file.absent params: path: /etc/soul-stack/markerOutput
| Поле | Тип | Описание |
|---|---|---|
path | string | |
installed | false |
core.file.directory — Каталог существует с заданными mode/owner/group (декларативная замена core.exec.run install -d).
Каталога не было (создан) либо дрейфят mode или владелец/группа (чинятся chmod/chown). Путь занят файлом — ошибка, без перезаписи.
Каталог существует, mode и владелец/группа совпадают.
Параметры
| Параметр | Тип | Обяз. / дефолт | Описание |
|---|---|---|---|
path | string | required | Целевой путь каталога. |
mode | string | optional | Права в восьмеричной форме, напр. "0755". |
owner | string | optional | Владелец (имя пользователя). |
group | string | optional | Группа-владелец (имя группы). |
parents | bool | optional | Создавать промежуточные каталоги (семантика mkdir -p). Default false. |
Пример — directory с цепочкой родителей (parents: true = mkdir -p)
- name: Ensure exporter data directory module: core.file.directory params: path: /var/lib/node_exporter/textfile parents: true mode: "0755" owner: node_exporter group: node_exporterOutput
| Поле | Тип | Описание |
|---|---|---|
path | string | |
mode | string | |
created | bool |
core.file.rendered — Файл = результат рендера text/template-шаблона (роль core.template).
Рендер в память → SHA-256 сверяется с существующим файлом → запись только при diff по любому из content/mode/owner. Запись атомарна (temp + rename в той же директории).
Отрендеренный контент, mode и owner совпали с файлом на диске.
Параметры
| Параметр | Тип | Обяз. / дефолт | Описание |
|---|---|---|---|
path | string | required | Целевой путь рендеренного файла. |
template | string | required | Путь к .tmpl-шаблону (резолвится локально, потом service-level). |
vars | map<string> | optional | Переменные рендера (видны шаблону как .vars.*). |
mode | string | optional | Права в восьмеричной форме, напр. "0644". |
owner | string | optional | Владелец (имя пользователя). |
group | string | optional | Группа-владелец (имя группы). |
Пример — rendered из шаблона; register — якорь onchanges: для рестарта сервиса
- name: Render redis.conf module: core.file.rendered register: redis_conf params: path: /etc/redis/redis.conf template: templates/redis.conf.tmpl mode: "0640" owner: redis group: redis vars: password: "${ input.password }"Output
| Поле | Тип | Описание |
|---|---|---|
path | string | |
sha256 | string | |
mode | string | |
installed | true |
Справочник
Корень контекста шаблона (rendered)
Что видит text/template при рендере core.file.rendered. Отсутствие обращённой переменной — ошибка рендера (strict-mode missingkey=error), а не молчаливая пустая подстановка.
| Ключ | Что это |
|---|---|
| .vars.* | производные переменные из vars: (вычислены CEL-фазой) |
| .input.* | резолвнутый operator-input прохода (Вариант B — напрямую, без passthrough через vars:; Keeper кладёт ключ, только если шаблон реально обращается к .input.*) |
| .self.* | проекция soulprint, snake_case: .self.os.pkg_mgr, .self.network.primary_ip |
| .role | declared-роль хоста |
| .essence.* | effective essence |
Заметки
- present задаёт содержимое ровно одним из content (inline) или src (копия regular-файла с хоста); оба вместе — ошибка (конфликт по присутствию ключа, даже content: "" вместе с src:). Ни того ни другого — пустой файл (legacy-поведение).
- src задаёт только содержимое (типично результат core.archive.extracted); mode/owner/group берутся из явных params, атрибуты src не наследуются. Путь src обязан быть абсолютным, симлинк reject-ится через Lstat (источник не следуется по ссылке).
- rendered: на wire Keeper обязан доставить и template_content, и render_context — без любого из них шаг падает (прод-инвариант golden-path, не optional).
- Ключи .self.* — snake_case (.self.os.pkg_mgr, .self.os.init_system), как soulprint.self.*; camelCase не сработает. register на rendered-задаче типично служит якорем onchanges: для рестарта сервиса при изменении конфига.
- present/absent/rendered/directory не запускают подпроцессов — рендер, запись, mkdir/chmod/chown идут in-process, без shell. recurse (рекурсивные права на содержимое каталога) в MVP не поддержан.
Безопасность и умолчания
- Прямая запись в произвольный путь ФС, включая системные (/etc/...), — главный риск: path/content/mode/owner/group должны приходить от автора Destiny, а не из недоверенного ввода (иначе подмена /etc/passwd, /etc/sudoers, unit-файла).
- Атомарность различается: rendered и src-ветка present пишут через temp + rename (наблюдатель не видит частично записанный конфиг), а content-ветка present — через os.WriteFile напрямую, без temp+rename: при сбое в момент записи файл может остаться усечённым. Для конфигов под работающим демоном предпочитайте rendered/src или рестарт потребителя через onchanges:.
- src — копия regular-файла с самого хоста (симлинк/каталог/device/относительный путь reject-ятся), но источник не становится доверенным: src, как и path, задаёт автор Destiny, иначе содержимое произвольного файла хоста окажется скопировано в path.
- mode/owner для секретов задавайте явно (0600/0640): без mode берётся дефолт os.WriteFile (зависит от umask), существующий mode не сверяется и не правится — секрет может оказаться world-readable.
- rendered рендерит в песочнице (sprig-allowlist, нет доступа к FS/сети/окружению — три sandbox-барьера), но секреты (${ vault(...) }, пароли через vars:) попадают в render_context и в итоговый файл — отсюда обязательность явного mode для конфигов с секретами.
- Привилегии: манифест объявляет fs_write_root (запись за пределы /var/lib/soul-stack/), но не run_as_root — модуль исполняется с привилегиями процесса soul-агента без повышения прав внутри; запись в системные пути на практике требует root.