Add enroll diff
parent
973e6756e3
commit
ebe49553e9
1 changed files with 260 additions and 0 deletions
260
enroll-diff.md
Normal file
260
enroll-diff.md
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
The Enroll `diff` subcommand will compare two 'harvests' and see if there are any differences.
|
||||||
|
|
||||||
|
# What does diff do?
|
||||||
|
|
||||||
|
These differences include:
|
||||||
|
|
||||||
|
* Packages added/removed
|
||||||
|
* Files added/removed/changed
|
||||||
|
* Users added/removed/changed
|
||||||
|
* Services added/removed/changed
|
||||||
|
|
||||||
|
The `diff` command can also understand SOPS-encrypted harvests, assume the user executing the command is able to decrypt the SOPS files.
|
||||||
|
|
||||||
|
# How do I run it?
|
||||||
|
|
||||||
|
Assuming you have two harvests on your filesystem:
|
||||||
|
|
||||||
|
```
|
||||||
|
enroll diff \
|
||||||
|
--old harvest.v1 \
|
||||||
|
--new harvest.v2
|
||||||
|
```
|
||||||
|
|
||||||
|
# Output formats
|
||||||
|
|
||||||
|
The default output is just a plain text format, which is the same as passing `--format text`.
|
||||||
|
|
||||||
|
You can pass `--format json` or `--format markdown` to get structured formats instead.
|
||||||
|
|
||||||
|
# --exit-code mode
|
||||||
|
|
||||||
|
You can pass `--exit-code` as an argument, which will return a return code of 2 if there was any 'diff'.
|
||||||
|
|
||||||
|
You can use this to script any further action your cron service might take, such as sending e-mail output, or rely on it to trigger failures in CI builds.
|
||||||
|
|
||||||
|
# Webhooks
|
||||||
|
|
||||||
|
You can also send the output to a webhook under your control. Here is an example:
|
||||||
|
|
||||||
|
```
|
||||||
|
enroll diff \
|
||||||
|
--old harvest.v1 \
|
||||||
|
--new harvest.v2 \
|
||||||
|
--webhook https://example.com/some/endpoint \
|
||||||
|
--webhook-format json \
|
||||||
|
--webhook-header 'X-Enroll-Secret: Some-Secret-Here'
|
||||||
|
```
|
||||||
|
|
||||||
|
You can pass `--webhook-format markdown` if you wanted the Markdown format in the payload, but most people probably want JSON structured data for consumption at the other end.
|
||||||
|
|
||||||
|
Use the `--webhook-header` to send some sort of authentication/authorization attribute (e.g an OAuth2.0 bearer token, static secret, etc) to help lock down your webhook from unauthorised requests. You can send multiple headers.
|
||||||
|
|
||||||
|
# Email
|
||||||
|
|
||||||
|
You can send the output via email using email and smtp arguments. See 'Full usage info' below for more information.
|
||||||
|
|
||||||
|
# Example: Running the diff on a systemd timer
|
||||||
|
|
||||||
|
In the example below, we set up a systemd timer to run on a periodic basis and trigger the webhook.
|
||||||
|
|
||||||
|
It will set up a 'golden' harvest if it didn't already exist, then periodically run a second harvest to compare against it.
|
||||||
|
|
||||||
|
You can optionally promote the new harvest to become the new 'golden' harvest for future invocations if you want a continuous, rolling 'diff' scenario.
|
||||||
|
|
||||||
|
## Create script
|
||||||
|
|
||||||
|
Store in `/usr/local/bin/enroll-harvest-diff.sh` and make it `chmod 0700`
|
||||||
|
|
||||||
|
```
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Required env
|
||||||
|
: "${WEBHOOK_URL:?Set WEBHOOK_URL in /etc/enroll/enroll-harvest-diff.env}"
|
||||||
|
: "${ENROLL_SECRET:?Set ENROLL_SECRET in /etc/enroll/enroll-harvest-diff.env}"
|
||||||
|
|
||||||
|
# Optional env
|
||||||
|
STATE_DIR="${ENROLL_STATE_DIR:-/var/lib/enroll}"
|
||||||
|
GOLDEN_DIR="${STATE_DIR}/golden"
|
||||||
|
PROMOTE_NEW="${PROMOTE_NEW:-1}" # 1=promote new->golden; 0=keep golden fixed
|
||||||
|
KEEP_BACKUPS="${KEEP_BACKUPS:-7}" # only used if PROMOTE_NEW=1
|
||||||
|
LOCKFILE="${STATE_DIR}/.enroll-harvest-diff.lock"
|
||||||
|
|
||||||
|
mkdir -p "${STATE_DIR}"
|
||||||
|
chmod 700 "${STATE_DIR}" || true
|
||||||
|
|
||||||
|
# single-instance lock (avoid overlapping timer runs)
|
||||||
|
exec 9>"${LOCKFILE}"
|
||||||
|
flock -n 9 || exit 0
|
||||||
|
|
||||||
|
tmp_new=""
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${tmp_new}" && -d "${tmp_new}" ]]; then
|
||||||
|
rm -rf "${tmp_new}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
make_tmp_dir() {
|
||||||
|
mktemp -d "${STATE_DIR}/.harvest.XXXXXX"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_harvest() {
|
||||||
|
local out_dir="$1"
|
||||||
|
rm -rf "${out_dir}"
|
||||||
|
mkdir -p "${out_dir}"
|
||||||
|
chmod 700 "${out_dir}" || true
|
||||||
|
enroll harvest --out "${out_dir}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# A) create golden if missing
|
||||||
|
if [[ ! -f "${GOLDEN_DIR}/state.json" ]]; then
|
||||||
|
tmp="$(make_tmp_dir)"
|
||||||
|
run_harvest "${tmp}"
|
||||||
|
rm -rf "${GOLDEN_DIR}"
|
||||||
|
mv "${tmp}" "${GOLDEN_DIR}"
|
||||||
|
echo "Golden harvest created at ${GOLDEN_DIR}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# B) create new harvest
|
||||||
|
tmp_new="$(make_tmp_dir)"
|
||||||
|
run_harvest "${tmp_new}"
|
||||||
|
|
||||||
|
# C) diff + webhook notify
|
||||||
|
enroll diff \
|
||||||
|
--old "${GOLDEN_DIR}" \
|
||||||
|
--new "${tmp_new}" \
|
||||||
|
--webhook "${WEBHOOK_URL}" \
|
||||||
|
--webhook-format json \
|
||||||
|
--webhook-header "X-Enroll-Secret: ${ENROLL_SECRET}"
|
||||||
|
|
||||||
|
# Promote or discard new harvest
|
||||||
|
if [[ "${PROMOTE_NEW}" == "1" || "${PROMOTE_NEW,,}" == "true" || "${PROMOTE_NEW}" == "yes" ]]; then
|
||||||
|
ts="$(date -u +%Y%m%d-%H%M%S)"
|
||||||
|
backup="${STATE_DIR}/golden.prev.${ts}"
|
||||||
|
mv "${GOLDEN_DIR}" "${backup}"
|
||||||
|
mv "${tmp_new}" "${GOLDEN_DIR}"
|
||||||
|
tmp_new="" # don't delete it in trap
|
||||||
|
|
||||||
|
# Keep only latest N backups
|
||||||
|
if [[ "${KEEP_BACKUPS}" =~ ^[0-9]+$ ]] && (( KEEP_BACKUPS > 0 )); then
|
||||||
|
ls -1dt "${STATE_DIR}"/golden.prev.* 2>/dev/null | tail -n +"$((KEEP_BACKUPS+1))" | xargs -r rm -rf
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Diff complete; baseline updated."
|
||||||
|
else
|
||||||
|
# tmp_new will be deleted by trap
|
||||||
|
echo "Diff complete; baseline unchanged (PROMOTE_NEW=${PROMOTE_NEW})."
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment file
|
||||||
|
|
||||||
|
Save as: `/etc/enroll/enroll-harvest-diff` and `chmod 0600` it.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Where to store golden + temp harvests
|
||||||
|
ENROLL_STATE_DIR=/var/lib/enroll
|
||||||
|
|
||||||
|
# 1 = each run becomes new baseline ("since last harvest")
|
||||||
|
# 0 = compare against a fixed baseline ("since golden")
|
||||||
|
PROMOTE_NEW=1
|
||||||
|
|
||||||
|
# If PROMOTE_NEW=1, keep this many old baselines
|
||||||
|
KEEP_BACKUPS=7
|
||||||
|
|
||||||
|
WEBHOOK_URL=https://example.com/webhook/xxxxxxxx
|
||||||
|
ENROLL_SECRET=xxxxxxxxxxxxxxxxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Systemd service
|
||||||
|
|
||||||
|
`/etc/systemd/system/enroll-harvest-diff.service`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=Enroll harvest + diff + webhook notify
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
ConditionPathExists=/etc/enroll/enroll-harvest-diff
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
EnvironmentFile=/etc/enroll/enroll-harvest-diff
|
||||||
|
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
UMask=0077
|
||||||
|
|
||||||
|
# Create /var/lib/enroll automatically
|
||||||
|
StateDirectory=enroll
|
||||||
|
|
||||||
|
ExecStart=/usr/local/bin/enroll-harvest-diff.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Systemd timer
|
||||||
|
|
||||||
|
`/etc/systemd/system/enroll-harvest-diff.timer`
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=Run Enroll harvest diff hourly
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=hourly
|
||||||
|
RandomizedDelaySec=10m
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enable + test
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now enroll-harvest-diff.timer
|
||||||
|
|
||||||
|
# run once now
|
||||||
|
sudo systemctl start enroll-harvest-diff.service
|
||||||
|
sudo journalctl -u enroll-harvest-diff.service -n 200 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want the “fixed baseline” behavior (always diff against the original golden snapshot), set `PROMOTE_NEW=0` and you're done.
|
||||||
|
|
||||||
|
|
||||||
|
# Full usage info
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: enroll diff [-h] --old OLD --new NEW [--sops] [--format {text,markdown,json}] [--out OUT] [--exit-code] [--notify-always] [--webhook WEBHOOK]
|
||||||
|
[--webhook-format {json,text,markdown}] [--webhook-header K:V] [--email-to EMAIL_TO] [--email-from EMAIL_FROM] [--email-subject EMAIL_SUBJECT] [--smtp SMTP]
|
||||||
|
[--smtp-user SMTP_USER] [--smtp-password-env SMTP_PASSWORD_ENV]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--old OLD Old/baseline harvest (directory, a path to state.json, a tarball, or a SOPS-encrypted bundle).
|
||||||
|
--new NEW New/current harvest (directory, a path to state.json, a tarball, or a SOPS-encrypted bundle).
|
||||||
|
--sops Allow SOPS-encrypted harvest bundle inputs (requires `sops` on PATH).
|
||||||
|
--format {text,markdown,json}
|
||||||
|
Report output format (default: text).
|
||||||
|
--out OUT Write the report to this file instead of stdout.
|
||||||
|
--exit-code Exit with status 2 if differences are detected.
|
||||||
|
--notify-always Send webhook/email even when there are no differences.
|
||||||
|
--webhook WEBHOOK POST the report to this URL (only when differences are detected, unless --notify-always).
|
||||||
|
--webhook-format {json,text,markdown}
|
||||||
|
Payload format for --webhook (default: json).
|
||||||
|
--webhook-header K:V Extra HTTP header for --webhook (repeatable), e.g. 'Authorization: Bearer ...'.
|
||||||
|
--email-to EMAIL_TO Email the report to this address (repeatable; only when differences are detected unless --notify-always).
|
||||||
|
--email-from EMAIL_FROM
|
||||||
|
From address for --email-to (default: enroll@<hostname>).
|
||||||
|
--email-subject EMAIL_SUBJECT
|
||||||
|
Subject for --email-to (default: 'enroll diff report').
|
||||||
|
--smtp SMTP SMTP server host[:port] for --email-to. If omitted, uses local sendmail.
|
||||||
|
--smtp-user SMTP_USER
|
||||||
|
SMTP username (optional).
|
||||||
|
--smtp-password-env SMTP_PASSWORD_ENV
|
||||||
|
Environment variable containing SMTP password (optional).
|
||||||
|
```
|
||||||
|
|
||||||
|
The main purpose of the `diff` command
|
||||||
Loading…
Add table
Add a link
Reference in a new issue