Preserve Photos deployment configuration and verify source dependencies
This commit is contained in:
@@ -56,6 +56,8 @@ jobs:
|
||||
PROD_DATA_DIR: ${{ vars.PROD_DATA_DIR }}
|
||||
PROD_CONFIG_DIR: ${{ vars.PROD_CONFIG_DIR }}
|
||||
PROD_PHOTOS_DIR: ${{ vars.PROD_PHOTOS_DIR }}
|
||||
SYNOLOGY_PHOTOS_URL: ${{ vars.SYNOLOGY_PHOTOS_URL }}
|
||||
SYNOLOGY_PHOTOS_CERT_SHA256: ${{ vars.SYNOLOGY_PHOTOS_CERT_SHA256 }}
|
||||
PROD_WEB_BIND: ${{ vars.PROD_WEB_BIND }}
|
||||
PROD_WEB_PORT: ${{ vars.PROD_WEB_PORT }}
|
||||
PROD_STATE_VOLUME: ${{ vars.PROD_STATE_VOLUME }}
|
||||
|
||||
@@ -301,3 +301,23 @@ and change the workflow pin. All validation steps remain unchanged.
|
||||
Python toolcache misses may still require setup-python to download Python from
|
||||
its upstream release source. This removes Action source download dependence,
|
||||
not every possible external download. No Runner or NAS network configuration is changed.
|
||||
|
||||
### Synology Photos dependencies
|
||||
|
||||
For a production photo source using Synology Photos person filtering, configure
|
||||
`SYNOLOGY_PHOTOS_URL` and `SYNOLOGY_PHOTOS_CERT_SHA256` as Gitea Variables. The
|
||||
certificate fingerprint must match the configured HTTPS endpoint. Keep the Photos
|
||||
session in the persistent `/config` directory; never put session credentials in
|
||||
Variables or repository files.
|
||||
|
||||
`PROD_PHOTOS_DIR` must identify the actual NAS photo directory mounted read-only
|
||||
at `/photos`. Adoption requires that mount to exist on the current container;
|
||||
changing this Variable alone does not create a new mount. Existing supplementary
|
||||
permission groups are preserved by deployment.
|
||||
|
||||
Restoring SQLite and session files does not restore these runtime dependencies.
|
||||
Production smoke checks now reject an enabled people-filtered source if its
|
||||
Photos configuration or stored connection is missing. These checks do not prove
|
||||
session validity or scan the complete catalog: after restoring a source, also
|
||||
verify its read-only `/api/v1/photo-sources/{id}/images?limit=1` endpoint before
|
||||
running an automation task.
|
||||
|
||||
@@ -376,6 +376,9 @@ class Deploy:
|
||||
service["image"] = image
|
||||
proposed = self.secret_config()
|
||||
service["environment"].update(proposed)
|
||||
for key in ("SYNOLOGY_PHOTOS_URL", "SYNOLOGY_PHOTOS_CERT_SHA256"):
|
||||
if self.env.get(key):
|
||||
service["environment"][key] = self.env[key]
|
||||
service["environment"].update(
|
||||
KOKONNA_MOCK=self.env["PROD_MOCK"],
|
||||
KOKONNA_WRITE_ENABLED=self.env["PROD_WRITE_ENABLED"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Read-only HTTP smoke checks. No frame writes; token read from env/private .env."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from urllib.request import Request, urlopen
|
||||
@@ -26,4 +27,22 @@ for path in [
|
||||
body = response.read()
|
||||
print(path, "HTTP", response.status, "bytes", len(body))
|
||||
assert response.status == 200
|
||||
# A restored job is not usable merely because the scheduler is ready.
|
||||
request = Request(
|
||||
base + "/api/v1/photo-sources", headers={"Authorization": "Bearer " + token}
|
||||
)
|
||||
with urlopen(request, timeout=15) as response:
|
||||
sources = json.load(response)
|
||||
if any(source.get("enabled") and source.get("people_filter") for source in sources):
|
||||
request = Request(
|
||||
base + "/api/v1/integrations/synology-photos",
|
||||
headers={"Authorization": "Bearer " + token},
|
||||
)
|
||||
with urlopen(request, timeout=15) as response:
|
||||
photos = json.load(response)
|
||||
if not photos.get("configured") or not photos.get("connected"):
|
||||
raise RuntimeError(
|
||||
"Enabled people-filtered photo source requires Photos connection"
|
||||
)
|
||||
print("Synology Photos dependency configured and connected")
|
||||
print("Read-only smoke checks passed")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Production smoke must reject incomplete restored photo dependencies."""
|
||||
|
||||
import io
|
||||
import json
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
from urllib.request import Request
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sources,connection,passes",
|
||||
[
|
||||
([], {}, True),
|
||||
([{"enabled": False, "people_filter": True}], {}, True),
|
||||
(
|
||||
[{"enabled": True, "people_filter": True}],
|
||||
{"configured": False, "connected": False},
|
||||
False,
|
||||
),
|
||||
(
|
||||
[{"enabled": True, "people_filter": True}],
|
||||
{"configured": True, "connected": False},
|
||||
False,
|
||||
),
|
||||
(
|
||||
[{"enabled": True, "people_filter": True}],
|
||||
{"configured": True, "connected": True},
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_smoke_validates_people_filter_dependency(
|
||||
monkeypatch, sources, connection, passes
|
||||
):
|
||||
calls = []
|
||||
|
||||
class Response(io.BytesIO):
|
||||
status = 200
|
||||
|
||||
def read_only(request: Request, timeout):
|
||||
assert request.get_method() == "GET"
|
||||
calls.append(request.full_url)
|
||||
value = sources if request.full_url.endswith("/photo-sources") else connection
|
||||
return Response(json.dumps(value).encode())
|
||||
|
||||
monkeypatch.setenv("FRAME_FLOW_TOKEN", "fixture")
|
||||
monkeypatch.setattr("urllib.request.urlopen", read_only)
|
||||
script = Path(__file__).parents[1] / "scripts/smoke-test.py"
|
||||
if passes:
|
||||
runpy.run_path(str(script))
|
||||
else:
|
||||
with pytest.raises(RuntimeError, match="requires Photos connection"):
|
||||
runpy.run_path(str(script))
|
||||
assert len(calls) >= 10
|
||||
Reference in New Issue
Block a user