Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions modules/bootc-hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# bootc-hooks Module

The `bootc-hooks` module provides a flexible way to run custom scripts at different stages of the system lifecycle, especially in relation to `bootc` image updates and switches.

## How It Works

This module sets up two separate `systemd` services that run sequentially to execute scripts in different contexts:

- `system-bootc-hooks.service`: Runs system-wide hooks as the `root` user.
- `user-bootc-hooks.service`: Runs user-specific hooks as the logged-in user. This service is configured to always run **after** the system service has completed.

The core logic resides in two scripts:
- `/usr/libexec/bootc-hooks/run-system-bootc-hooks.sh`
- `/usr/libexec/bootc-hooks/run-user-bootc-hooks.sh`

Here's the execution flow for each service:

**1. System Hooks Execution (`run-system-bootc-hooks.sh`)**
- First, it rotates the versioning files. The existing `/var/lib/bootc-hooks/version.yaml` is renamed to `/var/lib/bootc-hooks/version.previous.yaml`.
- It then calls `bootc status` to get the current boot's image name and digest.
- This new information is written to a fresh `/var/lib/bootc-hooks/version.yaml`.
- Finally, it compares `version.yaml` (current) with `version.previous.yaml` (past) to determine if `update` or `switch` hooks need to be triggered.

**2. User Hooks Execution (`run-user-bootc-hooks.sh`)**
- This script does **not** call `bootc status` because that command requires privileged permissions. Instead, it relies on the files generated by the system service to ensure consistency and work without elevated privileges.
- It reads the previous boot information from `/var/lib/bootc-hooks/version.previous.yaml`.
- It reads the current boot information from `/var/lib/bootc-hooks/version.yaml`.
- It compares the two versions to determine if its own `update` or `switch` hooks need to be triggered.

This approach ensures that user hooks are not only executed after system hooks but also base their logic on the exact same version information.

## Supported Hooks

The module organizes hooks by **scope** (`system` or `user`) and then by **event**. The three supported events are:

- **`boot`**: Scripts for this event are executed on every single boot (for system) or login (for user).
- **`update`**: These scripts run only when the digest of the `bootc` image has changed, indicating an image update has been applied.
- **`switch`**: These scripts are triggered when the image name itself has changed, which happens when rebasing to a different image entirely.

Scripts are executed from the following directories within `/usr/libexec/bootc-hooks/`:
- `system/boot/`
- `system/update/`
- `system/switch/`
- `user/boot/`
- `user/update/`
- `user/switch/`

## Usage

To use this module, you need to enable it in your `recipe.yml` and configure which scripts to run for each hook. The module supports two contexts (`system` and `user`) and three events (`boot`, `update`, `switch`).

All your hook scripts should be placed in the `files/scripts/` directory. In your `recipe.yml`, you will specify which script file to run for each hook event.

### Example

Let's say you want to run a system script on every boot and a user-specific script when a user logs in.

1. Add your scripts to the `files/scripts/` directory. For example:
* `files/scripts/system_boot.sh`
* `files/scripts/user_boot.sh`

2. Configure the `bootc-hooks` module in your `recipe.yml` to execute these scripts:

```yaml
modules:
- type: bootc-hooks
system:
boot:
# This script will run as root on every boot
- system_boot.sh
user:
boot:
# This script will run as the user on every login
- user_boot.sh
```

During the image build process, the module will copy these scripts to the appropriate execution directories in `/usr/libexec/bootc-hooks/` and set up the `systemd` services to trigger them at the correct time.

## Debugging

The hooks are executed by the `system-bootc-hooks.service` and `user-bootc-hooks.service` `systemd` services. You can check their status and logs to debug your scripts.

### System Hooks

- **Check service status**:
```bash
systemctl status system-bootc-hooks.service
```
- **View logs**:
```bash
journalctl -u system-bootc-hooks.service
```

### User Hooks

- **Check service status**:
```bash
systemctl --user status user-bootc-hooks.service
```
- **View logs**:
```bash
journalctl --user -u user-bootc-hooks.service
```
84 changes: 84 additions & 0 deletions modules/bootc-hooks/bootc-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/bin/bash
echo "Running bootc-hooks module"

set -euo pipefail

# Define paths
SYSTEM_SCRIPT_NAME="run-system-bootc-hooks.sh"
USER_SCRIPT_NAME="run-user-bootc-hooks.sh"
MODULE_DIR="$MODULE_DIRECTORY/bootc-hooks"
LIBEXEC_DIR="/usr/libexec/bootc-hooks"
SYSTEM_SERVICE_NAME="system-bootc-hooks.service"
USER_SERVICE_NAME="user-bootc-hooks.service"
SYSTEM_SERVICE_FILE="/usr/lib/systemd/system/$SYSTEM_SERVICE_NAME"
USER_SERVICE_FILE="/usr/lib/systemd/user/$USER_SERVICE_NAME"

