variant: flatcar version: 1.0.0 # Shared by every Flatcar VM: main.tf passes this file as each VM's inline # butane snippet. Loaded with file(), not templatefile(), so it is not a # template -- ${...} below reaches the VM as written. # # Keeps /etc/versions.env in sync with versions.env at the root of # AtlasHorizon/compose, which holds the image tag of every container on the # Flatcar VMs. A version bump merged to the branch below reaches each VM # within ~10 minutes with no plan or rebuild; see fetch-versions.sh for how # the affected units are found and restarted. # # To opt a container unit in, in its butane file: # [Unit] After=... versions-fetch.service # Wants=versions-fetch.service # [Service] EnvironmentFile=/etc/versions.env # and use the variable, in braces, wherever the image goes (the docker pull # and docker run arguments). In a .tftpl file the braces need a doubled # dollar sign so templatefile() leaves them alone. systemd: units: # Not enabled itself: the container units pull it in at boot (Wants= and # After=), so they start on the freshest copy, and the timer re-runs it. # A failed fetch keeps the last good copy, so a Gitea outage only delays # updates -- except on a brand-new VM, whose units cannot start until the # first fetch succeeds. Restart= keeps retrying that first fetch. - name: versions-fetch.service contents: | [Unit] Description=Fetch image versions from AtlasHorizon/compose Wants=network-online.target After=network-online.target [Service] Type=oneshot # Override with a drop-in to test unreleased versions on one VM. Environment=VERSIONS_BRANCH=main ExecStart=/opt/versions/fetch-versions.sh Restart=on-failure RestartSec=30s - name: versions-fetch.timer enabled: true contents: | [Unit] Description=Periodically re-fetch image versions [Timer] OnBootSec=10min OnUnitActiveSec=10min RandomizedDelaySec=1min [Install] WantedBy=timers.target storage: files: - path: /opt/versions/fetch-versions.sh mode: 0755 contents: inline: | #!/usr/bin/bash # Fetches versions.env and restarts the units that use a variable # whose value changed. A unit "uses" a variable when its unit file # contains it in braces (the docker pull/run image arguments), so # adding a container needs no change here. set -euo pipefail url="https://git.atlashorizon.net/AtlasHorizon/compose/raw/branch/$VERSIONS_BRANCH/versions.env" dest=/etc/versions.env tmp=$(mktemp /etc/.versions.env.XXXXXX) trap 'rm -f "$tmp"' EXIT curl -fsS --max-time 30 -o "$tmp" "$url" # Only comments, blank lines and VAR=value lines are accepted, and # at least one *_IMAGE line must be present, so an error page or a # truncated download never replaces the last good copy. if grep -qvE '^(#.*|[A-Z0-9_]+=[^[:space:]]+|)$' "$tmp" || ! grep -qE '^[A-Z0-9_]+_IMAGE=' "$tmp"; then echo "refusing malformed versions.env from $url" >&2 exit 1 fi if [ -f "$dest" ] && cmp -s "$tmp" "$dest"; then exit 0 fi # With no previous copy this is a brand-new VM: its units are waiting # on (or already failed without) this file, so start them outright. # Otherwise only units that are running get restarted, so a unit # stopped by hand stays stopped. if [ -f "$dest" ]; then action=try-restart changed=$(diff <(grep -E '^[A-Z0-9_]+=' "$dest" | sort) \ <(grep -E '^[A-Z0-9_]+=' "$tmp" | sort) | sed -nE 's/^[<>] ([A-Z0-9_]+)=.*/\1/p' | sort -u || true) else action=restart changed=$(sed -nE 's/^([A-Z0-9_]+)=.*/\1/p' "$tmp") fi chmod 0644 "$tmp" mv -f "$tmp" "$dest" for var in $changed; do echo "$var changed" pattern=$(printf '${%s}' "$var") # --no-block: the units are ordered after this one, so waiting on # their jobs from inside it would deadlock at boot. for unit in $(grep -lF "$pattern" /etc/systemd/system/*.service || true); do systemctl --no-block "$action" "$(basename "$unit")" done done