mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
feat(web-app-nextcloud): run the Talk backend from pinned upstream components
nextcloud/aio-talk bundled signaling, Janus, NATS and an onboard TURN server behind a start script that rewrote their configs, and AIO publishes only build stamps, so the updaters could not raise it and the role had to guard Janus against config drift. The backend now runs as talk (Janus built from a meetecho/janus-gateway tag, files/Dockerfiles/Janus), talk_signaling (upstream strukturag/nextcloud-spreed-signaling image), talk_nats (official nats image) and talk_recording (built from a nextcloud-talk-recording release tag, files/Dockerfiles/Recording), each semver-pinned, single-replica, healthchecked and resource-limited. The Dockerfiles take every value as a build arg from compose, and Janus and the recording server get their config path through `command:`. The role renders janus.jcfg, the signaling server.conf and the recording server.conf; they match what AIO stamp 20260409_094910 generated apart from the service addresses and the session keys, which are now derived from the signaling secret. Secrets reach the services only through those files. Janus may join the host network in compose mode, which keeps direct media available there, and an assert refuses host mode in swarm. talk and talk_recording keep their keys, so one deploy replaces the AIO containers in place. The onboard TURN server is gone with its ports, vars and credential, so Talk uses web-svc-coturn only, and the deploy now stops when Talk is enabled without it instead of handing out no STUN or TURN server. The recording image installs the injected root CA as a Firefox policy when the CA is mounted, so recordings work against self-signed TLS, and the recording scratch volume stays off NFS. The Janus drift guard and the janus_config_drift filter are removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4a14a49da9
commit
5728502a19
21 files changed
+1160
-580
No files matched your search
@@ -0,0 +1,61 @@
|
||||
ARG JANUS_BASE_IMAGE
|
||||
ARG JANUS_BASE_VERSION
|
||||
FROM ${JANUS_BASE_IMAGE}:${JANUS_BASE_VERSION} AS build
|
||||
ARG JANUS_REPOSITORY
|
||||
ARG JANUS_REF
|
||||
|
||||
WORKDIR /src
|
||||
RUN set -ex; \
|
||||
apk upgrade --no-cache -a; \
|
||||
apk add --no-cache \
|
||||
ca-certificates \
|
||||
git \
|
||||
autoconf \
|
||||
automake \
|
||||
build-base \
|
||||
pkgconfig \
|
||||
libtool \
|
||||
util-linux \
|
||||
glib-dev \
|
||||
zlib-dev \
|
||||
openssl-dev \
|
||||
jansson-dev \
|
||||
libnice-dev \
|
||||
libconfig-dev \
|
||||
libsrtp-dev \
|
||||
libusrsctp-dev \
|
||||
gengetopt-dev \
|
||||
libwebsockets-dev; \
|
||||
git clone --recursive "${JANUS_REPOSITORY}" --depth=1 --single-branch --branch "${JANUS_REF}" /src; \
|
||||
/src/autogen.sh; \
|
||||
/src/configure --disable-rabbitmq --disable-mqtt --disable-boringssl; \
|
||||
make; \
|
||||
make install; \
|
||||
make configs; \
|
||||
rename -v ".jcfg.sample" ".jcfg" /usr/local/etc/janus/*.jcfg.sample
|
||||
|
||||
FROM ${JANUS_BASE_IMAGE}:${JANUS_BASE_VERSION}
|
||||
|
||||
COPY --from=build /usr/local /usr/local
|
||||
|
||||
RUN set -ex; \
|
||||
apk upgrade --no-cache -a; \
|
||||
apk add --no-cache \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
netcat-openbsd \
|
||||
glib \
|
||||
zlib \
|
||||
libssl3 \
|
||||
libcrypto3 \
|
||||
jansson \
|
||||
libnice \
|
||||
libconfig \
|
||||
libsrtp \
|
||||
libusrsctp \
|
||||
libwebsockets; \
|
||||
mkdir -p /usr/local/lib/janus/loggers; \
|
||||
adduser -D -H -u 1000 janus
|
||||
|
||||
USER janus
|
||||
ENTRYPOINT ["janus", "--disable-colors", "--log-stdout", "--full-trickle", "--debug-level", "3"]
|
||||
@@ -0,0 +1,50 @@
|
||||
ARG RECORDING_BASE_IMAGE
|
||||
ARG RECORDING_BASE_VERSION
|
||||
FROM ${RECORDING_BASE_IMAGE}:${RECORDING_BASE_VERSION}
|
||||
ARG RECORDING_REPOSITORY
|
||||
ARG RECORDING_REF
|
||||
ARG RECORDING_USER_ID
|
||||
ARG RECORDING_DIRECTORY
|
||||
ARG RECORDING_CONFIG_DIRECTORY
|
||||
|
||||
RUN set -ex; \
|
||||
apk upgrade --no-cache -a; \
|
||||
apk add --no-cache \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
bash \
|
||||
xvfb \
|
||||
ffmpeg \
|
||||
firefox \
|
||||
font-noto-all \
|
||||
font-noto-cjk \
|
||||
font-noto-cjk-extra \
|
||||
bind-tools \
|
||||
netcat-openbsd \
|
||||
git \
|
||||
shadow \
|
||||
pulseaudio \
|
||||
build-base \
|
||||
linux-headers \
|
||||
geckodriver; \
|
||||
useradd -d "${RECORDING_DIRECTORY}" --system recording -u "${RECORDING_USER_ID}"; \
|
||||
git clone --recursive "${RECORDING_REPOSITORY}" --depth=1 --single-branch --branch "${RECORDING_REF}" /src; \
|
||||
python3 -m pip install --no-cache-dir /src; \
|
||||
rm -rf /src; \
|
||||
mkdir -p "${RECORDING_CONFIG_DIRECTORY}"; \
|
||||
chown recording:recording -R "${RECORDING_DIRECTORY}"; \
|
||||
apk del --no-cache \
|
||||
git \
|
||||
shadow \
|
||||
build-base \
|
||||
linux-headers
|
||||
|
||||
ARG RECORDING_CA_CERT
|
||||
RUN if [ -n "${RECORDING_CA_CERT}" ]; then \
|
||||
mkdir -p /etc/firefox/policies \
|
||||
&& printf '{"policies":{"Certificates":{"Install":["%s"]}}}\n' "${RECORDING_CA_CERT}" > /etc/firefox/policies/policies.json; \
|
||||
fi
|
||||
|
||||
WORKDIR ${RECORDING_DIRECTORY}
|
||||
USER ${RECORDING_USER_ID}
|
||||
ENTRYPOINT ["python", "-m", "nextcloud.talk.recording"]
|
||||
@@ -13,8 +13,7 @@ const nextcloudTalkSettingsUrl = decodeDotenvQuotedValue(process.env.NEXTCLOUD_T
|
||||
const nextcloudTalkExpectedSignalingUrl = decodeDotenvQuotedValue(process.env.NEXTCLOUD_TALK_EXPECTED_SIGNALING_URL);
|
||||
const nextcloudTalkExpectedStunServer = decodeDotenvQuotedValue(process.env.NEXTCLOUD_TALK_EXPECTED_STUN_SERVER);
|
||||
const nextcloudTalkExpectedTurnServer = decodeDotenvQuotedValue(process.env.NEXTCLOUD_TALK_EXPECTED_TURN_SERVER);
|
||||
const nextcloudTalkUnexpectedStunServer = decodeDotenvQuotedValue(process.env.NEXTCLOUD_TALK_UNEXPECTED_STUN_SERVER);
|
||||
const nextcloudTalkUnexpectedTurnServer = decodeDotenvQuotedValue(process.env.NEXTCLOUD_TALK_UNEXPECTED_TURN_SERVER);
|
||||
const nextcloudTalkExpectedSignalingVersion = decodeDotenvQuotedValue(process.env.NEXTCLOUD_TALK_EXPECTED_SIGNALING_VERSION);
|
||||
|
||||
// Error strings emitted by the spreed admin UI when a signaling / STUN / TURN
|
||||
// test button reports a failure. Kept in one place so the list stays easy to
|
||||
@@ -43,8 +42,8 @@ const talkTestServerRequiredOkPattern = /OK:\s*Running version:\s*\S+/i;
|
||||
|
||||
// Talk admin settings are partly rendered as plain text and partly as
|
||||
// `<input value="...">` fields. `innerText()` alone would miss the input
|
||||
// values, so collect both text and form values before asserting presence or
|
||||
// absence of configured / legacy endpoints.
|
||||
// values, so collect both text and form values before asserting the
|
||||
// configured endpoints.
|
||||
async function collectNextcloudSettingsText(target) {
|
||||
const bodyText = await target.locator("body").innerText().catch(() => "");
|
||||
const formValues = await target.locator("input, textarea, select").evaluateAll((elements) => {
|
||||
@@ -80,18 +79,6 @@ async function expectNextcloudSettingValue(page, expectedValue, label) {
|
||||
.toContain(expectedValue);
|
||||
}
|
||||
|
||||
async function expectNextcloudSettingAbsent(page, unexpectedValue, label) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => collectNextcloudSettingsText(page),
|
||||
{
|
||||
timeout: resolveTimeout(30_000),
|
||||
message: `Expected ${label} to stay absent in the Nextcloud Talk admin settings: ${unexpectedValue}`
|
||||
}
|
||||
)
|
||||
.not.toContain(unexpectedValue);
|
||||
}
|
||||
|
||||
async function clickAllTalkTestServerButtonsAndVerify(page) {
|
||||
// Spreed renders the test result inside `<span class="test-connection">`
|
||||
// and the explicit re-test button (icon-only NcButton with
|
||||
@@ -154,6 +141,25 @@ async function clickAllTalkTestServerButtonsAndVerify(page) {
|
||||
}
|
||||
}
|
||||
|
||||
function runningVersionPattern(version) {
|
||||
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(`Running version:\\s*${escaped}(?:~[^;\\s]*)?(?=;|\\s|$)`, "i");
|
||||
}
|
||||
|
||||
async function expectPinnedSignalingVersion(page) {
|
||||
const connectionSpans = page.locator("span.test-connection");
|
||||
await expect
|
||||
.poll(async () => connectionSpans.allInnerTexts().catch(() => []), {
|
||||
timeout: resolveTimeout(60_000),
|
||||
message: `Expected the HPB signaling row to report the pinned version ${nextcloudTalkExpectedSignalingVersion} as its running version`,
|
||||
})
|
||||
.toEqual(expect.arrayContaining([expect.stringMatching(runningVersionPattern(nextcloudTalkExpectedSignalingVersion))]));
|
||||
|
||||
for (const text of await connectionSpans.allInnerTexts()) {
|
||||
expect(text, `The HPB signaling server reports chat-relay as missing: ${text}`).not.toMatch(/missing features:[^\n]*\bchat-relay\b/i);
|
||||
}
|
||||
}
|
||||
|
||||
exports.register = function (shared) {
|
||||
test("nextcloud talk admin settings", async ({ browser }) => {
|
||||
test.skip(!nextcloudTalkSettingsCheckEnabled, "Talk admin checks are disabled in the current Playwright env");
|
||||
@@ -163,6 +169,7 @@ exports.register = function (shared) {
|
||||
expect(nextcloudTalkExpectedSignalingUrl, "NEXTCLOUD_TALK_EXPECTED_SIGNALING_URL must be set when Talk admin checks are enabled").toBeTruthy();
|
||||
expect(nextcloudTalkExpectedStunServer, "NEXTCLOUD_TALK_EXPECTED_STUN_SERVER must be set when Talk admin checks are enabled").toBeTruthy();
|
||||
expect(nextcloudTalkExpectedTurnServer, "NEXTCLOUD_TALK_EXPECTED_TURN_SERVER must be set when Talk admin checks are enabled").toBeTruthy();
|
||||
expect(nextcloudTalkExpectedSignalingVersion, "NEXTCLOUD_TALK_EXPECTED_SIGNALING_VERSION must be set when Talk admin checks are enabled").toBeTruthy();
|
||||
|
||||
const browserContext = await browser.newContext({
|
||||
ignoreHTTPSErrors: true
|
||||
@@ -203,15 +210,8 @@ exports.register = function (shared) {
|
||||
await expectNextcloudSettingValue(adminPage, nextcloudTalkExpectedStunServer, "Talk STUN server");
|
||||
await expectNextcloudSettingValue(adminPage, nextcloudTalkExpectedTurnServer, "Talk TURN server");
|
||||
|
||||
if (nextcloudTalkUnexpectedStunServer) {
|
||||
await expectNextcloudSettingAbsent(adminPage, nextcloudTalkUnexpectedStunServer, "legacy Talk STUN server");
|
||||
}
|
||||
|
||||
if (nextcloudTalkUnexpectedTurnServer) {
|
||||
await expectNextcloudSettingAbsent(adminPage, nextcloudTalkUnexpectedTurnServer, "legacy Talk TURN server");
|
||||
}
|
||||
|
||||
await clickAllTalkTestServerButtonsAndVerify(adminPage);
|
||||
await expectPinnedSignalingVersion(adminPage);
|
||||
} finally {
|
||||
await adminPage.close().catch(() => {});
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def _strip_comments(content):
|
||||
out = []
|
||||
in_string = in_block = False
|
||||
index = 0
|
||||
while index < len(content):
|
||||
char = content[index]
|
||||
if in_block:
|
||||
if content.startswith("*/", index):
|
||||
in_block = False
|
||||
index += 1
|
||||
elif char == "\n":
|
||||
out.append(char)
|
||||
elif in_string:
|
||||
out.append(char)
|
||||
if char == "\\" and index + 1 < len(content):
|
||||
out.append(content[index + 1])
|
||||
index += 1
|
||||
elif char == '"':
|
||||
in_string = False
|
||||
elif char == '"':
|
||||
in_string = True
|
||||
out.append(char)
|
||||
elif content.startswith("/*", index):
|
||||
in_block = True
|
||||
index += 1
|
||||
elif char == "#" or content.startswith("//", index):
|
||||
while index + 1 < len(content) and content[index + 1] != "\n":
|
||||
index += 1
|
||||
else:
|
||||
out.append(char)
|
||||
index += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _without_blanks(line):
|
||||
out = []
|
||||
in_string = False
|
||||
for index, char in enumerate(line):
|
||||
if char == '"' and (index == 0 or line[index - 1] != "\\"):
|
||||
in_string = not in_string
|
||||
if in_string or not char.isspace():
|
||||
out.append(char)
|
||||
return "".join(out).rstrip(";")
|
||||
|
||||
|
||||
def _key(setting):
|
||||
for index, char in enumerate(setting):
|
||||
if char in "=:":
|
||||
return setting[:index]
|
||||
if char == '"':
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
def janus_settings(content, overrides):
|
||||
"""Return the settings a Janus ``.jcfg`` file declares, one per line.
|
||||
|
||||
Args:
|
||||
content: text of a Janus libconfig file.
|
||||
overrides: setting names to leave out, the ones a deployment sets on
|
||||
top of the stock file; empty to keep every setting.
|
||||
|
||||
Returns:
|
||||
Every line that holds more than comments and blanks, with the comments,
|
||||
a trailing ``;`` and the whitespace outside quoted strings removed, in
|
||||
file order.
|
||||
"""
|
||||
dropped = {str(name) for name in overrides}
|
||||
settings = []
|
||||
for raw in _strip_comments(str(content)).splitlines():
|
||||
setting = _without_blanks(raw)
|
||||
if setting and _key(setting) not in dropped:
|
||||
settings.append(setting)
|
||||
return settings
|
||||
|
||||
|
||||
def janus_config_drift(template, stock, overrides, launcher, target):
|
||||
"""Return why a rendered Janus config may not replace an image's own.
|
||||
|
||||
Args:
|
||||
template: the rendered config that is mounted over ``target``.
|
||||
stock: the image's own file at ``target``.
|
||||
overrides: setting names the template adds to the stock settings.
|
||||
launcher: the image's process manager config that starts Janus.
|
||||
target: the path the template is mounted at.
|
||||
|
||||
Returns:
|
||||
One message per problem; empty when Janus reads ``target`` and the
|
||||
template without ``overrides`` declares exactly the stock settings.
|
||||
"""
|
||||
problems = []
|
||||
started_with = re.compile(rf"--config={re.escape(str(target))}( |$)", re.MULTILINE)
|
||||
if not started_with.search(str(launcher)):
|
||||
problems.append(f"Janus is not started with --config={target}")
|
||||
expected = janus_settings(stock, [])
|
||||
rendered = janus_settings(template, overrides)
|
||||
if expected != rendered:
|
||||
first = next(
|
||||
(
|
||||
f"{index}: image {theirs!r}, template {ours!r}"
|
||||
for index, (theirs, ours) in enumerate(
|
||||
zip(expected, rendered, strict=False)
|
||||
)
|
||||
if theirs != ours
|
||||
),
|
||||
f"{len(expected)} settings in the image, {len(rendered)} in the template",
|
||||
)
|
||||
problems.append(f"the stock settings differ at setting {first}")
|
||||
return problems
|
||||
|
||||
|
||||
class FilterModule:
|
||||
def filters(self):
|
||||
return {
|
||||
"janus_config_drift": janus_config_drift,
|
||||
}
|
||||
@@ -22,7 +22,6 @@ config:
|
||||
configvalue: >-
|
||||
{{
|
||||
[
|
||||
NEXTCLOUD_HPB_STUN_ONBOARD_CONFIG if NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED else none,
|
||||
NEXTCLOUD_HPB_STUN_STANDALONE_CONFIG if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED else none
|
||||
]
|
||||
| select
|
||||
@@ -35,7 +34,6 @@ config:
|
||||
configvalue: >-
|
||||
{{
|
||||
[
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_CONFIG if NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED else none,
|
||||
NEXTCLOUD_HPB_TURN_STANDALONE_CONFIG if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED else none
|
||||
]
|
||||
| select
|
||||
|
||||
@@ -17,16 +17,12 @@ credentials:
|
||||
description: Initial password for the Nextcloud administrator (change immediately and enable 2FA)
|
||||
algorithm: sha256
|
||||
validation: ^[a-f0-9]{64}$
|
||||
talk_turn_secret:
|
||||
description: TURN REST secret for the optional onboard Talk TURN server
|
||||
algorithm: base64_prefixed_32
|
||||
validation: ^base64:[A-Za-z0-9+/]{43}=$
|
||||
talk_signaling_secret:
|
||||
description: Secret for Talk signaling
|
||||
algorithm: base64_prefixed_32
|
||||
validation: ^base64:[A-Za-z0-9+/]{43}=$
|
||||
talk_internal_secret:
|
||||
description: Internal secret for AIO Talk
|
||||
description: Secret internal clients such as the Talk recording server present to the signaling server
|
||||
algorithm: base64_prefixed_32
|
||||
validation: ^base64:[A-Za-z0-9+/]{43}=$
|
||||
context_agent_app_secret:
|
||||
|
||||
@@ -101,12 +101,6 @@ nextcloud:
|
||||
internal:
|
||||
http: 80
|
||||
fpm: 9000
|
||||
public:
|
||||
stun_turn: 3480
|
||||
stun_turn_tls: 5350
|
||||
relay:
|
||||
start: 50000
|
||||
end: 59999
|
||||
run_after:
|
||||
- web-app-keycloak
|
||||
- web-app-matomo
|
||||
@@ -200,34 +194,79 @@ talk:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-svc-coturn' in group_names }}"
|
||||
shared: "{{ 'web-svc-coturn' in group_names }}"
|
||||
name: nextcloud-talk
|
||||
image: nextcloud/aio-talk
|
||||
# nocheck: docker-version -- upstream publishes only AIO build stamps and froze latest at 20250325_084656; later stamps start Janus from a generated /conf/janus.jcfg that the janus_conf mount cannot replace
|
||||
version: "20260409_094910"
|
||||
backup:
|
||||
no_stop_required: false
|
||||
name: nextcloud-talk-janus
|
||||
image: alpine
|
||||
version: 3.23.3
|
||||
custom: nextcloud-talk-janus
|
||||
repository: https://github.com/meetecho/janus-gateway.git
|
||||
ref: v1.4.0
|
||||
# nocheck: single-replica the signaling server addresses exactly one Janus by URL and keeps every media session on it
|
||||
replicas: 1
|
||||
turn_server:
|
||||
onboard_enabled: "{{ not ( 'web-svc-coturn' in group_names ) }}"
|
||||
standalone_enabled: "{{ 'web-svc-coturn' in group_names }}"
|
||||
network_mode: bridge
|
||||
direct_media: false
|
||||
cpus: "2.0"
|
||||
mem_reservation: 1g
|
||||
mem_limit: 2g
|
||||
mem_reservation: 512m
|
||||
mem_limit: 1g
|
||||
pids_limit: 1024
|
||||
min_storage: 279MB
|
||||
min_storage: 600MB
|
||||
ports:
|
||||
internal:
|
||||
signaling: 8081
|
||||
websocket: 8188
|
||||
public:
|
||||
relay:
|
||||
start: 61000
|
||||
end: 61999
|
||||
healthcheck:
|
||||
flavor: tcp
|
||||
flavor: nc
|
||||
port_key: websocket
|
||||
# nocheck: playwright-service-flag the signaling server is part of the Talk deployment, covered by the Talk admin settings spec
|
||||
# nocheck: dynamic-flag the signaling server is part of the Talk deployment, so it follows the talk entry rather than a role's presence
|
||||
talk_signaling:
|
||||
enabled: "{{ lookup('config', 'web-app-nextcloud', 'services.talk.enabled') | bool }}"
|
||||
shared: false
|
||||
name: nextcloud-talk-signaling
|
||||
image: strukturag/nextcloud-spreed-signaling
|
||||
version: 2.1.1
|
||||
# nocheck: single-replica sessions live in this process; a second replica behind the VIP would split one call across servers
|
||||
replicas: 1
|
||||
max_stream_bitrate: 1048576
|
||||
max_screen_bitrate: 2097152
|
||||
cpus: "1.0"
|
||||
mem_reservation: 128m
|
||||
mem_limit: 512m
|
||||
pids_limit: 256
|
||||
min_storage: 223MB
|
||||
ports:
|
||||
internal:
|
||||
signaling: 8081
|
||||
healthcheck:
|
||||
flavor: http_status
|
||||
port_key: signaling
|
||||
# nocheck: healthcheck-timing-override a sidecar of a stack whose main service alone takes minutes; the value predates this default and has not been measured against it
|
||||
start_period: 20m
|
||||
path: api/v1/welcome
|
||||
# nocheck: playwright-service-flag internal message bus, reachable only from the Talk signaling server
|
||||
# nocheck: dynamic-flag the message bus is part of the Talk deployment, so it follows the talk entry rather than a role's presence
|
||||
talk_nats:
|
||||
enabled: "{{ lookup('config', 'web-app-nextcloud', 'services.talk.enabled') | bool }}"
|
||||
shared: false
|
||||
name: nextcloud-talk-nats
|
||||
image: nats
|
||||
version: 2.12.6-alpine
|
||||
# nocheck: single-replica an unclustered NATS server; the signaling server connects to exactly one
|
||||
replicas: 1
|
||||
cpus: "0.5"
|
||||
mem_reservation: 32m
|
||||
mem_limit: 256m
|
||||
pids_limit: 128
|
||||
min_storage: 211MB
|
||||
ports:
|
||||
internal:
|
||||
nats: 4222
|
||||
http: 8222
|
||||
healthcheck:
|
||||
flavor: http
|
||||
path: healthz
|
||||
whiteboard:
|
||||
name: nextcloud-whiteboard
|
||||
image: ghcr.io/nextcloud-releases/whiteboard
|
||||
@@ -281,11 +320,13 @@ talk_recording:
|
||||
enabled: "{{ lookup('config', 'web-app-nextcloud', 'services.talk.enabled') | bool }}"
|
||||
shared: false
|
||||
name: nextcloud-talk-recording
|
||||
image: nextcloud/aio-talk-recording
|
||||
# nocheck: docker-version -- upstream publishes only AIO build stamps and froze latest at 20250325_084656; stays on the AIO release of services.talk.version
|
||||
version: "20260409_094910"
|
||||
backup:
|
||||
no_stop_required: true
|
||||
image: python
|
||||
version: 3.14.3-alpine3.23
|
||||
custom: nextcloud-talk-recording
|
||||
repository: https://github.com/nextcloud/nextcloud-talk-recording.git
|
||||
ref: v0.2.1
|
||||
# nocheck: single-replica Talk starts and stops a recording on the server it reached; a second replica behind the VIP would receive half of the stop requests
|
||||
replicas: 1
|
||||
cpus: "2.0"
|
||||
mem_reservation: 1g
|
||||
mem_limit: 3g
|
||||
@@ -294,6 +335,9 @@ talk_recording:
|
||||
ports:
|
||||
internal:
|
||||
recording: 1234
|
||||
healthcheck:
|
||||
flavor: nc
|
||||
port_key: recording
|
||||
# nocheck: playwright-service-flag: verified by web-svc-css provider spec
|
||||
css:
|
||||
bond: 1
|
||||
|
||||
@@ -33,6 +33,8 @@ talk_recording_tmp:
|
||||
name: nextcloud_talk_recording_tmp
|
||||
type: volume
|
||||
backup: false
|
||||
# nocheck: nfs-false-data-loss Reason: scratch for Xvfb sockets, the Firefox profile and the ffmpeg output before its upload; single replica, nothing persists.
|
||||
nfs: false
|
||||
mounts:
|
||||
- service: talk_recording
|
||||
target: *tmp_target
|
||||
@@ -68,10 +70,29 @@ infinito_conf:
|
||||
janus_conf:
|
||||
type: config
|
||||
source: "{{ [ lookup('container', 'web-app-nextcloud', 'directories.volumes'), 'janus.jcfg' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: talk
|
||||
target: /usr/local/etc/janus/janus.jcfg
|
||||
when: "{{ lookup('config', 'web-app-nextcloud', 'services.talk.direct_media') | bool }}"
|
||||
when: "{{ lookup('config', 'web-app-nextcloud', 'services.talk.enabled') | bool }}"
|
||||
|
||||
talk_signaling_conf:
|
||||
type: secret
|
||||
source: "{{ [ lookup('container', 'web-app-nextcloud', 'directories.volumes'), 'signaling.conf' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: talk_signaling
|
||||
target: /config/server.conf
|
||||
when: "{{ lookup('config', 'web-app-nextcloud', 'services.talk.enabled') | bool }}"
|
||||
|
||||
talk_recording_conf:
|
||||
type: secret
|
||||
source: "{{ [ lookup('container', 'web-app-nextcloud', 'directories.volumes'), 'recording.conf' ] | path_join }}"
|
||||
mode: "0444"
|
||||
mounts:
|
||||
- service: talk_recording
|
||||
target: /conf/recording.conf
|
||||
when: "{{ lookup('config', 'web-app-nextcloud', 'services.talk.enabled') | bool }}"
|
||||
|
||||
php_hardening:
|
||||
type: config
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
---
|
||||
- name: "🛑 Assert Janus joins the host network only in compose mode"
|
||||
ansible.builtin.assert:
|
||||
that: DEPLOYMENT_MODE == 'compose'
|
||||
fail_msg: >-
|
||||
services.talk.network_mode host gives Janus the host network namespace,
|
||||
which docker stack deploy cannot provide. Use bridge in swarm mode.
|
||||
quiet: true
|
||||
when:
|
||||
- NEXTCLOUD_HPB_SIGNALING_ENABLED | bool
|
||||
- NEXTCLOUD_HPB_JANUS_HOST_NETWORK | bool
|
||||
|
||||
- name: "🛑 Assert Talk has the TURN server its calls relay through"
|
||||
ansible.builtin.assert:
|
||||
that: NEXTCLOUD_HPB_TURN_ACTIVE_SERVER | length > 0
|
||||
fail_msg: >-
|
||||
services.talk.enabled is true, but {{ NEXTCLOUD_HPB_TURN_STANDALONE_ROLE }} is not deployed
|
||||
on this host, so Talk would hand out no STUN or TURN server and calls behind NAT would carry
|
||||
no media. Deploy {{ NEXTCLOUD_HPB_TURN_STANDALONE_ROLE }} or set services.talk.enabled to false.
|
||||
quiet: true
|
||||
when: NEXTCLOUD_HPB_SIGNALING_ENABLED | bool
|
||||
|
||||
- name: "🧩 load docker, db and proxy for {{ application_id }}"
|
||||
include_role:
|
||||
name: sys-stk-full
|
||||
@@ -7,51 +28,6 @@
|
||||
application_id: web-app-nextcloud
|
||||
webserver_client_max_body_size: 10G
|
||||
|
||||
- name: "🔍 Read the Janus launcher and config from {{ NEXTCLOUD_HPB_IMAGE }}"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- container
|
||||
- run
|
||||
- --rm
|
||||
- --pull
|
||||
- always
|
||||
- --entrypoint
|
||||
- cat
|
||||
- "{{ NEXTCLOUD_HPB_IMAGE }}"
|
||||
- "{{ item }}"
|
||||
environment:
|
||||
CA_CONTAINER_ENABLED: "0"
|
||||
loop:
|
||||
- "{{ NEXTCLOUD_HPB_JANUS_LAUNCHER }}"
|
||||
- "{{ NEXTCLOUD_HPB_JANUS_CONFIG_TARGET }}"
|
||||
register: nextcloud_janus_image_files
|
||||
changed_when: false
|
||||
retries: 3
|
||||
delay: 30
|
||||
until: nextcloud_janus_image_files is succeeded
|
||||
when:
|
||||
- NEXTCLOUD_HPB_MEDIA_DIRECT | bool
|
||||
- IS_STACK_HOST | bool
|
||||
|
||||
- name: "🛑 Assert Janus reads the mounted config and the image's copy still matches"
|
||||
ansible.builtin.assert:
|
||||
that: nextcloud_janus_drift | length == 0
|
||||
fail_msg: >-
|
||||
templates/janus.jcfg.j2 cannot replace {{ NEXTCLOUD_HPB_JANUS_CONFIG_TARGET }}
|
||||
of {{ NEXTCLOUD_HPB_IMAGE }}: {{ nextcloud_janus_drift | join('; ') }}.
|
||||
Re-derive the template from the new image before deploying.
|
||||
quiet: true
|
||||
vars:
|
||||
nextcloud_janus_drift: >-
|
||||
{{ lookup('template', 'janus.jcfg.j2')
|
||||
| janus_config_drift(nextcloud_janus_image_files.results[1].stdout,
|
||||
NEXTCLOUD_HPB_JANUS_OVERRIDES,
|
||||
nextcloud_janus_image_files.results[0].stdout,
|
||||
NEXTCLOUD_HPB_JANUS_CONFIG_TARGET) }}
|
||||
when:
|
||||
- NEXTCLOUD_HPB_MEDIA_DIRECT | bool
|
||||
- IS_STACK_HOST | bool
|
||||
|
||||
- name: "📄 Deploy '{{ NEXTCLOUD_INIT_GATE_HOST }}'"
|
||||
copy:
|
||||
src: "{{ NEXTCLOUD_INIT_GATE_SRC }}"
|
||||
@@ -124,12 +100,43 @@
|
||||
- src: "fpm-pool.conf.j2"
|
||||
dest: "{{ NEXTCLOUD_HOST_FPM_POOL_PATH }}"
|
||||
|
||||
- name: "📄 Render the Janus config that publishes Talk media directly"
|
||||
ansible.builtin.include_tasks:
|
||||
file: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml') }}"
|
||||
vars:
|
||||
role_templates:
|
||||
- src: "janus.jcfg.j2"
|
||||
dest: "{{ NEXTCLOUD_HPB_JANUS_CONFIG_HOST }}"
|
||||
mode: "0644"
|
||||
when: NEXTCLOUD_HPB_MEDIA_DIRECT | bool
|
||||
- when: NEXTCLOUD_HPB_SIGNALING_ENABLED | bool
|
||||
block:
|
||||
- name: "📁 Create the Talk build directories"
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ NEXTCLOUD_HPB_JANUS_BUILD_DIRECTORY }}"
|
||||
- "{{ NEXTCLOUD_RECORDING_BUILD_DIRECTORY }}"
|
||||
|
||||
- name: "📄 Deploy the Talk Dockerfiles that build Janus and the recording server"
|
||||
stack_host_copy:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
loop:
|
||||
- { src: "Dockerfiles/Janus/Dockerfile", dest: "{{ NEXTCLOUD_HPB_JANUS_DOCKERFILE }}" }
|
||||
- { src: "Dockerfiles/Recording/Dockerfile", dest: "{{ NEXTCLOUD_RECORDING_DOCKERFILE }}" }
|
||||
notify:
|
||||
- compose-build
|
||||
- compose-up
|
||||
|
||||
- name: "📄 Render the Janus, signaling and recording configs (local + swarm peers)"
|
||||
ansible.builtin.include_tasks:
|
||||
file: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/render_replicated_templates.yml') }}"
|
||||
vars:
|
||||
role_templates:
|
||||
- src: "janus.jcfg.j2"
|
||||
dest: "{{ NEXTCLOUD_HPB_JANUS_CONFIG_HOST }}"
|
||||
mode: "0644"
|
||||
- src: "signaling.conf.j2"
|
||||
dest: "{{ NEXTCLOUD_HPB_SIGNALING_CONFIG_HOST }}"
|
||||
owner: "{{ NEXTCLOUD_HPB_SIGNALING_USER_ID }}"
|
||||
group: "{{ NEXTCLOUD_HPB_SIGNALING_USER_ID }}"
|
||||
mode: "0400"
|
||||
- src: "recording.conf.j2"
|
||||
dest: "{{ NEXTCLOUD_RECORDING_CONFIG_HOST }}"
|
||||
owner: "{{ NEXTCLOUD_RECORDING_USER_ID }}"
|
||||
group: "{{ NEXTCLOUD_RECORDING_USER_ID }}"
|
||||
mode: "0400"
|
||||
@@ -11,9 +11,7 @@
|
||||
{{ lookup('container_volumes', service_name) | indent(4) }}
|
||||
{{ lookup('container_healthcheck', service_name, samples=(groups[application_id] | default([]) | length) if DEPLOYMENT_MODE == 'swarm' else 1) | indent(4) }}
|
||||
{{ lookup('container_networks') | indent(4) }}
|
||||
{{ lookup('container_extra_hosts', extra_hosts=(
|
||||
['host.docker.internal:host-gateway'] if NEXTCLOUD_HPB_NETWORK_MODE == 'host' else []
|
||||
)) | indent(4) }}
|
||||
{{ lookup('container_extra_hosts') | indent(4) }}
|
||||
|
||||
{% set service_name = NEXTCLOUD_SERVICE %}
|
||||
{% set container_healthcheck_start_period = '20m' %}
|
||||
@@ -74,41 +72,62 @@
|
||||
|
||||
{% if NEXTCLOUD_HPB_SIGNALING_ENABLED | bool %}
|
||||
{% set service_name = NEXTCLOUD_HPB_SERVICE %}
|
||||
{% set container_port = NEXTCLOUD_HPB_SIGNALING_PORT %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
{{ lookup('container_image', application_id, 'talk') }}
|
||||
build:
|
||||
context: {{ NEXTCLOUD_HPB_JANUS_BUILD_DIRECTORY }}
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
JANUS_BASE_IMAGE: "{{ NEXTCLOUD_HPB_JANUS_BASE_IMAGE }}"
|
||||
JANUS_BASE_VERSION: "{{ NEXTCLOUD_HPB_JANUS_BASE_VERSION }}"
|
||||
JANUS_REPOSITORY: "{{ NEXTCLOUD_HPB_JANUS_REPOSITORY }}"
|
||||
JANUS_REF: "{{ NEXTCLOUD_HPB_JANUS_REF }}"
|
||||
{{ lookup('compose_only', 'pull_policy', 'never') }}
|
||||
{{ lookup('container_image', application_id, service_name, version=NEXTCLOUD_HPB_JANUS_TAG) }}
|
||||
{{ lookup('compose_only', 'container_name', NEXTCLOUD_HPB_CONTAINER) }}
|
||||
command: ["--config={{ NEXTCLOUD_HPB_JANUS_CONFIG_TARGET }}"]
|
||||
init: true
|
||||
{% if NEXTCLOUD_HPB_NETWORK_MODE == 'bridge' %}
|
||||
{% set _talk_ports = [] %}
|
||||
{% if NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED | bool and NEXTCLOUD_HPB_TURN_ONBOARD_PUBLISH_PORTS | bool %}
|
||||
{% set _ = _talk_ports.extend([
|
||||
{'service': 'nextcloud', 'protocol': 'stun_turn',
|
||||
'ip': NEXTCLOUD_HPB_TURN_ONBOARD_BIND_HOST,
|
||||
'transport': ['tcp', 'udp'], 'container': 'same'},
|
||||
{'service': 'nextcloud', 'protocol': 'relay',
|
||||
'ip': NEXTCLOUD_HPB_TURN_ONBOARD_BIND_HOST,
|
||||
'transport': ['udp', 'tcp'], 'container': 'same'},
|
||||
]) %}
|
||||
{% endif %}
|
||||
{% if NEXTCLOUD_HPB_MEDIA_DIRECT | bool %}
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
{{ lookup('container_volumes', service_name) | indent(4) }}
|
||||
{% set _ = _talk_ports.append(
|
||||
{'service': 'talk', 'protocol': 'relay',
|
||||
'ip': NEXTCLOUD_HPB_MEDIA_PUBLIC_IP,
|
||||
'transport': 'udp', 'container': 'same'}) %}
|
||||
{% endif %}
|
||||
{% if _talk_ports %}
|
||||
{{ lookup('container_ports', *_talk_ports) | indent(4) }}
|
||||
{% if DEPLOYMENT_MODE == 'compose' and NEXTCLOUD_HPB_JANUS_HOST_NETWORK | bool %}
|
||||
network_mode: host
|
||||
{% else %}
|
||||
{% if NEXTCLOUD_HPB_MEDIA_DIRECT | bool %}
|
||||
{{ lookup('container_ports',
|
||||
{'service': service_name, 'protocol': 'relay',
|
||||
'ip': NEXTCLOUD_HPB_MEDIA_PUBLIC_IP,
|
||||
'transport': 'udp', 'container': 'same'}) | indent(4) }}
|
||||
{% endif %}
|
||||
expose:
|
||||
- "{{ container_port }}"
|
||||
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
|
||||
{% else %}
|
||||
network_mode: {{ NEXTCLOUD_HPB_NETWORK_MODE }}
|
||||
- "{{ NEXTCLOUD_HPB_JANUS_PORT }}"
|
||||
{{ lookup('container_networks', own_network_only=True, provider_self_alias=False) | indent(4) }}
|
||||
{% endif %}
|
||||
|
||||
{% set service_name = NEXTCLOUD_HPB_SIGNALING_SERVICE %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, service_name) }}
|
||||
{{ lookup('compose_only', 'container_name', NEXTCLOUD_HPB_SIGNALING_CONTAINER) }}
|
||||
user: "{{ NEXTCLOUD_HPB_SIGNALING_USER_ID }}:{{ NEXTCLOUD_HPB_SIGNALING_USER_ID }}"
|
||||
command: ["{{ NEXTCLOUD_HPB_SIGNALING_BINARY }}", "-config", "{{ NEXTCLOUD_HPB_SIGNALING_CONFIG_TARGET }}"]
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
{{ lookup('container_volumes', service_name) | indent(4) }}
|
||||
expose:
|
||||
- "{{ NEXTCLOUD_HPB_SIGNALING_PORT }}"
|
||||
{{ lookup('container_extra_hosts', host_alias=NEXTCLOUD_HPB_JANUS_HOST_NETWORK) | indent(4) }}
|
||||
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
|
||||
|
||||
{% set service_name = NEXTCLOUD_HPB_NATS_SERVICE %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, service_name) }}
|
||||
{{ lookup('compose_only', 'container_name', NEXTCLOUD_HPB_NATS_CONTAINER) }}
|
||||
user: "{{ NEXTCLOUD_HPB_NATS_USER_ID }}:{{ NEXTCLOUD_HPB_NATS_USER_ID }}"
|
||||
command: ["nats-server", "--addr", "0.0.0.0", "--port", "{{ NEXTCLOUD_HPB_NATS_PORT }}", "--http_port", "{{ NEXTCLOUD_HPB_NATS_MONITOR_PORT }}"]
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
expose:
|
||||
- "{{ NEXTCLOUD_HPB_NATS_PORT }}"
|
||||
{{ lookup('container_networks', own_network_only=True, provider_self_alias=False) | indent(4) }}
|
||||
{% endif %}
|
||||
|
||||
{% if NEXTCLOUD_WHITEBOARD_ENABLED %}
|
||||
@@ -188,17 +207,28 @@
|
||||
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
|
||||
|
||||
{% if NEXTCLOUD_RECORDING_ENABLED | bool %}
|
||||
{% set service_name = 'talk_recording' %}
|
||||
{% set service_name = NEXTCLOUD_RECORDING_SERVICE %}
|
||||
{{ service_name }}:
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, 'talk_recording') }}
|
||||
build:
|
||||
context: {{ NEXTCLOUD_RECORDING_BUILD_DIRECTORY }}
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
RECORDING_BASE_IMAGE: "{{ NEXTCLOUD_RECORDING_BASE_IMAGE }}"
|
||||
RECORDING_BASE_VERSION: "{{ NEXTCLOUD_RECORDING_BASE_VERSION }}"
|
||||
RECORDING_REPOSITORY: "{{ NEXTCLOUD_RECORDING_REPOSITORY }}"
|
||||
RECORDING_REF: "{{ NEXTCLOUD_RECORDING_REF }}"
|
||||
RECORDING_USER_ID: "{{ NEXTCLOUD_RECORDING_USER_ID }}"
|
||||
RECORDING_DIRECTORY: "{{ NEXTCLOUD_RECORDING_DIRECTORY }}"
|
||||
RECORDING_CONFIG_DIRECTORY: "{{ NEXTCLOUD_RECORDING_CONFIG_TARGET | dirname }}"
|
||||
RECORDING_CA_CERT: "{{ NEXTCLOUD_RECORDING_CA_CERT }}"
|
||||
{{ lookup('compose_only', 'pull_policy', 'never') }}
|
||||
{{ lookup('container_image', application_id, service_name, version=NEXTCLOUD_RECORDING_TAG) }}
|
||||
{{ lookup('compose_only', 'container_name', NEXTCLOUD_RECORDING_CONTAINER) }}
|
||||
command: ["--config", "{{ NEXTCLOUD_RECORDING_CONFIG_TARGET }}"]
|
||||
environment:
|
||||
- HPB_DOMAIN={{ NEXTCLOUD_HPB_DOMAIN }}
|
||||
- NC_DOMAIN={{ NEXTCLOUD_DOMAIN }}
|
||||
- RECORDING_SECRET={{ NEXTCLOUD_RECORDING_SECRET }}
|
||||
- INTERNAL_SECRET={{ NEXTCLOUD_HPB_INTERNAL_SECRET }}
|
||||
- TZ={{ HOST_TIMEZONE }}
|
||||
TZ: "{{ HOST_TIMEZONE }}"
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
expose:
|
||||
- "{{ NEXTCLOUD_RECORDING_PORT }}"
|
||||
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
|
||||
|
||||
@@ -114,13 +114,9 @@ http {
|
||||
}
|
||||
{% if NEXTCLOUD_HPB_SIGNALING_ENABLED | bool %}
|
||||
location {{ NEXTCLOUD_HPB_SIGNALING_LOCATION }} {
|
||||
{% if NEXTCLOUD_HPB_NETWORK_MODE == 'host' %}
|
||||
proxy_pass http://host.docker.internal:{{ NEXTCLOUD_HPB_SIGNALING_PORT }}/;
|
||||
{% else %}
|
||||
set $upstream_nextcloud_talk http://talk:{{ NEXTCLOUD_HPB_SIGNALING_PORT }};
|
||||
set $upstream_nextcloud_talk http://{{ NEXTCLOUD_HPB_SIGNALING_SERVICE }}:{{ NEXTCLOUD_HPB_SIGNALING_PORT }};
|
||||
rewrite ^{{ NEXTCLOUD_HPB_SIGNALING_LOCATION }}(.*)$ /$1 break;
|
||||
proxy_pass $upstream_nextcloud_talk;
|
||||
{% endif %}
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
|
||||
@@ -54,19 +54,6 @@ OBJECTSTORE_S3_SECRET={{ lookup('objstore', application_id, 'secret_key') | dote
|
||||
OBJECTSTORE_S3_AUTOCREATE=false
|
||||
{% endif %}
|
||||
|
||||
{% if NEXTCLOUD_HPB_PLUGIN_ENABLED %}
|
||||
NC_DOMAIN={{ NEXTCLOUD_DOMAIN }}
|
||||
TALK_HOST={{ NEXTCLOUD_HPB_DOMAIN }}
|
||||
SIGNALING_SECRET={{ NEXTCLOUD_HPB_SIGNALING_SECRET }}
|
||||
INTERNAL_SECRET={{ NEXTCLOUD_HPB_INTERNAL_SECRET }}
|
||||
TURN_SECRET={{ NEXTCLOUD_HPB_TURN_ONBOARD_SECRET }}
|
||||
TALK_PORT={{ NEXTCLOUD_HPB_TURN_ONBOARD_PORT }}
|
||||
TURN_MIN_PORT={{ NEXTCLOUD_HPB_TURN_ONBOARD_RELAY_PORT_START }}
|
||||
TURN_MAX_PORT={{ NEXTCLOUD_HPB_TURN_ONBOARD_RELAY_PORT_END }}
|
||||
COTURN_MIN_PORT={{ NEXTCLOUD_HPB_TURN_ONBOARD_RELAY_PORT_START }}
|
||||
COTURN_MAX_PORT={{ NEXTCLOUD_HPB_TURN_ONBOARD_RELAY_PORT_END }}
|
||||
{% endif %}
|
||||
|
||||
{% if NEXTCLOUD_WHITEBOARD_ENABLED %}
|
||||
NEXTCLOUD_URL={{ NEXTCLOUD_URL }}
|
||||
JWT_SECRET_KEY={{ NEXTCLOUD_WHITEBOARD_JWT }}
|
||||
|
||||
@@ -34,12 +34,16 @@ certificates: {
|
||||
}
|
||||
|
||||
media: {
|
||||
{% if NEXTCLOUD_HPB_MEDIA_DIRECT | bool %}
|
||||
rtp_port_range = "{{ NEXTCLOUD_HPB_MEDIA_PORT_START }}-{{ NEXTCLOUD_HPB_MEDIA_PORT_END }}"
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
nat: {
|
||||
{% if NEXTCLOUD_HPB_MEDIA_DIRECT | bool %}
|
||||
nat_1_1_mapping = "{{ NEXTCLOUD_HPB_MEDIA_PUBLIC_IP }}"
|
||||
keep_private_host = true
|
||||
{% endif %}
|
||||
ice_ignore_list = "vmnet"
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ NEXTCLOUD_TALK_DIRECT_MEDIA_PORT_START={{ NEXTCLOUD_HPB_MEDIA_PORT_START }}
|
||||
|
||||
NEXTCLOUD_TALK_EXPECTED_SIGNALING_URL={{ NEXTCLOUD_HPB_SIGNALING_URL | dotenv_quote }}
|
||||
|
||||
NEXTCLOUD_TALK_EXPECTED_SIGNALING_VERSION={{ NEXTCLOUD_HPB_SIGNALING_VERSION | dotenv_quote }}
|
||||
|
||||
NEXTCLOUD_TALK_EXPECTED_STUN_SERVER={{ NEXTCLOUD_HPB_STUN_ACTIVE_CONFIG | dotenv_quote }}
|
||||
|
||||
NEXTCLOUD_TALK_EXPECTED_TURN_SERVER={{ NEXTCLOUD_HPB_TURN_ACTIVE_SERVER | dotenv_quote }}
|
||||
@@ -59,10 +61,6 @@ NEXTCLOUD_TALK_SETTINGS_CHECK_ENABLED={{ NEXTCLOUD_HPB_PLUGIN_ENABLED | string |
|
||||
|
||||
NEXTCLOUD_TALK_SETTINGS_URL={{ NEXTCLOUD_TALK_ADMIN_URL | dotenv_quote }}
|
||||
|
||||
NEXTCLOUD_TALK_UNEXPECTED_STUN_SERVER={{ (NEXTCLOUD_HPB_STUN_ONBOARD_CONFIG if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED | bool else '') | dotenv_quote }}
|
||||
|
||||
NEXTCLOUD_TALK_UNEXPECTED_TURN_SERVER={{ (NEXTCLOUD_HPB_TURN_ONBOARD_CONFIG.server | default('') if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED | bool else '') | dotenv_quote }}
|
||||
|
||||
OIDC_ISSUER_URL={{ OIDC.CLIENT.ISSUER_URL | dotenv_quote }}
|
||||
|
||||
OPENPROJECT_BASE_URL={{ (lookup('tls', 'web-app-openproject', 'url.base') if (lookup('config', application_id, 'services.openproject.enabled') | bool) else '') | dotenv_quote }}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
[logs]
|
||||
level = 30
|
||||
|
||||
[http]
|
||||
listen = 0.0.0.0:{{ NEXTCLOUD_RECORDING_PORT }}
|
||||
|
||||
[backend]
|
||||
allowall = false
|
||||
secret = {{ NEXTCLOUD_RECORDING_SECRET }}
|
||||
backends = backend-1
|
||||
skipverify = false
|
||||
maxmessagesize = 1024
|
||||
videowidth = 1920
|
||||
videoheight = 1080
|
||||
directory = {{ NEXTCLOUD_RECORDING_DIRECTORY }}
|
||||
|
||||
[backend-1]
|
||||
url = {{ NEXTCLOUD_HPB_BACKEND_URL }}
|
||||
secret = {{ NEXTCLOUD_RECORDING_SECRET }}
|
||||
skipverify = false
|
||||
|
||||
[signaling]
|
||||
signalings = signaling-1
|
||||
|
||||
[signaling-1]
|
||||
url = {{ NEXTCLOUD_HPB_BACKEND_URL }}{{ NEXTCLOUD_HPB_SIGNALING_LOCATION }}
|
||||
internalsecret = {{ NEXTCLOUD_HPB_INTERNAL_SECRET }}
|
||||
|
||||
[ffmpeg]
|
||||
extensionaudio = .ogg
|
||||
extensionvideo = .webm
|
||||
|
||||
[recording]
|
||||
browser = firefox
|
||||
driverPath = /usr/bin/geckodriver
|
||||
browserPath = /usr/bin/firefox
|
||||
@@ -0,0 +1,34 @@
|
||||
[http]
|
||||
listen = 0.0.0.0:{{ NEXTCLOUD_HPB_SIGNALING_PORT }}
|
||||
|
||||
[app]
|
||||
debug = false
|
||||
|
||||
[sessions]
|
||||
hashkey = {{ ((NEXTCLOUD_HPB_SIGNALING_SECRET ~ '/sessions/hashkey') | hash('sha256'))[:32] }}
|
||||
blockkey = {{ ((NEXTCLOUD_HPB_SIGNALING_SECRET ~ '/sessions/blockkey') | hash('sha256'))[:32] }}
|
||||
|
||||
[clients]
|
||||
internalsecret = {{ NEXTCLOUD_HPB_INTERNAL_SECRET }}
|
||||
|
||||
[backend]
|
||||
backends = backend-1
|
||||
allowall = false
|
||||
timeout = 10
|
||||
connectionsperhost = 8
|
||||
skipverify = false
|
||||
|
||||
[backend-1]
|
||||
urls = {{ NEXTCLOUD_HPB_BACKEND_URL }}
|
||||
secret = {{ NEXTCLOUD_HPB_SIGNALING_SECRET }}
|
||||
maxstreambitrate = {{ NEXTCLOUD_HPB_SIGNALING_MAX_STREAM_BITRATE }}
|
||||
maxscreenbitrate = {{ NEXTCLOUD_HPB_SIGNALING_MAX_SCREEN_BITRATE }}
|
||||
|
||||
[nats]
|
||||
url = {{ NEXTCLOUD_HPB_NATS_URL }}
|
||||
|
||||
[mcu]
|
||||
type = janus
|
||||
url = {{ NEXTCLOUD_HPB_JANUS_URL }}
|
||||
maxstreambitrate = {{ NEXTCLOUD_HPB_SIGNALING_MAX_STREAM_BITRATE }}
|
||||
maxscreenbitrate = {{ NEXTCLOUD_HPB_SIGNALING_MAX_SCREEN_BITRATE }}
|
||||
@@ -157,6 +157,7 @@ NEXTCLOUD_HPB_NETWORK_MODE: "{{ lookup('config', application_id, 'services.' ~ N
|
||||
NEXTCLOUD_HPB_PLUGIN_ENABLED: "{{ lookup('config', application_id, 'addons.spreed.enabled') }}"
|
||||
NEXTCLOUD_HPB_INTERNAL_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.talk_internal_secret') }}"
|
||||
NEXTCLOUD_HPB_DOMAIN: "{{ NEXTCLOUD_DOMAIN }}"
|
||||
NEXTCLOUD_HPB_BACKEND_URL: "{{ NEXTCLOUD_PROTOCOL }}://{{ NEXTCLOUD_HPB_DOMAIN }}"
|
||||
|
||||
# nocheck: host-local-fact read only in compose mode, where the host rendering this file is the host Janus publishes its media ports on; Ansible omits the key on a host without the ip binary, which leaves the media on TURN
|
||||
NEXTCLOUD_HPB_HOST_ADDRESSES: "{{ ansible_facts['all_ipv4_addresses'] | default([]) }}"
|
||||
@@ -164,7 +165,6 @@ NEXTCLOUD_HPB_MEDIA_DIRECT: >-
|
||||
{{ (lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.direct_media') | bool)
|
||||
and (NEXTCLOUD_HPB_SIGNALING_ENABLED | bool)
|
||||
and (NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED | bool)
|
||||
and (lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.network_mode') == 'bridge')
|
||||
and (DEPLOYMENT_MODE == 'compose')
|
||||
and (networks.internet.ip4 in NEXTCLOUD_HPB_HOST_ADDRESSES) }}
|
||||
NEXTCLOUD_HPB_MEDIA_PUBLIC_IP: "{{ networks.internet.ip4 }}"
|
||||
@@ -172,38 +172,39 @@ NEXTCLOUD_HPB_MEDIA_PORT_START: "{{ lookup('config', application_id, 'services.'
|
||||
NEXTCLOUD_HPB_MEDIA_PORT_END: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.ports.public.relay.end') }}"
|
||||
NEXTCLOUD_HPB_JANUS_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.volumes'), 'janus.jcfg' ] | path_join }}"
|
||||
NEXTCLOUD_HPB_JANUS_CONFIG_TARGET: "{{ lookup('volume', application_id, 'janus_conf').target }}"
|
||||
NEXTCLOUD_HPB_JANUS_LAUNCHER: "/supervisord.conf"
|
||||
NEXTCLOUD_HPB_JANUS_OVERRIDES:
|
||||
- rtp_port_range
|
||||
- nat_1_1_mapping
|
||||
- keep_private_host
|
||||
NEXTCLOUD_HPB_IMAGE: "{{ lookup('image', application_id, NEXTCLOUD_HPB_SERVICE) }}"
|
||||
NEXTCLOUD_HPB_JANUS_PORT: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.ports.internal.websocket') }}"
|
||||
NEXTCLOUD_HPB_JANUS_HOST_NETWORK: "{{ NEXTCLOUD_HPB_NETWORK_MODE == 'host' }}"
|
||||
NEXTCLOUD_HPB_JANUS_URL: "ws://{{ 'host.docker.internal' if NEXTCLOUD_HPB_JANUS_HOST_NETWORK | bool else NEXTCLOUD_HPB_SERVICE }}:{{ NEXTCLOUD_HPB_JANUS_PORT }}"
|
||||
NEXTCLOUD_HPB_JANUS_REPOSITORY: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.repository') }}"
|
||||
NEXTCLOUD_HPB_JANUS_REF: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.ref') }}"
|
||||
NEXTCLOUD_HPB_JANUS_BASE_IMAGE: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.image') }}"
|
||||
NEXTCLOUD_HPB_JANUS_BASE_VERSION: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.version') }}"
|
||||
NEXTCLOUD_HPB_JANUS_TAG: "{{ NEXTCLOUD_HPB_JANUS_REF }}-{{ NEXTCLOUD_HPB_JANUS_BASE_VERSION }}"
|
||||
NEXTCLOUD_HPB_JANUS_BUILD_DIRECTORY: "{{ [ lookup('container', application_id, 'directories.services'), 'talk-janus' ] | path_join }}"
|
||||
NEXTCLOUD_HPB_JANUS_DOCKERFILE: "{{ [ NEXTCLOUD_HPB_JANUS_BUILD_DIRECTORY, 'Dockerfile' ] | path_join }}"
|
||||
|
||||
NEXTCLOUD_HPB_SIGNALING_SERVICE: "talk_signaling"
|
||||
NEXTCLOUD_HPB_SIGNALING_CONTAINER: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SIGNALING_SERVICE ~ '.name') }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_VERSION: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SIGNALING_SERVICE ~ '.version') }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.talk_signaling_secret') }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_LOCATION: "/standalone-signaling/"
|
||||
NEXTCLOUD_HPB_SIGNALING_PORT: "{{ lookup('config', application_id, 'services.talk.ports.internal.signaling') }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_PORT: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SIGNALING_SERVICE ~ '.ports.internal.signaling') }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_URL: "{{ [ NEXTCLOUD_URL, NEXTCLOUD_HPB_SIGNALING_LOCATION ] | url_join }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_ENABLED: "{{ NEXTCLOUD_HPB_PLUGIN_ENABLED }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_MAX_STREAM_BITRATE: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SIGNALING_SERVICE ~ '.max_stream_bitrate') }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_MAX_SCREEN_BITRATE: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SIGNALING_SERVICE ~ '.max_screen_bitrate') }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_USER_ID: 850
|
||||
NEXTCLOUD_HPB_SIGNALING_BINARY: "/usr/bin/nextcloud-spreed-signaling"
|
||||
NEXTCLOUD_HPB_SIGNALING_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.volumes'), 'signaling.conf' ] | path_join }}"
|
||||
NEXTCLOUD_HPB_SIGNALING_CONFIG_TARGET: "{{ lookup('volume', application_id, 'talk_signaling_conf').target }}"
|
||||
NEXTCLOUD_TALK_ADMIN_URL: "{{ [ NEXTCLOUD_URL, 'settings/admin/talk' ] | url_join }}"
|
||||
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_PORT: "{{ lookup('config', application_id, 'services.nextcloud.ports.public.stun_turn') }}"
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_BIND_HOST: "{{ DOCKER_PUBLIC_BIND_HOST }}"
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_SERVICE ~ '.turn_server.onboard_enabled') if NEXTCLOUD_HPB_PLUGIN_ENABLED
|
||||
else false }}"
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.talk_turn_secret') }}"
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_PUBLISH_PORTS: "{{ not DOCKER_IN_CONTAINER | bool }}"
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_RELAY_PORT_START: "{{ lookup('config', application_id, 'services.nextcloud.ports.public.relay.start') }}"
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_RELAY_PORT_END: "{{ lookup('config', application_id, 'services.nextcloud.ports.public.relay.end') }}"
|
||||
NEXTCLOUD_HPB_STUN_ONBOARD_CONFIG: "{{ NEXTCLOUD_HPB_DOMAIN }}:{{ NEXTCLOUD_HPB_TURN_ONBOARD_PORT }}"
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_CONFIG: >-
|
||||
{{
|
||||
{
|
||||
'server': NEXTCLOUD_HPB_DOMAIN ~ ':' ~ NEXTCLOUD_HPB_TURN_ONBOARD_PORT,
|
||||
'secret': NEXTCLOUD_HPB_TURN_ONBOARD_SECRET,
|
||||
'ttl': 86400,
|
||||
'protocols': 'udp,tcp'
|
||||
}
|
||||
}}
|
||||
NEXTCLOUD_HPB_NATS_SERVICE: "talk_nats"
|
||||
NEXTCLOUD_HPB_NATS_CONTAINER: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_NATS_SERVICE ~ '.name') }}"
|
||||
NEXTCLOUD_HPB_NATS_PORT: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_NATS_SERVICE ~ '.ports.internal.nats') }}"
|
||||
NEXTCLOUD_HPB_NATS_MONITOR_PORT: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_HPB_NATS_SERVICE ~ '.ports.internal.http') }}"
|
||||
NEXTCLOUD_HPB_NATS_URL: "nats://{{ NEXTCLOUD_HPB_NATS_SERVICE }}:{{ NEXTCLOUD_HPB_NATS_PORT }}"
|
||||
NEXTCLOUD_HPB_NATS_USER_ID: 65534
|
||||
|
||||
NEXTCLOUD_HPB_TURN_STANDALONE_ROLE: "web-svc-coturn"
|
||||
NEXTCLOUD_HPB_TURN_STANDALONE_PORT: "{{ lookup('config', NEXTCLOUD_HPB_TURN_STANDALONE_ROLE, 'services.coturn.ports.public.stun_turn') }}"
|
||||
@@ -221,26 +222,8 @@ NEXTCLOUD_HPB_TURN_STANDALONE_CONFIG: >-
|
||||
'protocols': 'udp,tcp'
|
||||
}
|
||||
}}
|
||||
NEXTCLOUD_HPB_STUN_ACTIVE_CONFIG: >-
|
||||
{{
|
||||
NEXTCLOUD_HPB_STUN_STANDALONE_CONFIG
|
||||
if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED | bool
|
||||
else (
|
||||
NEXTCLOUD_HPB_STUN_ONBOARD_CONFIG
|
||||
if NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED | bool
|
||||
else ''
|
||||
)
|
||||
}}
|
||||
NEXTCLOUD_HPB_TURN_ACTIVE_CONFIG: >-
|
||||
{{
|
||||
NEXTCLOUD_HPB_TURN_STANDALONE_CONFIG
|
||||
if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED | bool
|
||||
else (
|
||||
NEXTCLOUD_HPB_TURN_ONBOARD_CONFIG
|
||||
if NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED | bool
|
||||
else {}
|
||||
)
|
||||
}}
|
||||
NEXTCLOUD_HPB_STUN_ACTIVE_CONFIG: "{{ NEXTCLOUD_HPB_STUN_STANDALONE_CONFIG if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED | bool else '' }}"
|
||||
NEXTCLOUD_HPB_TURN_ACTIVE_CONFIG: "{{ NEXTCLOUD_HPB_TURN_STANDALONE_CONFIG if NEXTCLOUD_HPB_TURN_STANDALONE_ENABLED | bool else {} }}"
|
||||
NEXTCLOUD_HPB_TURN_ACTIVE_SERVER: "{{ NEXTCLOUD_HPB_TURN_ACTIVE_CONFIG.server | default('') }}"
|
||||
|
||||
NEXTCLOUD_WHITEBOARD_SERVICE: "whiteboard"
|
||||
@@ -261,6 +244,18 @@ NEXTCLOUD_RECORDING_CONTAINER: "{{ lookup('config', application_id, 'services.'
|
||||
NEXTCLOUD_RECORDING_ENABLED: "{{ NEXTCLOUD_HPB_PLUGIN_ENABLED }}"
|
||||
NEXTCLOUD_RECORDING_PORT: "{{ lookup('config', application_id, 'services.talk_recording.ports.internal.recording') }}"
|
||||
NEXTCLOUD_RECORDING_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.talk_recording_secret') }}"
|
||||
NEXTCLOUD_RECORDING_REPOSITORY: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_RECORDING_SERVICE ~ '.repository') }}"
|
||||
NEXTCLOUD_RECORDING_REF: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_RECORDING_SERVICE ~ '.ref') }}"
|
||||
NEXTCLOUD_RECORDING_BASE_IMAGE: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_RECORDING_SERVICE ~ '.image') }}"
|
||||
NEXTCLOUD_RECORDING_BASE_VERSION: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_RECORDING_SERVICE ~ '.version') }}"
|
||||
NEXTCLOUD_RECORDING_TAG: "{{ NEXTCLOUD_RECORDING_REF }}-{{ NEXTCLOUD_RECORDING_BASE_VERSION }}"
|
||||
NEXTCLOUD_RECORDING_BUILD_DIRECTORY: "{{ [ lookup('container', application_id, 'directories.services'), 'talk-recording' ] | path_join }}"
|
||||
NEXTCLOUD_RECORDING_DOCKERFILE: "{{ [ NEXTCLOUD_RECORDING_BUILD_DIRECTORY, 'Dockerfile' ] | path_join }}"
|
||||
NEXTCLOUD_RECORDING_USER_ID: 122
|
||||
NEXTCLOUD_RECORDING_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.volumes'), 'recording.conf' ] | path_join }}"
|
||||
NEXTCLOUD_RECORDING_CONFIG_TARGET: "{{ lookup('volume', application_id, 'talk_recording_conf').target }}"
|
||||
NEXTCLOUD_RECORDING_DIRECTORY: "{{ lookup('volume', application_id, 'talk_recording_tmp').target }}"
|
||||
NEXTCLOUD_RECORDING_CA_CERT: "{{ CA_TRUST.inject_cert_container if lookup('ca_injected', application_id) else '' }}"
|
||||
|
||||
NEXTCLOUD_COLLABORA_URL: "{{ lookup('tls', 'web-svc-collabora', 'url.base') }}"
|
||||
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
OVERRIDES = ("rtp_port_range", "nat_1_1_mapping", "keep_private_host")
|
||||
TARGET = "/usr/local/etc/janus/janus.jcfg"
|
||||
LAUNCHER = (
|
||||
"[program:janus]\n"
|
||||
"command=janus --config=/usr/local/etc/janus/janus.jcfg --disable-colors"
|
||||
" --log-stdout --full-trickle --debug-level 3\n"
|
||||
)
|
||||
|
||||
|
||||
def _load_module(rel_path: str, name: str):
|
||||
path = PROJECT_ROOT / rel_path
|
||||
spec = importlib.util.spec_from_file_location(name, str(path))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
MODULE = _load_module(
|
||||
"roles/web-app-nextcloud/filter_plugins/nextcloud_janus.py", "nextcloud_janus"
|
||||
)
|
||||
|
||||
|
||||
def _rendered_template() -> str:
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(
|
||||
str(PROJECT_ROOT / "roles/web-app-nextcloud/templates")
|
||||
),
|
||||
undefined=StrictUndefined,
|
||||
keep_trailing_newline=True,
|
||||
autoescape=select_autoescape(default_for_string=False),
|
||||
)
|
||||
return env.get_template("janus.jcfg.j2").render(
|
||||
NEXTCLOUD_HPB_MEDIA_PORT_START=61000,
|
||||
NEXTCLOUD_HPB_MEDIA_PORT_END=61999,
|
||||
NEXTCLOUD_HPB_MEDIA_PUBLIC_IP="203.0.113.7",
|
||||
)
|
||||
|
||||
|
||||
def _stock_like() -> str:
|
||||
lines = ["/* a block comment the image ships", " over two lines */"]
|
||||
for line in _rendered_template().splitlines():
|
||||
if line.strip().split(" ", 1)[0] in OVERRIDES:
|
||||
lines.append("\t#" + line.strip() + "\t\t# commented out upstream")
|
||||
continue
|
||||
lines.append("\t\t# a comment line the image ships")
|
||||
lines.append(line.replace(" = ", " = ") + "\t// trailing remark")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _drift(stock: str, launcher: str = LAUNCHER) -> list[str]:
|
||||
return MODULE.janus_config_drift(
|
||||
_rendered_template(), stock, OVERRIDES, launcher, TARGET
|
||||
)
|
||||
|
||||
|
||||
class TestJanusSettings(unittest.TestCase):
|
||||
def test_comments_and_blank_lines_are_dropped(self):
|
||||
content = "# head\n\ngeneral: {\n\tdebug_level = 4\t\t# level\n}\n"
|
||||
self.assertEqual(
|
||||
MODULE.janus_settings(content, []), ["general:{", "debug_level=4", "}"]
|
||||
)
|
||||
|
||||
def test_block_comments_and_a_trailing_semicolon_are_dropped(self):
|
||||
content = "/* head\n tail */\ndebug_level = 4; /* level */\n"
|
||||
self.assertEqual(MODULE.janus_settings(content, []), ["debug_level=4"])
|
||||
|
||||
def test_a_hash_inside_a_quoted_value_is_kept(self):
|
||||
content = 'log_prefix = "[janus] #1" # prefix\n'
|
||||
self.assertEqual(
|
||||
MODULE.janus_settings(content, []), ['log_prefix="[janus] #1"']
|
||||
)
|
||||
|
||||
def test_named_overrides_are_left_out(self):
|
||||
content = 'nat: {\n\tnat_1_1_mapping = "1.2.3.4"\n\tnice_debug = false\n}\n'
|
||||
self.assertEqual(
|
||||
MODULE.janus_settings(content, ["nat_1_1_mapping"]),
|
||||
["nat:{", "nice_debug=false", "}"],
|
||||
)
|
||||
|
||||
def test_the_template_sets_every_override_once(self):
|
||||
keys = [
|
||||
setting.split("=", 1)[0]
|
||||
for setting in MODULE.janus_settings(_rendered_template(), [])
|
||||
]
|
||||
for override in OVERRIDES:
|
||||
self.assertEqual(keys.count(override), 1, override)
|
||||
|
||||
|
||||
class TestJanusConfigDrift(unittest.TestCase):
|
||||
def test_a_stock_file_that_differs_only_in_comments_and_layout_passes(self):
|
||||
self.assertEqual(_drift(_stock_like()), [])
|
||||
|
||||
def test_a_moved_plugin_folder_stops_the_deploy(self):
|
||||
stock = _stock_like().replace(
|
||||
'"/usr/local/lib/janus/plugins"', '"/usr/lib/janus/plugins"'
|
||||
)
|
||||
self.assertEqual(len(_drift(stock)), 1)
|
||||
|
||||
def test_an_upstream_value_for_a_masked_key_stops_the_deploy(self):
|
||||
stock = _stock_like().replace("\t#rtp_port_range", "\trtp_port_range", 1)
|
||||
self.assertEqual(len(_drift(stock)), 1)
|
||||
|
||||
def test_a_setting_upstream_added_stops_the_deploy(self):
|
||||
stock = _stock_like().replace("nat: {", "nat: {\n\tice_tcp = true", 1)
|
||||
self.assertEqual(len(_drift(stock)), 1)
|
||||
|
||||
def test_an_empty_read_stops_the_deploy(self):
|
||||
self.assertEqual(len(_drift("")), 1)
|
||||
|
||||
def test_janus_reading_a_generated_config_stops_the_deploy(self):
|
||||
launcher = LAUNCHER.replace(TARGET, "/conf/janus.jcfg")
|
||||
self.assertEqual(
|
||||
_drift(_stock_like(), launcher),
|
||||
[f"Janus is not started with --config={TARGET}"],
|
||||
)
|
||||
|
||||
def test_a_config_path_that_only_shares_the_prefix_stops_the_deploy(self):
|
||||
launcher = LAUNCHER.replace(f"{TARGET} ", f"{TARGET}.generated ")
|
||||
self.assertEqual(len(_drift(_stock_like(), launcher)), 1)
|
||||
|
||||
def test_a_launcher_without_janus_stops_the_deploy(self):
|
||||
self.assertEqual(len(_drift(_stock_like(), "")), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,381 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import hashlib
|
||||
import posixpath
|
||||
import re
|
||||
import unittest
|
||||
from typing import ClassVar
|
||||
|
||||
from jinja2 import Environment, StrictUndefined, select_autoescape
|
||||
|
||||
from utils.cache.files import PROJECT_ROOT, read_text
|
||||
|
||||
TEMPLATES = PROJECT_ROOT / "roles" / "web-app-nextcloud" / "templates"
|
||||
FILES = PROJECT_ROOT / "roles" / "web-app-nextcloud" / "files"
|
||||
|
||||
SIGNALING_SECRET = "base64:signaling-fixture-secret"
|
||||
INTERNAL_SECRET = "base64:internal-fixture-secret"
|
||||
RECORDING_SECRET = "recording-fixture-secret"
|
||||
|
||||
VARIABLES = {
|
||||
"NEXTCLOUD_HPB_SIGNALING_PORT": 8081,
|
||||
"NEXTCLOUD_HPB_SIGNALING_SECRET": SIGNALING_SECRET,
|
||||
"NEXTCLOUD_HPB_INTERNAL_SECRET": INTERNAL_SECRET,
|
||||
"NEXTCLOUD_HPB_BACKEND_URL": "https://next.example.org",
|
||||
"NEXTCLOUD_HPB_SIGNALING_LOCATION": "/standalone-signaling/",
|
||||
"NEXTCLOUD_HPB_SIGNALING_MAX_STREAM_BITRATE": 1048576,
|
||||
"NEXTCLOUD_HPB_SIGNALING_MAX_SCREEN_BITRATE": 2097152,
|
||||
"NEXTCLOUD_HPB_NATS_URL": "nats://talk_nats:4222",
|
||||
"NEXTCLOUD_HPB_JANUS_URL": "ws://talk:8188",
|
||||
"NEXTCLOUD_RECORDING_PORT": 1234,
|
||||
"NEXTCLOUD_RECORDING_SECRET": RECORDING_SECRET,
|
||||
"NEXTCLOUD_RECORDING_DIRECTORY": "/tmp",
|
||||
"NEXTCLOUD_HPB_MEDIA_PORT_START": 61000,
|
||||
"NEXTCLOUD_HPB_MEDIA_PORT_END": 61999,
|
||||
"NEXTCLOUD_HPB_MEDIA_PUBLIC_IP": "203.0.113.7",
|
||||
"NEXTCLOUD_HPB_JANUS_BASE_IMAGE": "alpine",
|
||||
"NEXTCLOUD_HPB_JANUS_BASE_VERSION": "3.23.3",
|
||||
"NEXTCLOUD_HPB_JANUS_REPOSITORY": "https://github.com/meetecho/janus-gateway.git",
|
||||
"NEXTCLOUD_HPB_JANUS_REF": "v1.4.0",
|
||||
"NEXTCLOUD_HPB_JANUS_CONFIG_TARGET": "/usr/local/etc/janus/janus.jcfg",
|
||||
"NEXTCLOUD_RECORDING_BASE_IMAGE": "python",
|
||||
"NEXTCLOUD_RECORDING_BASE_VERSION": "3.14.3-alpine3.23",
|
||||
"NEXTCLOUD_RECORDING_REPOSITORY": "https://github.com/nextcloud/nextcloud-talk-recording.git",
|
||||
"NEXTCLOUD_RECORDING_REF": "v0.2.1",
|
||||
"NEXTCLOUD_RECORDING_USER_ID": 122,
|
||||
"NEXTCLOUD_RECORDING_CONFIG_TARGET": "/conf/recording.conf",
|
||||
"NEXTCLOUD_RECORDING_CA_CERT": "/tmp/infinito/ca/root-ca.crt",
|
||||
}
|
||||
|
||||
AIO_SIGNALING = {
|
||||
"http": {"listen": "0.0.0.0:8081"},
|
||||
"app": {"debug": "false"},
|
||||
"clients": {"internalsecret": INTERNAL_SECRET},
|
||||
"backend": {
|
||||
"backends": "backend-1",
|
||||
"allowall": "false",
|
||||
"timeout": "10",
|
||||
"connectionsperhost": "8",
|
||||
"skipverify": "false",
|
||||
},
|
||||
"backend-1": {
|
||||
"urls": "https://next.example.org",
|
||||
"secret": SIGNALING_SECRET,
|
||||
"maxstreambitrate": "1048576",
|
||||
"maxscreenbitrate": "2097152",
|
||||
},
|
||||
"nats": {"url": "nats://127.0.0.1:4222"},
|
||||
"mcu": {
|
||||
"type": "janus",
|
||||
"url": "ws://127.0.0.1:8188",
|
||||
"maxstreambitrate": "1048576",
|
||||
"maxscreenbitrate": "2097152",
|
||||
},
|
||||
}
|
||||
|
||||
AIO_RECORDING = {
|
||||
"logs": {"level": "30"},
|
||||
"http": {"listen": "0.0.0.0:1234"},
|
||||
"backend": {
|
||||
"allowall": "false",
|
||||
"secret": RECORDING_SECRET,
|
||||
"backends": "backend-1",
|
||||
"skipverify": "false",
|
||||
"maxmessagesize": "1024",
|
||||
"videowidth": "1920",
|
||||
"videoheight": "1080",
|
||||
"directory": "/tmp",
|
||||
},
|
||||
"backend-1": {
|
||||
"url": "https://next.example.org",
|
||||
"secret": RECORDING_SECRET,
|
||||
"skipverify": "false",
|
||||
},
|
||||
"signaling": {"signalings": "signaling-1"},
|
||||
"signaling-1": {
|
||||
"url": "https://next.example.org/standalone-signaling/",
|
||||
"internalsecret": INTERNAL_SECRET,
|
||||
},
|
||||
"ffmpeg": {"extensionaudio": ".ogg", "extensionvideo": ".webm"},
|
||||
"recording": {
|
||||
"browser": "firefox",
|
||||
"driverPath": "/usr/bin/geckodriver",
|
||||
"browserPath": "/usr/bin/firefox",
|
||||
},
|
||||
}
|
||||
|
||||
STOCK_JANUS = [
|
||||
"general:{",
|
||||
'configs_folder="/usr/local/etc/janus"',
|
||||
'plugins_folder="/usr/local/lib/janus/plugins"',
|
||||
'transports_folder="/usr/local/lib/janus/transports"',
|
||||
'events_folder="/usr/local/lib/janus/events"',
|
||||
'loggers_folder="/usr/local/lib/janus/loggers"',
|
||||
"debug_level=4",
|
||||
'admin_secret="janusoverlord"',
|
||||
"protected_folders=[",
|
||||
*[
|
||||
f'"{folder}",'
|
||||
for folder in (
|
||||
"/bin",
|
||||
"/boot",
|
||||
"/dev",
|
||||
"/etc",
|
||||
"/initrd",
|
||||
"/lib",
|
||||
"/lib32",
|
||||
"/lib64",
|
||||
"/proc",
|
||||
"/sbin",
|
||||
"/sys",
|
||||
"/usr",
|
||||
"/var",
|
||||
"/opt/janus/bin",
|
||||
"/opt/janus/etc",
|
||||
"/opt/janus/include",
|
||||
"/opt/janus/lib",
|
||||
"/opt/janus/lib32",
|
||||
"/opt/janus/lib64",
|
||||
)
|
||||
],
|
||||
'"/opt/janus/sbin"',
|
||||
"]",
|
||||
"}",
|
||||
"certificates:{",
|
||||
"}",
|
||||
"media:{",
|
||||
"}",
|
||||
"nat:{",
|
||||
'ice_ignore_list="vmnet"',
|
||||
"}",
|
||||
"plugins:{",
|
||||
"}",
|
||||
"transports:{",
|
||||
"}",
|
||||
"loggers:{",
|
||||
"}",
|
||||
"events:{",
|
||||
"}",
|
||||
]
|
||||
|
||||
|
||||
def _render(source: str, **overrides) -> str:
|
||||
env = Environment(
|
||||
trim_blocks=True,
|
||||
keep_trailing_newline=True,
|
||||
undefined=StrictUndefined,
|
||||
autoescape=select_autoescape(default_for_string=False),
|
||||
)
|
||||
env.filters["hash"] = lambda value, algorithm: hashlib.new(
|
||||
algorithm, str(value).encode()
|
||||
).hexdigest()
|
||||
env.filters["bool"] = bool
|
||||
env.filters["dirname"] = posixpath.dirname
|
||||
return env.from_string(source).render({**VARIABLES, **overrides})
|
||||
|
||||
|
||||
def _template(name: str) -> str:
|
||||
return read_text(str(TEMPLATES / name))
|
||||
|
||||
|
||||
def _build_args(dockerfile: str) -> str:
|
||||
compose = _template("compose.yml.j2")
|
||||
args: dict[str, str] = {}
|
||||
for arg in re.findall(r"^ARG (\w+)$", dockerfile, re.MULTILINE):
|
||||
passed = re.search(rf'^\s+{arg}: "(.*)"$', compose, re.MULTILINE)
|
||||
if passed is None:
|
||||
raise AssertionError(f"compose.yml.j2 passes no build arg {arg}")
|
||||
args[arg] = _render(passed.group(1))
|
||||
return re.sub(r"\$\{(\w+)\}", lambda match: args[match.group(1)], dockerfile)
|
||||
|
||||
|
||||
def _build(name: str) -> str:
|
||||
return _build_args(read_text(str(FILES / "Dockerfiles" / name / "Dockerfile")))
|
||||
|
||||
|
||||
def _ini(text: str) -> dict[str, dict[str, str]]:
|
||||
parser = configparser.ConfigParser(interpolation=None, strict=True)
|
||||
parser.optionxform = str
|
||||
parser.read_string(text)
|
||||
return {section: dict(parser[section]) for section in parser.sections()}
|
||||
|
||||
|
||||
def _settings(text: str) -> list[str]:
|
||||
return [joined for line in text.splitlines() if (joined := "".join(line.split()))]
|
||||
|
||||
|
||||
class TestSignalingConfig(unittest.TestCase):
|
||||
def test_matches_what_the_aio_start_script_wrote_apart_from_the_split(self):
|
||||
rendered = _ini(_render(_template("signaling.conf.j2")))
|
||||
sessions = rendered.pop("sessions")
|
||||
expected = {
|
||||
**AIO_SIGNALING,
|
||||
"nats": {"url": "nats://talk_nats:4222"},
|
||||
"mcu": {**AIO_SIGNALING["mcu"], "url": "ws://talk:8188"},
|
||||
}
|
||||
self.assertEqual(rendered, expected)
|
||||
self.assertEqual(sorted(sessions), ["blockkey", "hashkey"])
|
||||
|
||||
def test_session_keys_have_the_shape_aio_generated_and_stay_stable(self):
|
||||
first = _ini(_render(_template("signaling.conf.j2")))["sessions"]
|
||||
second = _ini(_render(_template("signaling.conf.j2")))["sessions"]
|
||||
self.assertEqual(first, second)
|
||||
for key in ("hashkey", "blockkey"):
|
||||
self.assertRegex(first[key], r"^[0-9a-f]{32}$")
|
||||
self.assertNotEqual(first["hashkey"], first["blockkey"])
|
||||
self.assertNotIn(first["hashkey"], SIGNALING_SECRET)
|
||||
|
||||
def test_negative_control_another_signaling_secret_changes_the_keys(self):
|
||||
first = _ini(_render(_template("signaling.conf.j2")))["sessions"]
|
||||
other = _ini(
|
||||
_render(
|
||||
_template("signaling.conf.j2"),
|
||||
NEXTCLOUD_HPB_SIGNALING_SECRET="base64:another-secret",
|
||||
)
|
||||
)["sessions"]
|
||||
self.assertNotEqual(first["hashkey"], other["hashkey"])
|
||||
self.assertNotEqual(first["blockkey"], other["blockkey"])
|
||||
|
||||
def test_negative_control_a_changed_setting_breaks_parity(self):
|
||||
drifted = _template("signaling.conf.j2").replace(
|
||||
"allowall = false", "allowall = true"
|
||||
)
|
||||
self.assertNotEqual(drifted, _template("signaling.conf.j2"))
|
||||
rendered = _ini(_render(drifted))
|
||||
rendered.pop("sessions")
|
||||
self.assertNotEqual(rendered["backend"], AIO_SIGNALING["backend"])
|
||||
|
||||
|
||||
class TestRecordingConfig(unittest.TestCase):
|
||||
def test_matches_what_the_aio_start_script_wrote(self):
|
||||
self.assertEqual(_ini(_render(_template("recording.conf.j2"))), AIO_RECORDING)
|
||||
|
||||
def test_negative_control_a_dropped_setting_breaks_parity(self):
|
||||
drifted = _template("recording.conf.j2").replace("maxmessagesize = 1024\n", "")
|
||||
self.assertNotEqual(drifted, _template("recording.conf.j2"))
|
||||
self.assertNotEqual(_ini(_render(drifted)), AIO_RECORDING)
|
||||
|
||||
|
||||
class TestJanusConfig(unittest.TestCase):
|
||||
def test_without_direct_media_it_equals_the_stock_janus_config(self):
|
||||
rendered = _render(_template("janus.jcfg.j2"), NEXTCLOUD_HPB_MEDIA_DIRECT=False)
|
||||
self.assertEqual(_settings(rendered), STOCK_JANUS)
|
||||
|
||||
def test_direct_media_adds_exactly_the_three_overrides(self):
|
||||
rendered = _settings(
|
||||
_render(_template("janus.jcfg.j2"), NEXTCLOUD_HPB_MEDIA_DIRECT=True)
|
||||
)
|
||||
expected = list(STOCK_JANUS)
|
||||
expected.insert(expected.index("media:{") + 1, 'rtp_port_range="61000-61999"')
|
||||
expected.insert(expected.index("nat:{") + 1, 'nat_1_1_mapping="203.0.113.7"')
|
||||
expected.insert(expected.index("nat:{") + 2, "keep_private_host=true")
|
||||
self.assertEqual(rendered, expected)
|
||||
|
||||
def test_negative_control_an_unguarded_override_breaks_the_stock_config(self):
|
||||
drifted = _template("janus.jcfg.j2").replace(
|
||||
"{% if NEXTCLOUD_HPB_MEDIA_DIRECT | bool %}\n\trtp_port_range",
|
||||
"{% if true %}\n\trtp_port_range",
|
||||
)
|
||||
self.assertNotEqual(drifted, _template("janus.jcfg.j2"))
|
||||
rendered = _render(drifted, NEXTCLOUD_HPB_MEDIA_DIRECT=False)
|
||||
self.assertNotEqual(_settings(rendered), STOCK_JANUS)
|
||||
|
||||
|
||||
class TestTalkDockerfiles(unittest.TestCase):
|
||||
AIO_JANUS_BUILD_PACKAGES: ClassVar[list[str]] = [
|
||||
"ca-certificates",
|
||||
"git",
|
||||
"autoconf",
|
||||
"automake",
|
||||
"build-base",
|
||||
"pkgconfig",
|
||||
"libtool",
|
||||
"util-linux",
|
||||
"glib-dev",
|
||||
"zlib-dev",
|
||||
"openssl-dev",
|
||||
"jansson-dev",
|
||||
"libnice-dev",
|
||||
"libconfig-dev",
|
||||
"libsrtp-dev",
|
||||
"libusrsctp-dev",
|
||||
"gengetopt-dev",
|
||||
"libwebsockets-dev",
|
||||
]
|
||||
|
||||
def test_janus_builds_the_pinned_tag_on_the_pinned_base(self):
|
||||
rendered = _build("Janus")
|
||||
self.assertEqual(
|
||||
re.findall(r"^FROM (\S+)", rendered, re.MULTILINE),
|
||||
["alpine:3.23.3", "alpine:3.23.3"],
|
||||
)
|
||||
self.assertIn(
|
||||
'git clone --recursive "https://github.com/meetecho/janus-gateway.git" '
|
||||
'--depth=1 --single-branch --branch "v1.4.0" /src',
|
||||
rendered,
|
||||
)
|
||||
self.assertIn(
|
||||
"/src/configure --disable-rabbitmq --disable-mqtt --disable-boringssl",
|
||||
rendered,
|
||||
)
|
||||
build_stage = rendered.split("\nFROM ")[1]
|
||||
for package in self.AIO_JANUS_BUILD_PACKAGES:
|
||||
with self.subTest(package=package):
|
||||
self.assertRegex(build_stage, rf"\n\s+{re.escape(package)}( \\|;)")
|
||||
self.assertIn(
|
||||
'ENTRYPOINT ["janus", "--disable-colors", "--log-stdout", '
|
||||
'"--full-trickle", "--debug-level", "3"]',
|
||||
rendered,
|
||||
)
|
||||
|
||||
def test_recording_installs_the_pinned_release_on_the_pinned_base(self):
|
||||
rendered = _build("Recording")
|
||||
self.assertEqual(
|
||||
re.findall(r"^FROM (\S+)", rendered, re.MULTILINE),
|
||||
["python:3.14.3-alpine3.23"],
|
||||
)
|
||||
self.assertIn(
|
||||
'git clone --recursive "https://github.com/nextcloud/nextcloud-talk-recording.git" '
|
||||
'--depth=1 --single-branch --branch "v0.2.1" /src',
|
||||
rendered,
|
||||
)
|
||||
self.assertIn('useradd -d "/tmp" --system recording -u "122"', rendered)
|
||||
self.assertIn('mkdir -p "/conf"', rendered)
|
||||
self.assertIn(
|
||||
"""printf '{"policies":{"Certificates":{"Install":["%s"]}}}\\n' """
|
||||
'"/tmp/infinito/ca/root-ca.crt" > /etc/firefox/policies/policies.json',
|
||||
rendered,
|
||||
)
|
||||
self.assertIn("USER 122", rendered)
|
||||
self.assertIn(
|
||||
'ENTRYPOINT ["python", "-m", "nextcloud.talk.recording"]', rendered
|
||||
)
|
||||
|
||||
def test_the_compose_command_points_each_binary_at_its_rendered_config(self):
|
||||
compose = _template("compose.yml.j2")
|
||||
self.assertIn(
|
||||
'command: ["--config={{ NEXTCLOUD_HPB_JANUS_CONFIG_TARGET }}"]', compose
|
||||
)
|
||||
self.assertIn(
|
||||
'command: ["--config", "{{ NEXTCLOUD_RECORDING_CONFIG_TARGET }}"]', compose
|
||||
)
|
||||
|
||||
def test_no_image_ships_a_start_script_that_writes_its_config(self):
|
||||
for name in ("Janus", "Recording"):
|
||||
with self.subTest(dockerfile=name):
|
||||
rendered = _build(name)
|
||||
self.assertNotRegex(rendered, r"ENTRYPOINT \[\"[^\"]*\.sh\"")
|
||||
self.assertNotIn(
|
||||
"COPY",
|
||||
rendered.split("\nFROM ")[-1].replace("COPY --from=build", ""),
|
||||
)
|
||||
self.assertNotIn("SECRET", rendered)
|
||||
|
||||
def test_negative_control_an_arg_compose_does_not_pass_fails_the_build(self):
|
||||
with self.assertRaisesRegex(AssertionError, "JANUS_UNPASSED"):
|
||||
_build_args("ARG JANUS_UNPASSED\nFROM ${JANUS_UNPASSED}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -28,48 +28,100 @@ ROLE = "roles/web-app-nextcloud"
|
||||
TEMPLATE = f"{ROLE}/{ROLE_FILE_TEMPL_COMPOSE}"
|
||||
PUBLIC_IP = "203.0.113.7"
|
||||
SERVICES = load_yaml_any(str(PROJECT_ROOT / ROLE / ROLE_FILE_META_SERVICES))
|
||||
TALK_SERVICES = ("talk", "talk_signaling", "talk_nats", "talk_recording")
|
||||
|
||||
LEGACY_BRIDGE_ONBOARD = """ talk:
|
||||
JANUS_HEAD = """ talk:
|
||||
<container-base>
|
||||
<lookup container_healthcheck ('talk',)>
|
||||
<lookup container_image ('web-app-nextcloud', 'talk')>
|
||||
build:
|
||||
context: <NEXTCLOUD_HPB_JANUS_BUILD_DIRECTORY>
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
JANUS_BASE_IMAGE: "<NEXTCLOUD_HPB_JANUS_BASE_IMAGE>"
|
||||
JANUS_BASE_VERSION: "<NEXTCLOUD_HPB_JANUS_BASE_VERSION>"
|
||||
JANUS_REPOSITORY: "<NEXTCLOUD_HPB_JANUS_REPOSITORY>"
|
||||
JANUS_REF: "<NEXTCLOUD_HPB_JANUS_REF>"
|
||||
<lookup compose_only ('pull_policy', 'never')>
|
||||
<lookup container_image ('web-app-nextcloud', 'talk') {'version': <NEXTCLOUD_HPB_JANUS_TAG>}>
|
||||
<lookup compose_only ('container_name', <NEXTCLOUD_HPB_CONTAINER>)>
|
||||
command: ["--config=<NEXTCLOUD_HPB_JANUS_CONFIG_TARGET>"]
|
||||
init: true
|
||||
ports:
|
||||
- "203.0.113.7:3480:3480/tcp"
|
||||
- "203.0.113.7:3480:3480/udp"
|
||||
- "203.0.113.7:50000-59999:50000-59999/udp"
|
||||
- "203.0.113.7:50000-59999:50000-59999/tcp"
|
||||
<lookup container_healthcheck ('talk',)>
|
||||
\n configs:
|
||||
- source: janus_conf
|
||||
target: /usr/local/etc/janus/janus.jcfg
|
||||
mode: 292"""
|
||||
|
||||
JANUS_HOST = JANUS_HEAD + "\n network_mode: host"
|
||||
|
||||
JANUS_BRIDGE = (
|
||||
JANUS_HEAD
|
||||
+ """
|
||||
expose:
|
||||
- "8081"
|
||||
<lookup container_networks ()>"""
|
||||
|
||||
LEGACY_BRIDGE = """ talk:
|
||||
<container-base>
|
||||
<lookup container_healthcheck ('talk',)>
|
||||
<lookup container_image ('web-app-nextcloud', 'talk')>
|
||||
<lookup compose_only ('container_name', <NEXTCLOUD_HPB_CONTAINER>)>
|
||||
init: true
|
||||
expose:
|
||||
- "8081"
|
||||
<lookup container_networks ()>"""
|
||||
|
||||
LEGACY_HOST = """ talk:
|
||||
<container-base>
|
||||
<lookup container_healthcheck ('talk',)>
|
||||
<lookup container_image ('web-app-nextcloud', 'talk')>
|
||||
<lookup compose_only ('container_name', <NEXTCLOUD_HPB_CONTAINER>)>
|
||||
init: true
|
||||
network_mode: host"""
|
||||
|
||||
DIRECT_MOUNT = (
|
||||
" \n"
|
||||
" configs:\n"
|
||||
" - source: janus_conf\n"
|
||||
" target: /usr/local/etc/janus/janus.jcfg"
|
||||
- "<NEXTCLOUD_HPB_JANUS_PORT>"
|
||||
<lookup container_networks () {'own_network_only': True, 'provider_self_alias': False}>"""
|
||||
)
|
||||
|
||||
DIRECT_PORT = ' - "203.0.113.7:61000-61999:61000-61999/udp"'
|
||||
DIRECT_PORT = (
|
||||
' ports:\n - "203.0.113.7:61000-61999:61000-61999/udp"\n expose:'
|
||||
)
|
||||
|
||||
SIGNALING = """ talk_signaling:
|
||||
<container-base>
|
||||
<lookup container_image ('web-app-nextcloud', 'talk_signaling')>
|
||||
<lookup compose_only ('container_name', <NEXTCLOUD_HPB_SIGNALING_CONTAINER>)>
|
||||
user: "<NEXTCLOUD_HPB_SIGNALING_USER_ID>:<NEXTCLOUD_HPB_SIGNALING_USER_ID>"
|
||||
command: ["<NEXTCLOUD_HPB_SIGNALING_BINARY>", "-config", "<NEXTCLOUD_HPB_SIGNALING_CONFIG_TARGET>"]
|
||||
<lookup container_healthcheck ('talk_signaling',)>
|
||||
\n secrets:
|
||||
- source: talk_signaling_conf
|
||||
target: /config/server.conf
|
||||
mode: 292
|
||||
expose:
|
||||
- "<NEXTCLOUD_HPB_SIGNALING_PORT>"
|
||||
<lookup container_extra_hosts () {'host_alias': {host_alias}}>
|
||||
<lookup container_networks () {'provider_self_alias': False}>"""
|
||||
|
||||
NATS = """ talk_nats:
|
||||
<container-base>
|
||||
<lookup container_image ('web-app-nextcloud', 'talk_nats')>
|
||||
<lookup compose_only ('container_name', <NEXTCLOUD_HPB_NATS_CONTAINER>)>
|
||||
user: "<NEXTCLOUD_HPB_NATS_USER_ID>:<NEXTCLOUD_HPB_NATS_USER_ID>"
|
||||
command: ["nats-server", "--addr", "0.0.0.0", "--port", "<NEXTCLOUD_HPB_NATS_PORT>", "--http_port", "<NEXTCLOUD_HPB_NATS_MONITOR_PORT>"]
|
||||
<lookup container_healthcheck ('talk_nats',)>
|
||||
expose:
|
||||
- "<NEXTCLOUD_HPB_NATS_PORT>"
|
||||
<lookup container_networks () {'own_network_only': True, 'provider_self_alias': False}>"""
|
||||
|
||||
RECORDING = """ talk_recording:
|
||||
<container-base>
|
||||
build:
|
||||
context: <NEXTCLOUD_RECORDING_BUILD_DIRECTORY>
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
RECORDING_BASE_IMAGE: "<NEXTCLOUD_RECORDING_BASE_IMAGE>"
|
||||
RECORDING_BASE_VERSION: "<NEXTCLOUD_RECORDING_BASE_VERSION>"
|
||||
RECORDING_REPOSITORY: "<NEXTCLOUD_RECORDING_REPOSITORY>"
|
||||
RECORDING_REF: "<NEXTCLOUD_RECORDING_REF>"
|
||||
RECORDING_USER_ID: "<NEXTCLOUD_RECORDING_USER_ID>"
|
||||
RECORDING_DIRECTORY: "<NEXTCLOUD_RECORDING_DIRECTORY>"
|
||||
RECORDING_CONFIG_DIRECTORY: "<dirname <NEXTCLOUD_RECORDING_CONFIG_TARGET>>"
|
||||
RECORDING_CA_CERT: "<NEXTCLOUD_RECORDING_CA_CERT>"
|
||||
<lookup compose_only ('pull_policy', 'never')>
|
||||
<lookup container_image ('web-app-nextcloud', 'talk_recording') {'version': <NEXTCLOUD_RECORDING_TAG>}>
|
||||
<lookup compose_only ('container_name', <NEXTCLOUD_RECORDING_CONTAINER>)>
|
||||
command: ["--config", "<NEXTCLOUD_RECORDING_CONFIG_TARGET>"]
|
||||
environment:
|
||||
TZ: "<HOST_TIMEZONE>"
|
||||
<lookup container_healthcheck ('talk_recording',)>
|
||||
expose:
|
||||
- "<NEXTCLOUD_RECORDING_PORT>"
|
||||
<lookup container_networks () {'provider_self_alias': False}>
|
||||
\n volumes:
|
||||
- talk_recording_tmp:/tmp
|
||||
secrets:
|
||||
- source: talk_recording_conf
|
||||
target: /conf/recording.conf
|
||||
mode: 292"""
|
||||
|
||||
|
||||
class _Marker(ChainableUndefined):
|
||||
@@ -87,9 +139,7 @@ def _bool(value):
|
||||
return str(value).strip().lower() in {"y", "yes", "true", "on", "1"}
|
||||
|
||||
|
||||
def _render(
|
||||
template_text: str, *, onboard: bool, direct: bool, mode: str, network: str
|
||||
) -> str:
|
||||
def _render(template_text: str, *, direct: bool, mode: str, network: str) -> str:
|
||||
applications = {"web-app-nextcloud": {"services": SERVICES}}
|
||||
env = Environment(
|
||||
loader=ChoiceLoader(
|
||||
@@ -113,6 +163,7 @@ def _render(
|
||||
)
|
||||
env.filters["bool"] = _bool
|
||||
env.filters["path_join"] = lambda parts: "/".join(str(part) for part in parts)
|
||||
env.filters["dirname"] = lambda value: f"<dirname {value}>"
|
||||
variables = {
|
||||
"application_id": "web-app-nextcloud",
|
||||
"NEXTCLOUD_PROXY_SERVICE": "proxy",
|
||||
@@ -122,11 +173,12 @@ def _render(
|
||||
"DEPLOYMENT_MODE": mode,
|
||||
"NEXTCLOUD_HPB_SIGNALING_ENABLED": True,
|
||||
"NEXTCLOUD_HPB_SERVICE": "talk",
|
||||
"NEXTCLOUD_HPB_SIGNALING_PORT": 8081,
|
||||
"NEXTCLOUD_HPB_SIGNALING_SERVICE": "talk_signaling",
|
||||
"NEXTCLOUD_HPB_NATS_SERVICE": "talk_nats",
|
||||
"NEXTCLOUD_RECORDING_SERVICE": "talk_recording",
|
||||
"NEXTCLOUD_RECORDING_ENABLED": True,
|
||||
"NEXTCLOUD_HPB_NETWORK_MODE": network,
|
||||
"NEXTCLOUD_HPB_TURN_ONBOARD_ENABLED": onboard,
|
||||
"NEXTCLOUD_HPB_TURN_ONBOARD_PUBLISH_PORTS": True,
|
||||
"NEXTCLOUD_HPB_TURN_ONBOARD_BIND_HOST": PUBLIC_IP,
|
||||
"NEXTCLOUD_HPB_JANUS_HOST_NETWORK": network == "host",
|
||||
"NEXTCLOUD_HPB_MEDIA_DIRECT": direct,
|
||||
"NEXTCLOUD_HPB_MEDIA_PUBLIC_IP": PUBLIC_IP,
|
||||
}
|
||||
@@ -144,7 +196,10 @@ def _render(
|
||||
|
||||
def lookup(name, *args, **kwargs):
|
||||
if name == "config":
|
||||
return {"services.talk.direct_media": direct}[args[1]]
|
||||
return {
|
||||
"services.talk.direct_media": direct,
|
||||
"services.talk.enabled": True,
|
||||
}[args[1]]
|
||||
if name == "container_ports":
|
||||
with mock.patch.object(container_ports_lookup, "lookup_loader", _Loader()):
|
||||
return container_ports_lookup.LookupModule().run(
|
||||
@@ -155,15 +210,16 @@ def _render(
|
||||
applications, "web-app-nextcloud", args[0], render_jinja=render_jinja
|
||||
)
|
||||
return "\n" + block if block else block
|
||||
return f"<lookup {name} {args!r}>"
|
||||
suffix = f" {dict(sorted(kwargs.items()))!r}" if kwargs else ""
|
||||
return f"<lookup {name} {args!r}{suffix}>"
|
||||
|
||||
env.globals["lookup"] = lookup
|
||||
return env.get_template("compose.yml.j2").render(variables)
|
||||
|
||||
|
||||
def _talk_block(rendered: str) -> str:
|
||||
def _block(rendered: str, service: str) -> str:
|
||||
lines = rendered.splitlines()
|
||||
start = lines.index(" talk:")
|
||||
start = lines.index(f" {service}:")
|
||||
end = next(
|
||||
index
|
||||
for index in range(start + 1, len(lines))
|
||||
@@ -172,72 +228,80 @@ def _talk_block(rendered: str) -> str:
|
||||
return "\n".join(lines[start:end]).rstrip()
|
||||
|
||||
|
||||
def _cells(direct: bool):
|
||||
for onboard, mode, network in itertools.product(
|
||||
def _cells():
|
||||
for direct, mode, network in itertools.product(
|
||||
(True, False), ("compose", "swarm"), ("bridge", "host")
|
||||
):
|
||||
yield {"onboard": onboard, "direct": direct, "mode": mode, "network": network}
|
||||
yield {"direct": direct, "mode": mode, "network": network}
|
||||
|
||||
|
||||
def _legacy(cell) -> str:
|
||||
if cell["network"] == "host":
|
||||
return LEGACY_HOST
|
||||
return LEGACY_BRIDGE_ONBOARD if cell["onboard"] else LEGACY_BRIDGE
|
||||
def _expected_janus(cell) -> str:
|
||||
if cell["mode"] == "compose" and cell["network"] == "host":
|
||||
return JANUS_HOST
|
||||
if cell["direct"]:
|
||||
return JANUS_BRIDGE.replace(" expose:", DIRECT_PORT)
|
||||
return JANUS_BRIDGE
|
||||
|
||||
|
||||
class TestTalkComposeBlock(unittest.TestCase):
|
||||
def test_every_cell_without_direct_media_renders_the_legacy_block(self):
|
||||
template = read_text(str(PROJECT_ROOT / TEMPLATE))
|
||||
for cell in _cells(direct=False):
|
||||
def _template() -> str:
|
||||
return read_text(str(PROJECT_ROOT / TEMPLATE))
|
||||
|
||||
|
||||
class TestTalkComposeBlocks(unittest.TestCase):
|
||||
def test_janus_block_follows_network_mode_and_direct_media(self):
|
||||
for cell in _cells():
|
||||
with self.subTest(**cell):
|
||||
self.assertEqual(_talk_block(_render(template, **cell)), _legacy(cell))
|
||||
|
||||
def test_host_networking_ignores_direct_media(self):
|
||||
template = read_text(str(PROJECT_ROOT / TEMPLATE))
|
||||
for cell in _cells(direct=True):
|
||||
if cell["network"] == "host":
|
||||
with self.subTest(**cell):
|
||||
self.assertEqual(
|
||||
_talk_block(_render(template, **cell)), LEGACY_HOST
|
||||
)
|
||||
|
||||
def test_direct_media_adds_only_the_janus_mount_and_the_udp_range(self):
|
||||
template = read_text(str(PROJECT_ROOT / TEMPLATE))
|
||||
for cell in _cells(direct=True):
|
||||
if cell["network"] != "bridge":
|
||||
continue
|
||||
with self.subTest(**cell):
|
||||
block = _talk_block(_render(template, **cell))
|
||||
legacy = _legacy(cell)
|
||||
expected = legacy.replace(
|
||||
" init: true", " init: true\n" + DIRECT_MOUNT
|
||||
self.assertEqual(
|
||||
_block(_render(_template(), **cell), "talk"), _expected_janus(cell)
|
||||
)
|
||||
if cell["onboard"]:
|
||||
expected = expected.replace(
|
||||
" expose:", DIRECT_PORT + "\n expose:"
|
||||
)
|
||||
else:
|
||||
expected = expected.replace(
|
||||
" expose:", " ports:\n" + DIRECT_PORT + "\n expose:"
|
||||
)
|
||||
self.assertEqual(block, expected)
|
||||
|
||||
def test_negative_control_an_unguarded_mount_breaks_the_legacy_block(self):
|
||||
template = read_text(str(PROJECT_ROOT / TEMPLATE)).replace(
|
||||
" {% if NEXTCLOUD_HPB_MEDIA_DIRECT | bool %}\n"
|
||||
" {{ lookup('container_volumes', service_name) | indent(4) }}\n",
|
||||
" {{ lookup('container_volumes', service_name) | indent(4) }}\n"
|
||||
" {% if NEXTCLOUD_HPB_MEDIA_DIRECT | bool %}\n",
|
||||
def test_signaling_block_is_the_same_in_every_cell(self):
|
||||
for cell in _cells():
|
||||
with self.subTest(**cell):
|
||||
expected = SIGNALING.replace(
|
||||
"{host_alias}", str(cell["network"] == "host")
|
||||
)
|
||||
self.assertEqual(
|
||||
_block(_render(_template(), **cell), "talk_signaling"), expected
|
||||
)
|
||||
|
||||
def test_nats_and_recording_blocks_are_the_same_in_every_cell(self):
|
||||
for cell in _cells():
|
||||
with self.subTest(**cell):
|
||||
rendered = _render(_template(), **cell)
|
||||
self.assertEqual(_block(rendered, "talk_nats"), NATS)
|
||||
self.assertEqual(_block(rendered, "talk_recording"), RECORDING)
|
||||
|
||||
def test_no_talk_service_passes_a_secret_through_compose(self):
|
||||
for cell in _cells():
|
||||
rendered = _render(_template(), **cell)
|
||||
for service in TALK_SERVICES:
|
||||
with self.subTest(service=service, **cell):
|
||||
self.assertNotIn("SECRET", _block(rendered, service))
|
||||
|
||||
def test_negative_control_an_ungated_host_network_reaches_swarm(self):
|
||||
template = _template().replace(
|
||||
"{% if DEPLOYMENT_MODE == 'compose' and NEXTCLOUD_HPB_JANUS_HOST_NETWORK | bool %}",
|
||||
"{% if NEXTCLOUD_HPB_JANUS_HOST_NETWORK | bool %}",
|
||||
)
|
||||
cell = {
|
||||
"onboard": False,
|
||||
"direct": False,
|
||||
"mode": "compose",
|
||||
"network": "bridge",
|
||||
}
|
||||
rendered = _render(template, **cell)
|
||||
self.assertNotIn("janus_conf", rendered)
|
||||
self.assertNotEqual(_talk_block(rendered), _legacy(cell))
|
||||
cell = {"direct": False, "mode": "swarm", "network": "host"}
|
||||
self.assertNotEqual(template, _template())
|
||||
block = _block(_render(template, **cell), "talk")
|
||||
self.assertIn("network_mode: host", block)
|
||||
self.assertNotEqual(block, _expected_janus(cell))
|
||||
|
||||
def test_negative_control_a_secret_in_the_environment_is_caught(self):
|
||||
template = _template().replace(
|
||||
' TZ: "{{ HOST_TIMEZONE }}"',
|
||||
' TZ: "{{ HOST_TIMEZONE }}"\n'
|
||||
' RECORDING_SECRET: "{{ NEXTCLOUD_RECORDING_SECRET }}"',
|
||||
)
|
||||
self.assertNotEqual(template, _template())
|
||||
block = _block(
|
||||
_render(template, direct=False, mode="compose", network="bridge"),
|
||||
"talk_recording",
|
||||
)
|
||||
self.assertIn("SECRET", block)
|
||||
|
||||
|
||||
ROLE_VARS = load_yaml_any(str(PROJECT_ROOT / ROLE / ROLE_FILE_VARS_MAIN))
|
||||
@@ -294,9 +358,9 @@ class TestDirectMediaFlag(unittest.TestCase):
|
||||
def test_opting_in_with_standalone_turn_and_an_owned_address_turns_it_on(self):
|
||||
self.assertIs(_direct_media(list(STANDALONE_TURN), applications=OPTED_IN), True)
|
||||
|
||||
def test_host_networking_keeps_it_off(self):
|
||||
def test_host_networking_keeps_it_on(self):
|
||||
host = _talk(direct_media=True, network_mode="host")
|
||||
self.assertIs(_direct_media(list(STANDALONE_TURN), applications=host), False)
|
||||
self.assertIs(_direct_media(list(STANDALONE_TURN), applications=host), True)
|
||||
|
||||
def test_an_address_the_host_does_not_own_keeps_it_off(self):
|
||||
self.assertIs(
|
||||
@@ -318,7 +382,7 @@ class TestDirectMediaFlag(unittest.TestCase):
|
||||
False,
|
||||
)
|
||||
|
||||
def test_onboard_turn_keeps_it_off(self):
|
||||
def test_talk_without_coturn_keeps_it_off(self):
|
||||
forced = _talk(enabled=True, direct_media=True)
|
||||
self.assertIs(_direct_media(["web-app-nextcloud"], applications=forced), False)
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
import unittest.mock as mock
|
||||
from typing import ClassVar
|
||||
|
||||
from utils.cache.files import PROJECT_ROOT, read_text
|
||||
from utils.cache.yaml import load_yaml_any
|
||||
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
||||
from utils.update import docker, repository
|
||||
|
||||
ROLE = "web-app-nextcloud"
|
||||
SERVICES_FILE = PROJECT_ROOT / "roles" / ROLE / ROLE_FILE_META_SERVICES
|
||||
JANUS = "https://github.com/meetecho/janus-gateway.git"
|
||||
RECORDING = "https://github.com/nextcloud/nextcloud-talk-recording.git"
|
||||
|
||||
|
||||
def _pinned(service: str, key: str) -> str:
|
||||
return str(load_yaml_any(str(SERVICES_FILE))[service][key])
|
||||
|
||||
|
||||
class TestTalkPinsAreWatched(unittest.TestCase):
|
||||
IMAGES: ClassVar[dict[str, str]] = {
|
||||
"talk_signaling": "strukturag/nextcloud-spreed-signaling",
|
||||
"talk_nats": "nats",
|
||||
"talk": "alpine",
|
||||
"talk_recording": "python",
|
||||
}
|
||||
REFS: ClassVar[dict[str, str]] = {"talk": JANUS, "talk_recording": RECORDING}
|
||||
|
||||
def test_the_docker_updater_watches_every_talk_image(self):
|
||||
entries = {
|
||||
(entry.service, entry.image): entry.version
|
||||
for entry in docker.collect_entries(PROJECT_ROOT)
|
||||
if entry.role == ROLE
|
||||
}
|
||||
for service, image in self.IMAGES.items():
|
||||
with self.subTest(service=service):
|
||||
self.assertEqual(
|
||||
entries.get((service, image)), _pinned(service, "version")
|
||||
)
|
||||
|
||||
def test_the_repository_updater_watches_both_built_sources(self):
|
||||
entries = {
|
||||
entry.entity_path: (entry.repository, entry.ref)
|
||||
for entry in repository.collect_entries(PROJECT_ROOT)
|
||||
if entry.role == ROLE
|
||||
}
|
||||
for service, url in self.REFS.items():
|
||||
with self.subTest(service=service):
|
||||
self.assertEqual(
|
||||
entries.get((service,)), (url, _pinned(service, "ref"))
|
||||
)
|
||||
|
||||
def test_no_talk_pin_opts_out_of_the_updaters(self):
|
||||
lines = read_text(str(SERVICES_FILE)).splitlines()
|
||||
owner = None
|
||||
for number, line in enumerate(lines):
|
||||
if match := re.match(r"^([a-z_]+):", line):
|
||||
owner = match.group(1)
|
||||
if owner in self.IMAGES and re.match(r"^\s+(version|ref):", line):
|
||||
with self.subTest(service=owner, line=number + 1):
|
||||
self.assertNotIn("nocheck", lines[number - 1])
|
||||
self.assertNotIn("nocheck", line)
|
||||
|
||||
def test_a_newer_upstream_release_is_reported_for_every_pin(self):
|
||||
newer = {
|
||||
"strukturag/nextcloud-spreed-signaling": ["2.1.1", "9.0.0", "9.0.0-proxy"],
|
||||
"nats": ["2.12.6-alpine", "9.0.0-alpine", "9.1.0"],
|
||||
"alpine": ["3.23.3", "9.0.0"],
|
||||
"python": ["3.14.3-alpine3.23", "3.14.9-alpine3.23", "3.15.0-slim"],
|
||||
}
|
||||
tags = {JANUS: ["v1.4.0", "v9.0.0"], RECORDING: ["v0.2.1", "v9.0.0"]}
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"INFINITO_WORKER_FETCH": "2"}),
|
||||
mock.patch.object(
|
||||
docker, "fetch_dockerhub_tags", lambda image: newer.get(image, [])
|
||||
),
|
||||
mock.patch.object(docker, "fetch_ghcr_tags", lambda image: []),
|
||||
mock.patch.object(docker, "fetch_mcr_tags", lambda image: []),
|
||||
mock.patch.object(
|
||||
docker, "fetch_registry_tags", lambda image, last=None: []
|
||||
),
|
||||
mock.patch.object(
|
||||
repository, "git_ls_remote_tags", lambda url: tags.get(url, [])
|
||||
),
|
||||
):
|
||||
images = {
|
||||
(update.entry.service, update.latest)
|
||||
for update in docker.find_outdated_updates(PROJECT_ROOT)
|
||||
if update.entry.role == ROLE
|
||||
}
|
||||
refs = {
|
||||
(update.entry.entity_path, update.latest)
|
||||
for update in repository.find_outdated_updates(PROJECT_ROOT)
|
||||
if update.entry.role == ROLE
|
||||
}
|
||||
self.assertTrue(
|
||||
{
|
||||
("talk_signaling", "9.0.0"),
|
||||
("talk_nats", "9.0.0-alpine"),
|
||||
("talk", "9.0.0"),
|
||||
("talk_recording", "3.14.9-alpine3.23"),
|
||||
}
|
||||
<= images,
|
||||
images,
|
||||
)
|
||||
self.assertEqual(refs, {(("talk",), "v9.0.0"), (("talk_recording",), "v9.0.0")})
|
||||
|
||||
def test_negative_control_current_tags_report_nothing(self):
|
||||
current = {
|
||||
image: [_pinned(service, "version")]
|
||||
for service, image in self.IMAGES.items()
|
||||
}
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"INFINITO_WORKER_FETCH": "2"}),
|
||||
mock.patch.object(
|
||||
docker, "fetch_dockerhub_tags", lambda image: current.get(image, [])
|
||||
),
|
||||
mock.patch.object(docker, "fetch_ghcr_tags", lambda image: []),
|
||||
mock.patch.object(docker, "fetch_mcr_tags", lambda image: []),
|
||||
mock.patch.object(
|
||||
docker, "fetch_registry_tags", lambda image, last=None: []
|
||||
),
|
||||
):
|
||||
reported = {
|
||||
update.entry.service
|
||||
for update in docker.find_outdated_updates(PROJECT_ROOT)
|
||||
if update.entry.role == ROLE
|
||||
}
|
||||
self.assertFalse(reported & set(self.IMAGES), reported)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user