# Copy the scripts to /usr/libexec
echo "Copying script runners"
install -Dm 0755 "$MODULE_DIR/$SYSTEM_SCRIPT_NAME" "$LIBEXEC_DIR/$SYSTEM_SCRIPT_NAME"
install -Dm 0755 "$MODULE_DIR/$USER_SCRIPT_NAME" "$LIBEXEC_DIR/$USER_SCRIPT_NAME"

# Create the system systemd service file
echo "Creating services"
cat <<EOF >"$SYSTEM_SERVICE_FILE"
[Unit]
Description=Run bootc hooks after boot
Requires=network-online.target
After=network-online.target multi-user.target

[Service]
Type=oneshot
ExecStart=$LIBEXEC_DIR/$SYSTEM_SCRIPT_NAME

[Install]
WantedBy=multi-user.target
EOF

# Create the user systemd service file
cat <<EOF >"$USER_SERVICE_FILE"
[Unit]
Description=Run user bootc hooks after login
After=default.target system-bootc-hooks.service

[Service]
Type=oneshot
ExecStart=$LIBEXEC_DIR/$USER_SCRIPT_NAME

[Install]
WantedBy=default.target
EOF

# Enable the service
systemctl -f enable $SYSTEM_SERVICE_NAME
systemctl -f --global enable $USER_SERVICE_NAME

echo "Copying hooks"
SCRIPT_DIR="${CONFIG_DIRECTORY}/scripts"
mkdir -p /usr/libexec/bootc-hooks/{system,user}/{boot,switch,update}

