1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
# shuriken-sync: a SEPARATE CronJob that publishes the generated
# /data/shuriken.sh/<site>/dist trees to the public web servers (fishfinger +
# blowfish) over the rsync DAEMON protocol (no SSH, no key). The frontends run
# rsyncd via inetd with `hosts allow = *.wg0.wan.buetow.org,*.wg0,localhost`; the
# k3s pods run on r-nodes that have .wg0 (WireGuard) connectivity, so they match
# and can push over the mesh. The writable modules `irregular-ninja` and
# `alt-irregular-ninja` are declared in frontends/etc/rsyncd.conf.tpl (deploy
# with `rex -f frontends/Rexfile rsync`).
#
# It only publishes a site when its content actually changed: each site's
# dist/status.json (image_count + total_size_bytes) is compared against a
# backup copy of the status.json from the last publish
# (<site>/.last-published-status.json on NFS -- the persisted "did it
# change" state). generated_at is excluded from the comparison because
# shuriken rewrites it on every run regardless of whether the source images
# changed, so comparing the whole file would publish every tick. The shuriken
# `--sync` over SSH stays available as an option; this cron job just uses the
# rsync protocol instead.
#
# The nfs-check initContainer refuses to start if NFS is down on the node, so
# rsync --delete can never run against an empty/stale source and wipe the live
# public site.
apiVersion: batch/v1
kind: CronJob
metadata:
name: shuriken-sync
namespace: services
spec:
# Twice a day, during daytime hours, well clear of the 04:00 generation
# run (avoids the flock race the two CronJobs used to hit when a tick
# landed on top of a still-running generation). Each tick is a cheap
# per-site status.json comparison; only sites that actually changed
# since the last publish get rsynced.
schedule: "0 10,18 * * *"
timeZone: Europe/Sofia
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 3600
template:
spec:
restartPolicy: Never
hostAliases:
# The frontends are reached over the WireGuard mesh (.wg0); cluster
# DNS does not resolve *.wg0 names, so pin them to the mesh IPs.
- ip: 192.168.2.111
hostnames:
- fishfinger.wg0
- fishfinger.wg0.wan.buetow.org
- ip: 192.168.2.110
hostnames:
- blowfish.wg0
- blowfish.wg0.wan.buetow.org
initContainers:
- name: nfs-check
image: busybox:stable
command:
- sh
- -c
- |
test -f /mnt/shuriken.sh/.nfs-sentinel || (
echo "ERROR: NFS sentinel missing at /mnt/shuriken.sh/.nfs-sentinel"
echo "refusing to publish; NFS likely unmounted on this node"
echo "rsync --delete against an empty/stale source would wipe the live site"
exit 1
)
volumeMounts:
- name: data
mountPath: /mnt
readOnly: true
containers:
- name: shuriken-sync
image: registry.lan.buetow.org:30001/shuriken:0.14.0
imagePullPolicy: Always
command: ["/bin/bash", "-c"]
args:
- |
set -euo pipefail
D=/data/shuriken.sh
# Mutually exclude with the generation CronJob: don't publish
# while a generation is running. Non-blocking -- if the lock is
# held (generation in progress), skip this tick and let the next
# one publish once generation finishes. flock auto-releases if
# the generation pod dies, so a crash never wedges the sync.
exec 9>"$D/.lock"
if ! flock -n 9; then
echo "shuriken-sync: generation in progress (lock held); skipping"
exit 0
fi
# Pull "image_count" and "total_size_bytes" out of a shuriken
# status.json without a JSON parser (the image ships grep/sed,
# not jq). The sidecar's layout is fixed
# (status-metadata.source.sh in shuriken.sh), so grep+digits
# is safe. generated_at is deliberately NOT part of the
# fingerprint: it's rewritten on every run regardless of
# whether the source images changed, so including it would
# make every tick look "changed".
status_fingerprint() {
grep '"image_count"' "$1" | grep -oE '[0-9]+'
grep '"total_size_bytes"' "$1" | grep -oE '[0-9]+'
}
published=0
for pair in irregular.ninja:irregular-ninja alt.irregular.ninja:alt-irregular-ninja; do
site=${pair%%:*}
mod=${pair##*:}
status="$D/$site/dist/status.json"
backup="$D/$site/.last-published-status.json"
if [ ! -f "$status" ]; then
echo "shuriken-sync: $status absent (generation in progress or failed); skipping $site"
continue
fi
cur=$(status_fingerprint "$status")
prev=""
[ -f "$backup" ] && prev=$(status_fingerprint "$backup")
if [ "$cur" = "$prev" ]; then
echo "shuriken-sync: $site unchanged since last publish; skipping"
continue
fi
echo "shuriken-sync: $site changed since last publish; publishing"
for srv in fishfinger.wg0 blowfish.wg0; do
echo "shuriken-sync: $site -> rsync://$srv/$mod/"
rsync -a --delete --info=stats1 \
"$D/$site/dist/" "rsync://$srv/$mod/"
done
cp "$status" "$backup"
published=1
done
if [ "$published" -eq 0 ]; then
echo "shuriken-sync: nothing changed; no sites published"
else
echo "shuriken-sync: done"
fi
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: "1"
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
runAsUser: 0
runAsGroup: 0
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: shuriken-data-pvc
- name: tmp
emptyDir:
sizeLimit: 256Mi
|