summaryrefslogtreecommitdiff
path: root/f3s/shuriken/helm-chart/templates/sync-cronjob.yaml
blob: f7e5c3a65959ccfa907d06ebf990105c2731ab5a (plain)
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