for scope in system user; do
for event in boot update switch; do
query=".${scope}.${event}[]?"
declare -a scripts_array=()
get_json_array scripts_array "${query}" "$1"
if [ ${#scripts_array[@]} -gt 0 ]; then
dest_dir="${LIBEXEC_DIR}/${scope}/${event}"
echo "Copying ${#scripts_array[@]} scripts for ${scope}/${event} hook..."
for script in "${scripts_array[@]}"; do
script_path="${SCRIPT_DIR}/${script}"
if [ -f "$script_path" ]; then
echo " - ${script}"
install -m 0755 "$script_path" "$dest_dir/"
else
echo "Warning: Script '${script}' for ${scope}/${event} hook not found in ${SCRIPT_DIR}" >&2
fi
done
fi
done
done

chmod -R +x /usr/libexec/bootc-hooks

# Install dependencies
echo "Install yq dependency"
dnf -y install yq
27 changes: 27 additions & 0 deletions modules/bootc-hooks/bootc-hooks.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import "@typespec/json-schema";
using TypeSpec.JsonSchema;

model HookEvents {
/** Actions to run on boot. */
boot?: string[];
/** Actions to run on OS update. */
update?: string[];
/** Actions to run on OS switch (rollback). */
switch?: string[];
}

@jsonSchema("/modules/bootc-hooks-latest.json")
model BootcHooksModuleLatest {
...BootcHooksModuleV1
}
@jsonSchema("/modules/bootc-hooks-v1.json")
model BootcHooksModuleV1 {
/**
* The bootc-hooks module adds a service that runs scripts based on bootc events.
*/
type: "bootc-hooks" | "bootc-hooks@v1" | "bootc-hooks@latest";
/** Hooks to run in the system context. */
system?: HookEvents;
/** Hooks to run in the user context. */
user?: HookEvents;
}
18 changes: 18 additions & 0 deletions modules/bootc-hooks/module.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: bootc-hooks
shortdesc: The bootc-hooks module allows to run scripts on boot, when image updated or switched
example: |
type: bootc-hooks
system:
boot:
- script-to-run-on-boot.sh
update:
- script-to-run-on-update.sh
switch:
- script-to-run-on-switch.sh
user:
boot:
- user-script-on-boot.sh
update:
- user-script-on-update.sh
switch:
- user-script-on-switch.sh
78 changes: 78 additions & 0 deletions modules/bootc-hooks/run-system-bootc-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash

set -euo pipefail

VERSION_DIR="/var/lib/bootc-hooks"
VERSION_FILE="$VERSION_DIR/version.yaml"
PREVIOUS_VERSION_FILE="$VERSION_DIR/version.previous.yaml"
SWITCH_HOOKS_DIR="/usr/libexec/bootc-hooks/system/switch"
UPDATE_HOOKS_DIR="/usr/libexec/bootc-hooks/system/update"
BOOT_HOOKS_DIR="/usr/libexec/bootc-hooks/system/boot"

# Create the directory if it doesn't exist
sudo mkdir -p "$VERSION_DIR"

# Rotate version files
if [ -f "$VERSION_FILE" ]; then
echo "Moving old version file to $PREVIOUS_VERSION_FILE"
sudo mv "$VERSION_FILE" "$PREVIOUS_VERSION_FILE"
fi

old_image=""
old_digest=""
if [ -f "$PREVIOUS_VERSION_FILE" ]; then
echo "Reading previous image and digest from $PREVIOUS_VERSION_FILE"
old_image=$(yq e '.image' "$PREVIOUS_VERSION_FILE")
old_digest=$(yq e '.digest' "$PREVIOUS_VERSION_FILE")
echo "Previous Image: ${old_image}"
echo "Previous Digest: ${old_digest}"
fi

output=$(bootc status --format yaml --booted)

new_image=$(echo "$output" | yq e '.status.booted.image.image.image')
new_digest=$(echo "$output" | yq e '.status.booted.image.imageDigest')

# Create the YAML content
yaml_content=$(cat <<EOF
image: ${new_image}
digest: ${new_digest}
EOF
)

# Write the YAML content to the file
echo "$yaml_content" | sudo tee "$VERSION_FILE" > /dev/null

if [ "${new_image}" != "${old_image}" ]; then
echo "Image has changed. Running switch hooks."
if [ -d "$SWITCH_HOOKS_DIR" ]; then
for hook in "$SWITCH_HOOKS_DIR"/*; do
if [ -x "$hook" ]; then
echo "Running hook: $hook"
"$hook"
fi
done
fi
fi

if [ "${new_digest}" != "${old_digest}" ]; then
echo "Digest has changed. Running update hooks."
if [ -d "$UPDATE_HOOKS_DIR" ]; then
for hook in "$UPDATE_HOOKS_DIR"/*; do
if [ -x "$hook" ]; then
echo "Running hook: $hook"
"$hook"
fi
done
fi
fi

echo "Running boot hooks."
if [ -d "$BOOT_HOOKS_DIR" ]; then
for hook in "$BOOT_HOOKS_DIR"/*; do
if [ -x "$hook" ]; then
echo "Running hook: $hook"
"$hook"
fi
done
fi
72 changes: 72 additions & 0 deletions modules/bootc-hooks/run-user-bootc-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env bash

set -euo pipefail

# Define paths
VERSION_FILE="/var/lib/bootc-hooks/version.yaml"
PREVIOUS_VERSION_FILE="/var/lib/bootc-hooks/version.previous.yaml"
SWITCH_HOOKS_DIR="/usr/libexec/bootc-hooks/user/switch"
UPDATE_HOOKS_DIR="/usr/libexec/bootc-hooks/user/update"
BOOT_HOOKS_DIR="/usr/libexec/bootc-hooks/user/boot"

old_image=""
old_digest=""
if [ -f "$PREVIOUS_VERSION_FILE" ]; then
echo "Reading previous image and digest from $PREVIOUS_VERSION_FILE"
old_image=$(yq e '.image' "$PREVIOUS_VERSION_FILE")
old_digest=$(yq e '.digest' "$PREVIOUS_VERSION_FILE")
echo "Previous User Image: ${old_image}"
echo "Previous User Digest: ${old_digest}"
fi

new_image=""
new_digest=""
if [ -f "$VERSION_FILE" ]; then
echo "Reading new image and digest from $VERSION_FILE"
new_image=$(yq e '.image' "$VERSION_FILE")
new_digest=$(yq e '.digest' "$VERSION_FILE")
echo "New User Image: ${new_image}"
echo "New User Digest: ${new_digest}"
fi

# --- Run Hooks ---

# Run switch hooks if the image has changed
if [ "${new_image}" != "${old_image}" ]; then
echo "Image has changed for user. Running switch hooks."
if [ -d "$SWITCH_HOOKS_DIR" ]; then
for hook in "$SWITCH_HOOKS_DIR"/*; do
if [ -x "$hook" ]; then
echo "Running user switch hook: $hook"
"$hook"
fi
done
fi
fi

# Run update hooks if the digest has changed
if [ "${new_digest}" != "${old_digest}" ]; then
echo "Digest has changed for user. Running update hooks."
if [ -d "$UPDATE_HOOKS_DIR" ]; then
for hook in "$UPDATE_HOOKS_DIR"/*; do
if [ -x "$hook" ]; then
echo "Running user update hook: $hook"
"$hook"
fi
done
fi
fi

# Always run boot hooks
echo "Running user boot hooks."
if [ -d "$BOOT_HOOKS_DIR" ]; then
for hook in "$BOOT_HOOKS_DIR"/*; do
if [ -x "$hook" ];
then
echo "Running user boot hook: $hook"
"$hook"
fi
done
fi

echo "User bootc hooks finished."