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_DATA_DIR: ${{ vars.PROD_DATA_DIR }}
|
||||||
PROD_CONFIG_DIR: ${{ vars.PROD_CONFIG_DIR }}
|
PROD_CONFIG_DIR: ${{ vars.PROD_CONFIG_DIR }}
|
||||||
PROD_PHOTOS_DIR: ${{ vars.PROD_PHOTOS_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_BIND: ${{ vars.PROD_WEB_BIND }}
|
||||||
PROD_WEB_PORT: ${{ vars.PROD_WEB_PORT }}
|
PROD_WEB_PORT: ${{ vars.PROD_WEB_PORT }}
|
||||||
PROD_STATE_VOLUME: ${{ vars.PROD_STATE_VOLUME }}
|
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
|
Python toolcache misses may still require setup-python to download Python from
|
||||||
its upstream release source. This removes Action source download dependence,
|
its upstream release source. This removes Action source download dependence,
|
||||||
not every possible external download. No Runner or NAS network configuration is changed.
|
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
|
service["image"] = image
|
||||||
proposed = self.secret_config()
|
proposed = self.secret_config()
|
||||||
service["environment"].update(proposed)
|
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(
|
service["environment"].update(
|
||||||
KOKONNA_MOCK=self.env["PROD_MOCK"],
|
KOKONNA_MOCK=self.env["PROD_MOCK"],
|
||||||
KOKONNA_WRITE_ENABLED=self.env["PROD_WRITE_ENABLED"],
|
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."""
|
"""Read-only HTTP smoke checks. No frame writes; token read from env/private .env."""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
@@ -26,4 +27,22 @@ for path in [
|
|||||||
body = response.read()
|
body = response.read()
|
||||||
print(path, "HTTP", response.status, "bytes", len(body))
|
print(path, "HTTP", response.status, "bytes", len(body))
|
||||||
assert response.status == 200
|
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")
|
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