Files and Directories

  • file.copy
  • file.download
  • file.link
  • file.remove
  • file.unarchive
  • directory.copy
  • directory.create
  • directory.remove

All directory.* actions can also be written as dir.*, e.g. dir.create.

Every file.* and directory.* action takes privileged: true (or sudo: true) to make its change as root, e.g. for a file below /etc. See Privilege Escalation.

Note

The following commands expect the from/source to point to files/directories which are themselves under a "files" directory. This is a restriction so that comtrya knows not to parse any .yaml file e.g. a config for a different tool as a manifest. To see how it works check the examples

  • file.link
  • file.copy
  • directory.copy

file.copy

Action used to copy a file from one location to another.

KeyTypeOptionalDescription
actionstringnofile.copy
fromstringnosource file
tostringnodestination file
templatebooleanyesrenders files using context providers
default: false
chmodintegeryesoctal permissions, e.g. 644 or "0644"
default: 644
owned_by_userstringyesuser for chown
owned_by_groupstringyesgroup for chown
passphrasestringyesdecrypts an age file encrypted with a passphrase
process_withlistyescommand that gets the file on stdin; its output is written instead, see below

Examples

# Simple file copy
- action: file.copy
  from: procs-config.toml
  to: "{{ user.config_dir }}/procs/config.toml"

# With template and permissions
- action: file.copy
  from: managed_file
  to: /root/file
  template: true
  chmod: 644

# With encrypted file
- action: file.copy
  from: encrypted-file
  to: /tmp/some-decrypted-file
  passphrase: "1KZ2EXDHSQKZFQP43JK2LPXUFZ8D365CM5WQXRSH97U7N9WKRVFKS0TCS30"

# file copy with chown on unix systems
- action: file.copy
  from: procs-config.toml
  to: "{{ user.config_dir }}/procs/config.toml"
  owned_by_user: test
  owned_by_group: test

Note: utilizing chown functionality will require running comtrya as root. Also, both a user and a group need to be specified.

Seeing what changes

comtrya apply --dry-run shows what file.copy would change, without changing anything:

 INFO execute:{manifest="git"}:{action=file.copy}: Contents of /home/me/.gitconfig change:
--- current
+++ new
@@ -1,3 +1,3 @@
 [user]
-  name = Old Name
+  name = New Name
   email = me@example.com
  • For a file that doesn't exist yet or is empty, comtrya only says that it is created or written, without its contents.
  • Permission changes from chmod get their own line.
  • Binary files aren't diffed, comtrya only says that they change.
  • Files written with process_with or passphrase are never shown, as they usually hold decrypted secrets. Rendered templates are shown, so a template that includes secrets from variables shows them too.

During a normal apply, the same diff is logged with -v.

Processing files with a command

process_with runs a command, sends the source file to its stdin and writes its stdout to to. Use it to decrypt secrets with the tool you already use: the encrypted file is the only copy in your repository, and no encrypted copy is left behind next to the decrypted one.

  • The first entry is the program, the rest are its arguments. No shell is involved, so ~ and $VAR are not expanded: use contexts like {{ user.home_dir }}, or [sh, -c, "..."].
  • The command runs while comtrya plans the action, also with --dry-run, so it must not change the system. It runs as your user, without privileged.
  • If the output equals the current file, nothing is written.
  • Its output is never shown in a diff, see Seeing what changes.
  • With template: true, the output is rendered as a template afterwards.
  • If the command fails, the action fails and its stderr is shown.
  • passphrase and process_with can't be combined.
# GnuPG: uses your keyring and gpg-agent, so smartcards and YubiKeys work too
- action: file.copy
  from: credentials.fish.asc
  to: "{{ user.config_dir }}/fish/conf.d/credentials.fish"
  chmod: 600
  process_with: [gpg, --quiet, --decrypt]

# age with a key file (age1... or post-quantum age1pq1... keys)
- action: file.copy
  from: credentials.toml.age
  to: "{{ user.home_dir }}/.cargo/credentials.toml"
  chmod: 600
  process_with: [age, --decrypt, --identity, "{{ user.home_dir }}/.config/age/key.txt"]

# 1Password: the file contains secret references like {{ op://Private/GitHub/token }}.
# With more than one account signed in, pick one with --account
- action: file.copy
  from: gh-hosts.yml.tpl
  to: "{{ user.config_dir }}/gh/hosts.yml"
  chmod: 600
  process_with: [op, inject, --account, my.1password.com]

# sops
- action: file.copy
  from: secrets.yaml
  to: "{{ user.config_dir }}/app/secrets.yaml"
  chmod: 600
  process_with: [sops, --decrypt, --input-type, yaml, --output-type, yaml, /dev/stdin]

