flickr-immich-k8s-sync-operator 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flickr_immich_k8s_sync_operator/__init__.py +1 -1
- flickr_immich_k8s_sync_operator/__main__.py +3 -0
- flickr_immich_k8s_sync_operator/config.py +5 -0
- flickr_immich_k8s_sync_operator/operator.py +15 -5
- {flickr_immich_k8s_sync_operator-0.0.1.dist-info → flickr_immich_k8s_sync_operator-0.0.3.dist-info}/METADATA +24 -3
- flickr_immich_k8s_sync_operator-0.0.3.dist-info/RECORD +10 -0
- flickr_immich_k8s_sync_operator-0.0.1.dist-info/RECORD +0 -10
- {flickr_immich_k8s_sync_operator-0.0.1.dist-info → flickr_immich_k8s_sync_operator-0.0.3.dist-info}/WHEEL +0 -0
- {flickr_immich_k8s_sync_operator-0.0.1.dist-info → flickr_immich_k8s_sync_operator-0.0.3.dist-info}/entry_points.txt +0 -0
- {flickr_immich_k8s_sync_operator-0.0.1.dist-info → flickr_immich_k8s_sync_operator-0.0.3.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import signal
|
|
2
2
|
import sys
|
|
3
3
|
import threading
|
|
4
|
+
from dataclasses import fields
|
|
4
5
|
|
|
5
6
|
from loguru import logger as glogger
|
|
6
7
|
|
|
@@ -28,6 +29,8 @@ def main() -> None:
|
|
|
28
29
|
|
|
29
30
|
try:
|
|
30
31
|
cfg = OperatorConfig.from_env()
|
|
32
|
+
for f in fields(cfg):
|
|
33
|
+
glogger.info(" {}: {}", f.name, getattr(cfg, f.name))
|
|
31
34
|
except ValueError as exc:
|
|
32
35
|
glogger.error("Configuration error: {}", exc)
|
|
33
36
|
sys.exit(1)
|
|
@@ -17,6 +17,7 @@ class OperatorConfig:
|
|
|
17
17
|
job_names: list[str]
|
|
18
18
|
check_interval: int
|
|
19
19
|
restart_delay: int
|
|
20
|
+
skip_delay_on_oom: bool
|
|
20
21
|
|
|
21
22
|
@classmethod
|
|
22
23
|
def from_env(cls) -> OperatorConfig:
|
|
@@ -32,6 +33,9 @@ class OperatorConfig:
|
|
|
32
33
|
Seconds between check cycles (default ``60``).
|
|
33
34
|
RESTART_DELAY : str, optional
|
|
34
35
|
Seconds after failure before a Job is restarted (default ``3600``).
|
|
36
|
+
SKIP_DELAY_ON_OOM : str, optional
|
|
37
|
+
If ``"true"`` (case-insensitive), skip the restart delay when the
|
|
38
|
+
failure reason is ``OOMKilled`` (default ``"false"``).
|
|
35
39
|
|
|
36
40
|
Raises
|
|
37
41
|
------
|
|
@@ -48,4 +52,5 @@ class OperatorConfig:
|
|
|
48
52
|
job_names=job_names,
|
|
49
53
|
check_interval=int(os.environ.get("CHECK_INTERVAL", "60")),
|
|
50
54
|
restart_delay=int(os.environ.get("RESTART_DELAY", "3600")),
|
|
55
|
+
skip_delay_on_oom=os.environ.get("SKIP_DELAY_ON_OOM", "false").strip().lower() == "true",
|
|
51
56
|
)
|
|
@@ -123,9 +123,16 @@ class JobRestartOperator:
|
|
|
123
123
|
"""Log pod details and restart the Job if the restart delay has elapsed."""
|
|
124
124
|
elapsed = (datetime.now(timezone.utc) - failure_time).total_seconds()
|
|
125
125
|
|
|
126
|
-
self.
|
|
126
|
+
reasons = self._get_pod_failure_reasons(job_name)
|
|
127
|
+
skip_delay = self._cfg.skip_delay_on_oom and "OOMKilled" in reasons
|
|
127
128
|
|
|
128
|
-
if
|
|
129
|
+
if skip_delay:
|
|
130
|
+
self._log.info(
|
|
131
|
+
"\t{} failed with OOMKilled — skipping restart delay, restarting immediately.",
|
|
132
|
+
job_name,
|
|
133
|
+
)
|
|
134
|
+
self._restart_job(job_name, shutdown_event)
|
|
135
|
+
elif elapsed >= self._cfg.restart_delay:
|
|
129
136
|
self._log.info(
|
|
130
137
|
"\t{} failed {:.0f}s ago (>= {}s). Deleting and recreating...",
|
|
131
138
|
job_name,
|
|
@@ -142,8 +149,9 @@ class JobRestartOperator:
|
|
|
142
149
|
remaining,
|
|
143
150
|
)
|
|
144
151
|
|
|
145
|
-
def
|
|
146
|
-
"""List pods for *job_name
|
|
152
|
+
def _get_pod_failure_reasons(self, job_name: str) -> set[str]:
|
|
153
|
+
"""List pods for *job_name*, log exit codes + tail logs, and return termination reasons."""
|
|
154
|
+
reasons: set[str] = set()
|
|
147
155
|
try:
|
|
148
156
|
pods = self._core_v1.list_namespaced_pod(
|
|
149
157
|
self._cfg.namespace,
|
|
@@ -157,7 +165,8 @@ class JobRestartOperator:
|
|
|
157
165
|
if cs.state and cs.state.terminated:
|
|
158
166
|
exit_code = cs.state.terminated.exit_code
|
|
159
167
|
reason = cs.state.terminated.reason
|
|
160
|
-
|
|
168
|
+
if reason:
|
|
169
|
+
reasons.add(reason)
|
|
161
170
|
try:
|
|
162
171
|
tail = self._core_v1.read_namespaced_pod_log(
|
|
163
172
|
pod_name,
|
|
@@ -176,6 +185,7 @@ class JobRestartOperator:
|
|
|
176
185
|
)
|
|
177
186
|
except Exception:
|
|
178
187
|
self._log.warning("\tCould not retrieve pod details for {}", job_name)
|
|
188
|
+
return reasons
|
|
179
189
|
|
|
180
190
|
def _restart_job(self, job_name: str, shutdown_event: threading.Event) -> None:
|
|
181
191
|
"""Delete and recreate a Job from the cached manifest."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flickr-immich-k8s-sync-operator
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Operator for syncing photos from Flickr to Immich on Kubernetes
|
|
5
5
|
Project-URL: Homepage, https://github.com/vroomfondel/flickr-immich-k8s-sync-operator
|
|
6
6
|
Project-URL: Repository, https://github.com/vroomfondel/flickr-immich-k8s-sync-operator
|
|
@@ -26,12 +26,21 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
[](https://github.com/vroomfondel/flickr-immich-k8s-sync-operator/actions/workflows/mypynpytests.yml)
|
|
27
27
|

|
|
28
28
|
[](https://hub.docker.com/r/xomoxcc/flickr-immich-k8s-sync-operator/tags)
|
|
29
|
+
[](https://pepy.tech/projects/flickr-immich-k8s-sync-operator)
|
|
30
|
+
|
|
31
|
+
[](https://hub.docker.com/r/xomoxcc/flickr-immich-k8s-sync-operator)
|
|
29
32
|
|
|
30
33
|
Kubernetes operator that watches per-user Flickr download Jobs in a namespace,
|
|
31
34
|
restarts failed Jobs after a configurable delay, and retrieves pod logs and exit
|
|
32
35
|
codes for failed Jobs before restarting them. Designed to run alongside
|
|
33
36
|
[Immich](https://immich.app/) (self-hosted photo management).
|
|
34
37
|
|
|
38
|
+
### Operator in Action
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
*The operator monitoring Flickr download Jobs in [k9s](https://k9scli.io/), detecting failures, and scheduling restarts.*
|
|
43
|
+
|
|
35
44
|
## Status
|
|
36
45
|
|
|
37
46
|
**Beta (v0.0.1)** — the core job-restart loop is implemented and functional.
|
|
@@ -45,10 +54,19 @@ The project scaffolding (packaging, Docker image, CI) is in place.
|
|
|
45
54
|
- On failure (after a configurable delay), **deletes** the Job with `Foreground` propagation policy and **recreates** it from a cached manifest
|
|
46
55
|
- Logs pod exit codes and tail logs before every restart
|
|
47
56
|
|
|
57
|
+
### How it works
|
|
58
|
+
|
|
59
|
+
1. An Ansible playbook ([`kubectlstuff_flickr_downloader.yml`](https://github.com/vroomfondel/somestuff/blob/main/flickrdownloaderstuff/kubectlstuff_flickr_downloader.yml)) creates per-user Flickr download Jobs in the `flickr-downloader` namespace
|
|
60
|
+
2. Each Job runs [`flickr_download`](https://github.com/beaufour/flickr-download) with `BACKOFF_EXIT_ON_429=true`, so it exits immediately on HTTP 429 rate-limit errors instead of sleeping
|
|
61
|
+
3. Jobs mount host directories for config, backup, and cache per user
|
|
62
|
+
4. This operator watches all configured Jobs for failure conditions
|
|
63
|
+
5. When a Job fails, the operator logs pod exit codes and tail logs, waits `RESTART_DELAY` seconds (default 1 hour), then deletes and recreates the Job from a cached manifest
|
|
64
|
+
6. The operator uses namespace-scoped RBAC with minimal permissions (Jobs, Pods, Pod logs)
|
|
65
|
+
|
|
48
66
|
## Prerequisites
|
|
49
67
|
|
|
50
68
|
- A running Kubernetes cluster
|
|
51
|
-
- Flickr download Jobs already deployed — the operator manages their lifecycle (restart on failure), not initial creation
|
|
69
|
+
- Per-user Flickr download Jobs already deployed (e.g. via the Ansible playbook above) — the operator manages their lifecycle (restart on failure), not initial creation
|
|
52
70
|
- An [Immich](https://immich.app/) instance (for planned sync functionality)
|
|
53
71
|
|
|
54
72
|
## Configuration
|
|
@@ -60,6 +78,7 @@ The project scaffolding (packaging, Docker image, CI) is in place.
|
|
|
60
78
|
| `JOB_NAMES` | Comma-separated Job names to monitor (**required**) | — |
|
|
61
79
|
| `CHECK_INTERVAL` | Seconds between check cycles | `60` |
|
|
62
80
|
| `RESTART_DELAY` | Seconds to wait after failure before restart | `3600` |
|
|
81
|
+
| `SKIP_DELAY_ON_OOM` | Skip restart delay when failure reason is `OOMKilled` | `false` |
|
|
63
82
|
|
|
64
83
|
## Kubernetes Deployment
|
|
65
84
|
|
|
@@ -126,7 +145,7 @@ spec:
|
|
|
126
145
|
serviceAccountName: flickr-operator
|
|
127
146
|
containers:
|
|
128
147
|
- name: operator
|
|
129
|
-
image: flickr-immich-k8s-sync-operator:latest
|
|
148
|
+
image: xomoxcc/flickr-immich-k8s-sync-operator:latest
|
|
130
149
|
env:
|
|
131
150
|
- name: JOB_NAMES
|
|
132
151
|
value: "flickr-downloader-alice,flickr-downloader-bob"
|
|
@@ -138,6 +157,8 @@ spec:
|
|
|
138
157
|
# value: "60" # default
|
|
139
158
|
# - name: RESTART_DELAY
|
|
140
159
|
# value: "3600" # default
|
|
160
|
+
# - name: SKIP_DELAY_ON_OOM
|
|
161
|
+
# value: "false" # default
|
|
141
162
|
resources:
|
|
142
163
|
requests:
|
|
143
164
|
cpu: 50m
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
flickr_immich_k8s_sync_operator/__init__.py,sha256=lYbVbcSDvUYvYT-2dtVaBNc_X19jG-whK1BGt-43rVM,1086
|
|
2
|
+
flickr_immich_k8s_sync_operator/__main__.py,sha256=uh9plJ6myhg9b4YPTT0_EnPQ7v9G5BDdLS89K1er57U,1366
|
|
3
|
+
flickr_immich_k8s_sync_operator/config.py,sha256=8-0grb5goapBOU9EOIAvQX2mF8HnQbp3my834TYNXQc,2071
|
|
4
|
+
flickr_immich_k8s_sync_operator/operator.py,sha256=7JQEgGPEGTFgUq-dkum1IJjF0W8jUAIE-oFmSOiymwY,7962
|
|
5
|
+
flickr_immich_k8s_sync_operator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
flickr_immich_k8s_sync_operator-0.0.3.dist-info/METADATA,sha256=yIUugWRITGQej1FRYHmjwMViXPrJIBtTf8n_WkhZ5mk,8705
|
|
7
|
+
flickr_immich_k8s_sync_operator-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
flickr_immich_k8s_sync_operator-0.0.3.dist-info/entry_points.txt,sha256=2ka6kPb3391I_-DmcFWZj7UyCsjJZ3OOQ2xxdRO8Rfs,98
|
|
9
|
+
flickr_immich_k8s_sync_operator-0.0.3.dist-info/licenses/LICENSE.md,sha256=RG51X65V_wNLuyG-RGcLXxFsKyZnlH5wNvK_5mMlOag,7560
|
|
10
|
+
flickr_immich_k8s_sync_operator-0.0.3.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
flickr_immich_k8s_sync_operator/__init__.py,sha256=5Agx1JJrCUbtY8GWdBcO_t9HZeDqsxLbGl4Kbs2P5cg,1086
|
|
2
|
-
flickr_immich_k8s_sync_operator/__main__.py,sha256=IYEbP3_Te17oGpMp2NCcMiUceWceKGKWXqk6ZelgOBA,1238
|
|
3
|
-
flickr_immich_k8s_sync_operator/config.py,sha256=K0h0Mdh3ikh0UPloEZ_zCrsIY51n04tCp69JP5DZjrw,1754
|
|
4
|
-
flickr_immich_k8s_sync_operator/operator.py,sha256=mpi04TRa0slmf5FeCN9pX_hxvOTLkuYz7yBIL7At5K0,7470
|
|
5
|
-
flickr_immich_k8s_sync_operator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
flickr_immich_k8s_sync_operator-0.0.1.dist-info/METADATA,sha256=2O77NBy0y9ayAFdB9lJT-4PzKLMx3FC1Nsr7t1dDuw4,6948
|
|
7
|
-
flickr_immich_k8s_sync_operator-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
-
flickr_immich_k8s_sync_operator-0.0.1.dist-info/entry_points.txt,sha256=2ka6kPb3391I_-DmcFWZj7UyCsjJZ3OOQ2xxdRO8Rfs,98
|
|
9
|
-
flickr_immich_k8s_sync_operator-0.0.1.dist-info/licenses/LICENSE.md,sha256=RG51X65V_wNLuyG-RGcLXxFsKyZnlH5wNvK_5mMlOag,7560
|
|
10
|
-
flickr_immich_k8s_sync_operator-0.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|