provisioning-catalog/components/fleet_agent/taskserv/install-fleet_agent.sh

463 lines
21 KiB
Bash
Raw Permalink Normal View History

#!/bin/bash
# install-fleet_agent.sh — taskserv installer for libre-forge fleet-agent.
# Runs on the target Debian/Ubuntu host as root. Idempotent.
#
# Order on the server: fleet_base → (acme_certd | lian_node) → fleet_agent.
# Hard precondition: fleet_base must have installed /etc/fleet/age.key.
#
# Inputs (env file, rendered from workspace NCL by deploy.just):
# FLEET_AGENT_NODE_ID — logical node id ("UNSET" rejected)
# FLEET_AGENT_TARGET_SERVER — physical hostname (informational)
# FLEET_AGENT_IMAGE_REF — OCI ref carrying the agent binary
# FLEET_AGENT_DAEMON_URL — WebSocket endpoint of fleet-daemon (adr-010)
# FLEET_AGENT_DECLARED_* — static cpu/ram_gb/disk_gb (heartbeat capacity)
# FLEET_AGENT_HEARTBEAT_S — heartbeat cadence (default 15s)
# FLEET_AGENT_SYSTEM_USER — system user name (default "fleet-agent")
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for f in env-fleet_agent _credentials.env; do
if [ -f "${SCRIPT_DIR}/${f}" ]; then
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/${f}"
fi
done
: "${FLEET_AGENT_NODE_ID:?FLEET_AGENT_NODE_ID must be set}"
: "${FLEET_AGENT_IMAGE_REF:?FLEET_AGENT_IMAGE_REF must be set}"
: "${FLEET_AGENT_DAEMON_URL:?FLEET_AGENT_DAEMON_URL must be set}"
FLEET_AGENT_DECLARED_CPU="${FLEET_AGENT_DECLARED_CPU:-4}"
FLEET_AGENT_DECLARED_RAM_GB="${FLEET_AGENT_DECLARED_RAM_GB:-8}"
FLEET_AGENT_DECLARED_DISK_GB="${FLEET_AGENT_DECLARED_DISK_GB:-80}"
FLEET_AGENT_HEARTBEAT_S="${FLEET_AGENT_HEARTBEAT_S:-15}"
FLEET_AGENT_SYSTEM_USER="${FLEET_AGENT_SYSTEM_USER:-fleet-agent}"
bold() { printf "\033[1m%s\033[0m\n" "$*"; }
fatal() { printf "\033[31mFATAL:\033[0m %s\n" "$*" >&2; exit 1; }
warn() { printf "\033[33mWARN :\033[0m %s\n" "$*" >&2; }
[ "$(id -u)" -eq 0 ] || fatal "must run as root"
[ "$FLEET_AGENT_NODE_ID" != "UNSET" ] || fatal "FLEET_AGENT_NODE_ID is sentinel 'UNSET' — caller must override"
# fleet_base precondition: the age key must already be on disk.
[ -r /etc/fleet/age.key ] || fatal "/etc/fleet/age.key missing — install fleet_base first"
ACTION="${1:-install}"
AGENT_BIN="/usr/local/bin/fleet-agent"
AGENT_ETC="/etc/fleet-agent"
AGENT_UNIT="/etc/systemd/system/fleet-agent.service"
# Image install marker. Written after a successful pull+install; consulted at
# the start of every re-install to skip the costly OCI pull when the requested
# ref already matches what's on disk. PRVNG_FORCE / PROVISIONING_FORCE bypasses
# the cache and always re-pulls.
AGENT_IMAGE_MARK="/etc/fleet/fleet-agent.image.ref"
# Resolve PRVNG_/PROVISIONING_ aliases (mirrors the fleet_daemon helper).
case "$(printf '%s' "${PROVISIONING_FORCE:-${PRVNG_FORCE:-}}" | tr '[:upper:]' '[:lower:]')" in
1|true|yes) FORCE=true ;; *) FORCE=false ;;
esac
# Locate the fleet-agent binary inside whatever `oras pull` writes.
# Container-image layout (lamina output): nested under usr/local/bin/.
# Plain-artifact layout (future): top-level fleet-agent file.
# Layered tarball case: scan the pulled dir tree.
_extract_agent_binary() {
local pull_dir="$1"
local candidates=(
"${pull_dir}/usr/local/bin/fleet-agent"
"${pull_dir}/fleet-agent"
)
for c in "${candidates[@]}"; do
if [ -f "$c" ]; then
echo "$c"
return 0
fi
done
# Fallback: scan for any executable named fleet-agent.
local found
found=$(find "${pull_dir}" -name 'fleet-agent' -type f 2>/dev/null | head -1 || true)
if [ -n "$found" ]; then
echo "$found"
return 0
fi
return 1
}
# Container-image fallback: when `oras pull` skips all layers (because they
# lack `org.opencontainers.image.title` annotations, i.e. they are docker/OCI
# image layers not flat-artifact files), fall back to `oras copy --to-oci-layout`
# and extract every gzipped tar blob into a flat tree. This recovers the binary
# regardless of which stage's layer contains it.
#
# IMPORTANT: this function is called via $(...) so its stdout becomes a file
# path. Every subordinate command (oras copy, tar, etc.) MUST redirect its
# stdout to stderr (>&2) so only `_extract_agent_binary`'s final echo reaches
# the caller.
_extract_via_oci_layout() {
local image_ref="$1"
local pull_dir="$2"
local layout_dir="${pull_dir}/layout"
local extract_dir="${pull_dir}/extract"
local _plat
_plat="linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
mkdir -p "${layout_dir}" "${extract_dir}"
DOCKER_CONFIG=/etc/fleet/docker \
oras copy "${image_ref}" --platform "${_plat}" --to-oci-layout "${layout_dir}:fleet-agent" >&2 \
|| return 1
local blob
# Process blobs smallest-first: the application binary lives in the topmost
# (smallest) layer; base image layers are large (Debian + Rust = 100-200 MB
# compressed). Stopping as soon as the binary appears skips those entirely.
# SHA-256 hex filenames never contain spaces so the ls subshell is safe.
for blob in $(ls -rS "${layout_dir}/blobs/sha256/" 2>/dev/null \
| awk -v d="${layout_dir}/blobs/sha256" '{print d"/"$0}'); do
[ -f "${blob}" ] || continue
# Detect gzip magic bytes (1f 8b) — config/manifest blobs are JSON, skip.
if [ "$(head -c 2 "${blob}" | od -An -tx1 | tr -d ' \n')" = "1f8b" ]; then
tar -xzf "${blob}" -C "${extract_dir}" 2>/dev/null || true
_extract_agent_binary "${extract_dir}" >/dev/null 2>&1 && break
fi
done
_extract_agent_binary "${extract_dir}"
}
cmd_install() {
bold "==> [1/6] Ensure system packages"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq ca-certificates
# The fleet_agent taskserv ASSUMES oras is already on the host. lian_node
# installs it; if this host doesn't have lian_node, the operator must
# provide an oras binary via the host's standard PATH.
command -v oras >/dev/null 2>&1 || fatal "oras not found on PATH — install lian_node first or provision oras separately"
# Resolver setup: the fleet_agent deploy owns the node's DNS-resolution
# contract. When FLEET_AGENT_HOSTS_ENTRIES is set (newline-separated
# `<ip> <name>` lines), they are written to /etc/hosts under a managed
# section. Idempotent (re-running re-writes the section; clearing the env
# to empty removes the managed section from disk on next install — the
# strip always runs, the append is conditional on entries being present).
bold "==> [1.5/6] /etc/hosts entries managed by fleet-agent"
local marker_begin="# fleet-agent managed entries — begin (do not edit by hand)"
local marker_end="# fleet-agent managed entries — end"
# Strip any previous managed section atomically, regardless of whether
# new entries follow.
local tmp
tmp=$(mktemp)
awk -v b="$marker_begin" -v e="$marker_end" \
'BEGIN{skip=0} $0==b{skip=1; next} $0==e{skip=0; next} skip==0{print}' \
/etc/hosts > "$tmp"
if [ -n "${FLEET_AGENT_HOSTS_ENTRIES:-}" ]; then
{
cat "$tmp"
echo "$marker_begin"
printf '%s\n' "$FLEET_AGENT_HOSTS_ENTRIES"
echo "$marker_end"
} > /etc/hosts.new
chmod 0644 /etc/hosts.new
mv /etc/hosts.new /etc/hosts
rm -f "$tmp"
echo " /etc/hosts managed section refreshed:"
printf '%s\n' "$FLEET_AGENT_HOSTS_ENTRIES" | sed 's/^/ /'
else
chmod 0644 "$tmp"
mv "$tmp" /etc/hosts
echo " /etc/hosts managed section removed (no entries declared — resolving via system DNS)"
fi
bold "==> [2/6] System user ${FLEET_AGENT_SYSTEM_USER}"
if id "${FLEET_AGENT_SYSTEM_USER}" >/dev/null 2>&1; then
echo " user ${FLEET_AGENT_SYSTEM_USER} already exists"
else
useradd --system --no-create-home --shell /usr/sbin/nologin "${FLEET_AGENT_SYSTEM_USER}"
echo " created system user ${FLEET_AGENT_SYSTEM_USER}"
fi
# The agent writes + chowns buildkitd TLS material at runtime (cert_install.rs).
# Requirements:
# - buildkit group must exist (chown root:buildkit target group)
# - fleet-agent must be a secondary member (allows chown :buildkit from non-root with CAP_CHOWN)
# - /etc/buildkit/tls must exist before the unit starts (ProtectSystem=strict
# creates readonly bind-mounts for all of /etc; ReadWritePaths punches holes
# only for directories that exist at bind-mount time)
groupadd -f buildkit
usermod -aG buildkit "${FLEET_AGENT_SYSTEM_USER}"
install -d -m 0755 /etc/buildkit
install -d -m 0750 -o root -g buildkit /etc/buildkit/tls
echo " buildkit group ensured; ${FLEET_AGENT_SYSTEM_USER} added; /etc/buildkit/tls pre-created"
bold "==> [3/6] Pull binary from ${FLEET_AGENT_IMAGE_REF}"
# Idempotency check: compare requested ref's manifest digest to the marker
# file. If equal and no FORCE → skip the pull+extract entirely. The binary,
# creds, unit, hosts entries from this run are still re-validated below; only
# the costly OCI fetch is avoided.
local current_digest=""
local skip_pull=false
if [ "$FORCE" != "true" ] && [ -r "$AGENT_IMAGE_MARK" ] && [ -x "$AGENT_BIN" ]; then
# Marker present + binary on disk → check if remote digest still matches.
local marker_ref marker_digest
marker_ref=$(awk -F= '/^image_ref=/{print $2; exit}' "$AGENT_IMAGE_MARK")
marker_digest=$(awk -F= '/^image_digest=/{print $2; exit}' "$AGENT_IMAGE_MARK")
if [ "$marker_ref" = "$FLEET_AGENT_IMAGE_REF" ] && [ -n "$marker_digest" ]; then
current_digest=$(DOCKER_CONFIG=/etc/fleet/docker \
oras manifest fetch --descriptor "$FLEET_AGENT_IMAGE_REF" 2>/dev/null \
| sed -n 's/.*"digest":"\([^"]*\)".*/\1/p' | head -1 || true)
if [ -n "$current_digest" ] && [ "$current_digest" = "$marker_digest" ]; then
skip_pull=true
echo " ↩ cache hit: ${marker_digest}"
echo " (marker /etc/fleet/fleet-agent.image.ref matches remote — PRVNG_FORCE=true to bypass)"
else
echo " digest drift detected — remote=${current_digest:-unknown}, marker=${marker_digest}"
fi
fi
elif [ "$FORCE" = "true" ]; then
echo " [force] bypassing image marker — pulling fresh"
fi
if [ "$skip_pull" = "true" ]; then
echo " binary already installed at expected digest — skipping pull+extract"
else
# Stop the running unit (if any) so we can swap the binary atomically.
if systemctl is-active --quiet fleet-agent 2>/dev/null; then
echo " stopping fleet-agent to swap binary"
systemctl stop fleet-agent
fi
local pull_tmp
pull_tmp=$(mktemp -d)
# Honour /etc/containers/auth.json (set up by fleet_base) for the OCI pull.
# Try flat-artifact pull first (cheapest path, works when the image was
# pushed with per-file annotations).
# Pass --platform so oras resolves the correct sub-manifest from an OCI
# image index without touching unreachable peer-platform manifests (Zot
# does not serve manifest-by-digest for manifests that have no direct tag).
local _OCI_PLATFORM
_OCI_PLATFORM="linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
DOCKER_CONFIG=/etc/fleet/docker \
oras pull "${FLEET_AGENT_IMAGE_REF}" --platform "${_OCI_PLATFORM}" -o "${pull_tmp}" \
|| { rm -rf "${pull_tmp}"; fatal "oras pull ${FLEET_AGENT_IMAGE_REF} failed"; }
local extracted
extracted=$(_extract_agent_binary "${pull_tmp}" 2>/dev/null || true)
if [ -z "${extracted}" ]; then
# `oras pull` ran but skipped all layers because the image is a Dockerfile-
# built container image (lamina/cargo-chef/distroless output) — its layers
# don't carry `org.opencontainers.image.title` annotations. Fall back to
# `oras copy --to-oci-layout` + extract every gzipped tar blob.
echo " flat-artifact pull yielded no binary — falling back to OCI layout extraction"
extracted=$(_extract_via_oci_layout "${FLEET_AGENT_IMAGE_REF}" "${pull_tmp}") \
|| { rm -rf "${pull_tmp}"; fatal "fleet-agent binary not found after OCI layout extraction"; }
fi
install -m 0755 -o root -g root "${extracted}" "${AGENT_BIN}"
rm -rf "${pull_tmp}"
echo " installed ${AGENT_BIN}"
# Write the install marker. Resolve current_digest now if we didn't earlier.
if [ -z "$current_digest" ]; then
current_digest=$(DOCKER_CONFIG=/etc/fleet/docker \
oras manifest fetch --descriptor "$FLEET_AGENT_IMAGE_REF" 2>/dev/null \
| sed -n 's/.*"digest":"\([^"]*\)".*/\1/p' | head -1 || true)
fi
install -d -m 0755 /etc/fleet
umask 077
cat > "${AGENT_IMAGE_MARK}.new" <<MARKER
image_ref=${FLEET_AGENT_IMAGE_REF}
image_digest=${current_digest:-unknown}
installed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
MARKER
chmod 0644 "${AGENT_IMAGE_MARK}.new"
mv "${AGENT_IMAGE_MARK}.new" "${AGENT_IMAGE_MARK}"
echo " wrote ${AGENT_IMAGE_MARK} (digest=${current_digest:-unknown})"
fi
bold "==> [4/6] Config TOML at ${AGENT_ETC}/config.toml (adr-010: no nats, no creds file)"
install -d -m 0750 -o "${FLEET_AGENT_SYSTEM_USER}" -g "${FLEET_AGENT_SYSTEM_USER}" "${AGENT_ETC}"
# Auto-detect node specs from /proc + df. NCL `declared` is a fallback only.
local detected_cpu detected_ram detected_disk
detected_cpu=$(nproc 2>/dev/null || echo "${FLEET_AGENT_DECLARED_CPU}")
detected_ram=$(awk '/^MemTotal:/ { kb=$2; gb=int((kb + 1048575) / 1048576); print gb; exit }' /proc/meminfo 2>/dev/null || echo "${FLEET_AGENT_DECLARED_RAM_GB}")
detected_disk=$(df -BG --output=size / 2>/dev/null | tail -1 | tr -d ' G' || echo "${FLEET_AGENT_DECLARED_DISK_GB}")
echo " auto-detected: cpu=${detected_cpu} ram_gb=${detected_ram} disk_gb=${detected_disk}"
echo " (NCL declared was: cpu=${FLEET_AGENT_DECLARED_CPU} ram_gb=${FLEET_AGENT_DECLARED_RAM_GB} disk_gb=${FLEET_AGENT_DECLARED_DISK_GB} — auto wins)"
local agent_config="${AGENT_ETC}/config.toml"
install -d -m 0755 "${AGENT_ETC}"
cat > "${agent_config}.new" <<TOML
# Auto-generated by install-fleet_agent.sh — do not edit by hand.
node_id = "${FLEET_AGENT_NODE_ID}"
declared_cpu = ${detected_cpu}
declared_ram_gb = ${detected_ram}
declared_disk_gb = ${detected_disk}
heartbeat_seconds = ${FLEET_AGENT_HEARTBEAT_S}
daemon_url = "${FLEET_AGENT_DAEMON_URL}"
TOML
chown "${FLEET_AGENT_SYSTEM_USER}:${FLEET_AGENT_SYSTEM_USER}" "${agent_config}.new"
chmod 0644 "${agent_config}.new"
# Append channel_token_path when the token is being written now OR when an
# existing token file is already on disk. This preserves the auth config
# across binary-only updates that run without FLEET_AGENT_CHANNEL_TOKEN set.
local token_file="${AGENT_ETC}/channel.token"
if [ -n "${FLEET_AGENT_CHANNEL_TOKEN:-}" ] || [ -f "${token_file}" ]; then
printf '\nchannel_token_path = "/etc/fleet-agent/channel.token"\n' >> "${agent_config}.new"
fi
mv "${agent_config}.new" "${agent_config}"
echo " wrote ${agent_config}"
# Stage channel bootstrap token — same token the daemon validates.
if [ -n "${FLEET_AGENT_CHANNEL_TOKEN:-}" ]; then
printf '%s' "${FLEET_AGENT_CHANNEL_TOKEN}" > "${token_file}.new"
chown "${FLEET_AGENT_SYSTEM_USER}:${FLEET_AGENT_SYSTEM_USER}" "${token_file}.new"
chmod 0400 "${token_file}.new"
mv "${token_file}.new" "${token_file}"
echo " wrote ${token_file} (mode 0400)"
elif [ -f "${token_file}" ]; then
echo " channel.token already on disk — keeping existing token"
else
echo " WARN: FLEET_AGENT_CHANNEL_TOKEN absent — channel.token not staged; channel will be unauthenticated"
fi
bold "==> [5/6] systemd unit ${AGENT_UNIT}"
cat > "${AGENT_UNIT}" <<EOF
[Unit]
Description=libre-forge fleet-agent (node-side claim binder + heartbeat reporter)
Documentation=https://github.com/anthropics/libre-forge
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${FLEET_AGENT_SYSTEM_USER}
Group=${FLEET_AGENT_SYSTEM_USER}
Environment=FLEET_AGENT_CONFIG=${AGENT_ETC}/config.toml
ExecStart=${AGENT_BIN}
ExecReload=/bin/kill -HUP \$MAINPID
Restart=on-failure
RestartSec=5s
# Hardening. CAP_CHOWN + CAP_DAC_OVERRIDE + CAP_FOWNER: write and chown
# /etc/buildkit/tls files (cert_install.rs). CAP_SETUID + CAP_SETGID: required
# in the bounding set so the setuid sudo binary can escalate to root for
# `systemctl reload/restart buildkitd`; without them the kernel strips SETUID
# from the executable's permitted set before sudo can change UID. The sudoers
# NOPASSWD rule (fleet-agent-buildkitd) limits what root can do from this path.
# NoNewPrivileges is intentionally absent.
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_SETUID CAP_SETGID
AmbientCapabilities=CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER
ReadWritePaths=${AGENT_ETC} /etc/buildkit/tls
[Install]
WantedBy=multi-user.target
EOF
# Sudoers rule: allow fleet-agent to reload/restart buildkitd so that
# new TLS material is picked up without manual intervention (cert_install.rs
# tries plain systemctl first, then sudo — see sudoers entry comment).
install -d -m 0750 /etc/sudoers.d
cat > /etc/sudoers.d/fleet-agent-buildkitd <<SUDOERS
# Allow fleet-agent to reload/restart buildkitd after TLS cert rotation.
${FLEET_AGENT_SYSTEM_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl reload buildkitd, /usr/bin/systemctl restart buildkitd
SUDOERS
chmod 0440 /etc/sudoers.d/fleet-agent-buildkitd
visudo -c -f /etc/sudoers.d/fleet-agent-buildkitd >/dev/null \
&& echo " sudoers rule written: /etc/sudoers.d/fleet-agent-buildkitd" \
|| { warn "sudoers syntax check failed — removing invalid rule"; rm -f /etc/sudoers.d/fleet-agent-buildkitd; }
# Polkit rule (supplementary, best-effort when polkit is installed).
if [ -d /etc/polkit-1/rules.d ]; then
cat > /etc/polkit-1/rules.d/50-fleet-agent-buildkit.rules <<POLKIT
polkit.addRule(function(action, subject) {
if (action.id === "org.freedesktop.systemd1.manage-units" &&
subject.user === "${FLEET_AGENT_SYSTEM_USER}" &&
(action.lookup("verb") === "reload" || action.lookup("verb") === "restart") &&
action.lookup("unit") === "buildkitd.service") {
return polkit.Result.YES;
}
});
POLKIT
echo " polkit rule written: /etc/polkit-1/rules.d/50-fleet-agent-buildkit.rules"
fi
systemctl daemon-reload
systemctl enable fleet-agent
systemctl restart fleet-agent
bold "==> [6/6] Startup verification"
# Best-effort: wait up to 30s for the unit to come up cleanly. Connectivity
# to the daemon's WebSocket is established asynchronously after start.
local waited=0
while [ "${waited}" -lt 30 ]; do
if systemctl is-active --quiet fleet-agent; then
echo " fleet-agent active after ${waited}s"
break
fi
sleep 2
waited=$((waited + 2))
done
systemctl is-active --quiet fleet-agent \
|| { systemctl status fleet-agent --no-pager || true; fatal "fleet-agent did not come up within 30s"; }
bold "==> Done"
echo " node_id : ${FLEET_AGENT_NODE_ID}"
echo " daemon_url : ${FLEET_AGENT_DAEMON_URL}"
echo " declared : cpu=${FLEET_AGENT_DECLARED_CPU} ram_gb=${FLEET_AGENT_DECLARED_RAM_GB} disk_gb=${FLEET_AGENT_DECLARED_DISK_GB}"
echo " heartbeat_s : ${FLEET_AGENT_HEARTBEAT_S}"
echo " binary : ${AGENT_BIN}"
echo " unit : ${AGENT_UNIT}"
}
cmd_status() {
echo "=== systemd ==="
systemctl status fleet-agent --no-pager || true
echo "=== binary ==="
if [ -x "${AGENT_BIN}" ]; then
echo " ${AGENT_BIN} present"
else
echo " ${AGENT_BIN} MISSING"
fi
echo "=== config ==="
local cfg="${AGENT_ETC}/config.toml"
if [ -f "${cfg}" ]; then
stat -c ' %n owner=%U:%G mode=%a size=%s' "${cfg}"
grep "^daemon_url" "${cfg}" || true
else
echo " ${cfg} MISSING"
fi
echo "=== buildkit TLS dir ==="
if [ -d /etc/buildkit/tls ]; then
stat -c ' %n owner=%U:%G mode=%a' /etc/buildkit/tls
ls -la /etc/buildkit/tls/ 2>/dev/null || true
else
echo " /etc/buildkit/tls MISSING — run install to pre-create"
fi
echo "=== polkit rule ==="
[ -f /etc/polkit-1/rules.d/50-fleet-agent-buildkit.rules ] \
&& echo " 50-fleet-agent-buildkit.rules present" \
|| echo " 50-fleet-agent-buildkit.rules MISSING — buildkitd reload will not work without manual restart"
echo "=== age key (fleet_base) ==="
[ -r /etc/fleet/age.key ] && echo " /etc/fleet/age.key present" || echo " /etc/fleet/age.key MISSING"
}
cmd_uninstall() {
systemctl disable --now fleet-agent 2>/dev/null || true
rm -f "${AGENT_UNIT}"
systemctl daemon-reload
rm -f "${AGENT_BIN}"
rm -f "${AGENT_ETC}/channel.token"
rmdir --ignore-fail-on-non-empty "${AGENT_ETC}" 2>/dev/null || true
rm -f /etc/polkit-1/rules.d/50-fleet-agent-buildkit.rules
rm -f /etc/sudoers.d/fleet-agent-buildkitd
# Preserve the system user, buildkit group membership, and /etc/buildkit/tls —
# buildkitd owns the dir and the group; fleet-agent just uses them.
echo "uninstalled (system user ${FLEET_AGENT_SYSTEM_USER} preserved; /etc/fleet/age.key preserved)"
}
case "$ACTION" in
install) cmd_install ;;
status) cmd_status ;;
uninstall) cmd_uninstall ;;
*) fatal "unknown action: $ACTION (install|status|uninstall)" ;;
esac