When gpg or age need a passphrase, they ask for it themselves: gpg through its pinentry, age on the terminal.

file.chown

This action will change the user and group owner of a file.

Note: In order to utilize this, must run comtrya as root. Also both a user and group need to be specified.

KeyTypeOptionalDescription
actionstringnofile.chown
pathstringnofile to change ownership on
userstringnouser to specify as file owner
groupstringnogroup to specify as file owner

Examples

actions:
  - action: file.chown
    path: ./files/some-file
    user: test
    group: test

file.download

This action will download a file.

KeyTypeOptionalDescription
actionstringnofile.download
fromsourcestringno
totargetstringno
chmodintegeryesoctal permissions, e.g. 644 or "0644", default: 644
owned_by_userstringyesuser for chown
owned_by_groupstringyesgroup for chown
sha256stringyesexpected SHA-256 checksum of the file, as hex

Without sha256, the file is downloaded once and kept as long as it exists. With sha256:

  • the download fails if its checksum doesn't match, and an existing file is left untouched
  • an existing file with a different checksum is downloaded again, so updating from and sha256 together updates the file

GitHub shows the checksum of every release file on the release page, and many projects publish a SHA256SUMS file. Otherwise, download the file once and run shasum -a 256 <file> (or Get-FileHash <file> in PowerShell).

An alias also exists such that source can be used in lieu of from and target can be used in lieu of to.

Example

actions:
  # This will be rendered with contexts
  - action: file.download
    from: https://google.com/robots.txt
    to: /tmp/google-robots.txt

  # This whill also run a chown step on downloaded file
  - action: file.download
    from: https://google.com/robots.txt
    to: /tmp/google-robots.txt
    owned_by_user: nobody
    owned_by_group: nobody

  # Verified download, replaced when the checksum changes
  - action: file.download
    from: https://github.com/jtroo/kanata/releases/download/v1.12.0/linux-binaries-x64.zip
    to: "{{ user.cache_dir }}/kanata/linux-binaries-x64.zip"
    sha256: 0bedd91567c5d7c54679061baadc37e4f83fb71750003999bc1d11f2c9754f36

Note: utilizing chown functionality will require running comtrya as root. Also, both a user and a group need to be specified.

Create a symlink for files. This action can be used to symlink a single file or files in a directory.

KeyTypeOptionalDescription
actionstringnofile.link
fromstringnosymlink location
tostringnosymlink points to
walk dirbooleanyesWalk diles in directory
default: false
sourcestringyesUsed in conjunction with walk dir
in liue of from

Example

# Symlink for a single file
- action: file.link
  from: /root/symlink
  to: managed_file

# Symlink for all files in a directory
- action: file.link
  source: walker
  target: /tml/walker-123
  walk_dir: true

file.remove

Removes a file.

KeyTypeOptionalDescription
actionstringnofile.remove
targetstringnofile to be removed

Example

- action: file.remove
  target: /tmp/some-file-rendered

file.unarchive

This action provides the ability to unarchive a tar.gz file.

KeyTypeOptionalDescription
actionstringnofile.unarchive
fromstringnofull path to archive
tostringnodestination of unarchived contents
forceboolyesforce the unarchiving (defaults true)

Example

actions:
  - action: file.download
    from: https://github.com/comtrya/comtrya/archive/refs/tags/v0.9.0.tar.gz
    to: /tmp/comtrya

  - action: file.unarchive
    from: /tmp/comtrya
    to: /tmp/

directory.copy

Copies the contents of a directory into another directory, the same on all platforms. After the action, to holds everything from holds: to and its subdirectories are created if missing, files with the same path are overwritten and other files in to are left alone. A trailing / on to makes no difference. On unix, symlinks inside from are copied as symlinks; on Windows the file or directory they point to is copied, and a link or junction back to a directory above it fails the action instead of copying forever. The action fails if to is from itself or a directory inside it, like cp does.

Before 0.11.0, unix copied the directory itself to <to>/<name> unless to ended with /. To keep that, put the name into to.

KeyTypeOptionalDescription
actionstringnodirectory.copy
fromstringnosource directory
tostringnodirectory the contents of from are copied into

Example

- action: directory.copy
  from: managed_directory
  to: /root/location

directory.create

Creates a directory, including all missing parent directories. Does nothing if the directory already exists.

KeyTypeOptionalDescription
actionstringnodirectory.create
pathstringnodirectory to create

Example

- action: directory.create
  path: "{{ user.home_dir }}/.config/nvim"

directory.remove

Removes a directory. Only empty directories are removed: if the directory isn't empty or doesn't exist, comtrya logs an error and skips the action.

KeyTypeOptionalDescription
actionstringnodirectory.remove
targetstringnodirectory to remove

Example

- action: directory.remove
  target: /tmp/old-directory