diff --git a/CHANGELOG.md b/CHANGELOG.md index 001dd78a..24860431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,34 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html). before it writes anything, logs both zones as a critical error and reports the close unhealthy in `/health/ready`. See `docs/operations/licensed-agent-ledger.md`. (#348) +- **Calls whose `Hangup` never reached Platform now end on the pool sweep (Verbara.Sdk `2.8.0`).** + Every 30 s each Asterisk server holding calls older than 60 s receives one AMI `Status`; a held call + Asterisk no longer lists is ended and wraps up like any other hang-up (Sdk #402, H142). Before, such a + call stayed open for the life of the process. The sweep needs, on **every** Asterisk Platform + connects to, an AMI user whose `write` includes `system`, `call` or `reporting` and that does **not** + filter out `Hangup` (`eventfilter`); otherwise the sweep fails on that server and those calls stay + open as before. Every Asterisk of a multi-server pool also needs its own `systemname` in + `asterisk.conf` `[options]`, or channel ids collide across servers (Sdk #403 now logs a Warning on a + collision). The signal of a missing permission is `[LIVE] Status refused: …` at Warning in the API's + **start-up** log, because every state load sends `Status`. Platform's Compose `manager.conf` template + already complies; the AMI user and its reasons are in the voice manual + (`docs/manuales/smb/06-canal-voz-sip.md`). +- **Helm: the Asterisk chart changed in a breaking way.** It now ships Platform's AMI user (the same + `read`/`write` classes as the Compose template, no `eventfilter`) and its ARI user, and serves ARI on + port 8088. It needs both passwords at install: pass `asterisk.ami.password` and + `asterisk.ari.password`, or create the Secrets named by `asterisk.ami.passwordSecret` and + `asterisk.ari.passwordSecret` first; without them the Asterisk pod does not start. Keep them equal to + the platform chart's `api.asterisk.ami.password` and `api.asterisk.ari.password`; + `scripts/k8s-apps.sh` now requires `VERBARA_AMI_PASSWORD` and `VERBARA_ARI_PASSWORD` and passes each + to both charts. The chart sets `systemname` to the pod name, runs **one** Asterisk + (`asterisk.replicas: 1`) and refuses to render with `replicas > 1`, because Platform registers one AMI + server. Its PodDisruptionBudget (`minAvailable: 1`) therefore blocks a drain of the only Asterisk on + purpose. To drain its node, plan a maintenance window: cordon the node; set the agents Away and pause + queues and campaigns; wait until `asterisk -rx 'core show channels count'` shows 0 active channels; + run `kubectl delete pod asterisk-0` (or `kubectl drain --disable-eviction`); uncordon. The pod stays + `Pending` until its node is back, because its volumes use `local-path` storage. A pod that is not + Ready can always be evicted (`unhealthyPodEvictionPolicy: AlwaysAllow`). + ### Added - **Licensed-agent ledger.** Every change that can alter who counts as a licensed agent — agent created @@ -85,6 +113,27 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **The PJSIP desired state enumerates every agent of a tenant.** It was capped at 1000 agents per tenant, so agents beyond the first page were never provisioned. (#348) +### Dependencies + +- **Verbara.Sdk `2.7.0` → `2.8.0`** and **Verbara.Sdk.Pro `2.17.1-pro` → `2.18.0-pro`** — all 29 + consumed packages in one commit (7 `Verbara.Sdk.*` pins + 22 `Verbara.Sdk.Pro.*` pins). The + compatibility triple is **Verbara.Sdk `2.8.0` × Verbara.Sdk.Pro `2.18.0-pro` × Verbara.Platform (the + next version cut from `main`)**; per `Verbara.Sdk/ADR-0040` D3 the cascade cuts no version of its + own. **No floor moved:** `dotnet restore` reported no `NU1605`/`NU1109`, and the per-project + `project.assets.json` diff by package name moves only the Verbara packages; nothing entered or left + the graph. +- **The `OpenTelemetry.Exporter.Prometheus.AspNetCore` pin is removed.** Verbara.Sdk.OpenTelemetry + `2.8.0` declares the exporter at `1.19.1-beta.1` itself (Sdk #388), so the exporter resolves to the + same version without Platform's pin; `GET /metrics` is unchanged and its test stays. +- **`DeferredPrimaryAmiConnection` forwards `IAmiConnection.Subscribe(Func)`**, the member Sdk `2.8.0` added with a default body (Sdk #391). + Without the forwarding, a handler subscribed through the wrapper got a token that is never + cancelled, so the primary connection's fast close never reached it. +- **Behaviour Platform takes on — the pool sweep (H142).** `AddVerbaraSessionsMultiServer()` now + registers the reconciliation sweep for every server, on by default; Platform keeps the Sdk defaults + (`ReconciliationInterval` 30 s, `DialingTimeout` 60 s). A call the sweep ends carries no hangup + cause, so it never triggers a callback-on-drop. Prerequisites and signal: see **Upgrading**. + --- ## [2.26.1] - 2026-10-08 diff --git a/Directory.Packages.props b/Directory.Packages.props index 0158ce47..9ecf4ad7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,9 +21,9 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/manuales/smb/06-canal-voz-sip.md b/docs/manuales/smb/06-canal-voz-sip.md index 73ed0ac6..5e91b4da 100644 --- a/docs/manuales/smb/06-canal-voz-sip.md +++ b/docs/manuales/smb/06-canal-voz-sip.md @@ -57,6 +57,60 @@ Después de cualquier cambio en trunks/colas/agentes, Asterisk los toma on-deman $ docker exec verbara-asterisk asterisk -rx 'module reload res_pjsip_endpoint_identifier_ip.so' ``` +### 0.1 Un Asterisk propio: usuario AMI y `systemname` + +Si usás el Asterisk del stack de Verbara (Compose), esto ya está hecho: su `manager.conf` se genera en cada arranque desde `docker/asterisk-config/manager.conf.template`. Si conectás Verbara a **tu propio Asterisk** (o a varios), su usuario AMI tiene que ser **igual** a este: + +```ini +; manager.conf +[general] +enabled = yes +bindaddr = 0.0.0.0 +port = 5038 + +[platform] +secret = +read = system,call,agent,user,config,originate,reporting,command +write = system,call,agent,user,config,originate,command +deny = 0.0.0.0/0.0.0.0 +permit = 0.0.0.0/0.0.0.0 +``` + +El `secret` es el mismo valor que Verbara usa como `Asterisk__Ami__Password`. Restringí `permit` a la IP del servidor de Verbara si podés. + +**Por qué cada clase de `write`** (cada acción AMI exige una clase; Asterisk rechaza la acción si el usuario no la tiene): + +| Clase | Qué hace Verbara con ella | +|-------|---------------------------| +| `system` / `call` | `Status`: la carga del estado al arrancar (y en cada reconexión) y el barrido que cierra las llamadas cuyo `Hangup` se perdió (cada 30 s). `Hangup` y `ModuleLoad` (recarga de módulos) también la usan. | +| `call` | `Redirect` + `SetVar`: las transferencias de agente (`VoiceCallControlService`); `GetVar`; `BlindTransfer` hacia la encuesta CSAT por voz. | +| `agent` | `QueuePause`: el estado del agente (Disponible / Ausente) pausa o despausa su miembro de cola (`RealtimeStateBridge`); `QueueAdd`/`QueueRemove` al reconciliar miembros. | +| `originate` | `Originate`: llamadas salientes del agente y callbacks. | +| `command` | `Command`: `core show codecs` (lista de codecs instalados en la consola de administración) y la prueba de conectividad de trunks. | +| `user`, `config` | No encontramos ninguna acción de Verbara que las exija; se mantienen como en la plantilla. | + +**Por qué cada clase de `read`** (qué eventos recibe Verbara): `call` (canales, puentes y cada `Hangup`: las sesiones de llamada), `agent` (`AgentConnect` y el estado de los miembros de cola: sin ella la llamada de cola no tiene dueño ni screen-pop) y `system` (`FullyBooted`: sabe cuándo Asterisk terminó de arrancar). Para `user`, `config`, `originate`, `reporting` y `command` no encontramos un evento que Verbara necesite; se mantienen como en la plantilla. + +> ⚠️ **No filtres el evento `Hangup`.** No agregues un `eventfilter` que lo excluya (por ejemplo `eventfilter=!Event: Hangup`). Verbara cierra la llamada con ese evento; si no llega, sólo el barrido la cierra, y el barrido necesita `Status`. + +**Varios Asterisk (un pool):** cada servidor necesita su propio nombre de sistema, o los ids de canal se repiten entre servidores y las llamadas se mezclan: + +```ini +; asterisk.conf +[options] +systemname = <único por servidor> ; por ejemplo pbx-1, pbx-2 +``` + +**Qué falla sin esto:** sin `write` con `system`, `call` o `reporting`, el `Status` se rechaza: las llamadas cuyo `Hangup` se perdió quedan abiertas (el agente sigue Ocupado). Sin `call` fallan las transferencias; sin `agent` el agente no se pausa ni despausa en la cola; sin `originate` no salen llamadas. Con dos servidores sin `systemname` distinto, Verbara puede mezclar llamadas de servidores distintos y sólo lo avisa con un Warning en el log. + +**Cómo detectarlo:** el `Status` se manda en cada arranque del API, así que un permiso faltante aparece en el **log de arranque**: + +```bash +$ dc logs platform-api | grep 'Status refused' +``` + +✓ Sin salida = el usuario AMI tiene el permiso. Si aparece `[LIVE] Status refused: …` (nivel Warning), corregí el `write` del usuario AMI, recargá el manager en Asterisk (`asterisk -rx 'manager reload'`) y reiniciá el API. + ## 1. Pre-requisitos de red — checklist final Si esto NO está verde, **revisá manual 01 §2 y §3** antes de seguir. diff --git a/docs/manuales/smb/checklist-validacion-cliente.md b/docs/manuales/smb/checklist-validacion-cliente.md index 93d48d7e..5a5dd32d 100644 --- a/docs/manuales/smb/checklist-validacion-cliente.md +++ b/docs/manuales/smb/checklist-validacion-cliente.md @@ -147,6 +147,12 @@ Test inbound (golden path — ✅ V1): - ☐ Hangup limpio - ☐ CDR aparece en `/admin/analytics/calls` +Usuario AMI (manual [06](06-canal-voz-sip.md) §0.1): +- ☐ Log de arranque del Api sin `[LIVE] Status refused`: tras reiniciar el API, `docker logs verbara-platform-api 2>&1 | grep 'Status refused'` no devuelve nada (si aparece, al usuario AMI le falta `write` con `system`, `call` o `reporting`) + +Test transferencia (dos agentes): +- ☐ Una transferencia entre dos agentes completa: un cliente llama al DID y lo atiende el agente A; el agente A hace una **transferencia ciega** al agente B desde la consola (control de llamada); el teléfono o softphone del agente B suena, B atiende y el cliente queda hablando con B con audio en ambas direcciones; A queda libre. Si la transferencia falla o el cliente queda colgado, revisá que el usuario AMI tenga `write` con `call` (manual 06 §0.1) + Test concurrencia mínima (5 calls): - ☐ 5 llamadas simultáneas al DID (SIPp o 5 teléfonos) → 5 entran a la cola - ☐ 5 agentes distintos atienden (5 teléfonos SIP registrados) diff --git a/docs/operations/staging-environment.md b/docs/operations/staging-environment.md index 8a228ef1..1e9f3fb5 100644 --- a/docs/operations/staging-environment.md +++ b/docs/operations/staging-environment.md @@ -161,7 +161,7 @@ A fresh Phase 0L bring-up was the first scenario to exercise them. | DB | CloudNativePG operator → Postgres 17 (3-instance HA) | CNPG 1.25.0 | | DB pool | PgBouncer (CNPG Pooler CR, transaction mode) | (managed) | | Cache | Redis 8 (StatefulSet, AOF persistence) | 8-alpine | -| PBX | Asterisk (StatefulSet, 2 replicas, anti-affinity) | 22 | +| PBX | Asterisk (StatefulSet, 1 replica — the chart refuses more: Platform registers one AMI server) | 22 | | SBC | Kamailio (DaemonSet, hostNetwork) | 5.8.8 | | Media | RTPEngine (DaemonSet, hostNetwork) | fonoster/latest | | API | Platform.Api (Deployment, HPA 2→8) | 1.14.6 | @@ -198,7 +198,7 @@ A fresh Phase 0L bring-up was the first scenario to exercise them. | Feature | Scope | Detail | |---------|-------|--------| -| PodDisruptionBudgets | Asterisk + Platform API | `minAvailable: 1` — protects during node drain/upgrades | +| PodDisruptionBudgets | Asterisk + Platform API | `minAvailable: 1`. Platform API: keeps one pod serving during a node drain. Asterisk (one replica): **blocks** the drain of its node on purpose, so live calls are not dropped — voice does not stay up through a drain; plan a maintenance window (procedure in the CHANGELOG upgrade note of the Sdk 2.8.0 cascade). A pod that is not Ready can always be evicted (`unhealthyPodEvictionPolicy: AlwaysAllow`) | | NetworkPolicies | 17 policies, 4 namespaces | Default-deny ingress + service-level whitelists; standard `networking.k8s.io/v1` | | ResourceQuotas | r55-data, r55-asterisk, r55-platform | Per-namespace CPU/memory/pod caps with headroom for HPA | | SecurityContext | All workloads | `runAsNonRoot`, `seccompProfile: RuntimeDefault`, `allowPrivilegeEscalation: false` | diff --git a/infra/k8s/helm/asterisk/templates/asterisk-ami-secret.yaml b/infra/k8s/helm/asterisk/templates/asterisk-ami-secret.yaml new file mode 100644 index 00000000..e53a09c6 --- /dev/null +++ b/infra/k8s/helm/asterisk/templates/asterisk-ami-secret.yaml @@ -0,0 +1,17 @@ +{{- /* +Platform's AMI password. Rendered only when asterisk.ami.password is passed at +install; otherwise the Secret named by asterisk.ami.passwordSecret must already +exist. The repository holds no literal AMI password. +*/ -}} +{{- if .Values.asterisk.ami.password }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.asterisk.ami.passwordSecret }} + namespace: {{ .Values.namespace }} + labels: + {{- include "asterisk.labels" . | nindent 4 }} +type: Opaque +stringData: + {{ .Values.asterisk.ami.passwordKey }}: {{ .Values.asterisk.ami.password | quote }} +{{- end }} diff --git a/infra/k8s/helm/asterisk/templates/asterisk-ari-secret.yaml b/infra/k8s/helm/asterisk/templates/asterisk-ari-secret.yaml new file mode 100644 index 00000000..f7661a1c --- /dev/null +++ b/infra/k8s/helm/asterisk/templates/asterisk-ari-secret.yaml @@ -0,0 +1,17 @@ +{{- /* +Platform's ARI password. Rendered only when asterisk.ari.password is passed at +install; otherwise the Secret named by asterisk.ari.passwordSecret must already +exist. The repository holds no literal ARI password. +*/ -}} +{{- if .Values.asterisk.ari.password }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.asterisk.ari.passwordSecret }} + namespace: {{ .Values.namespace }} + labels: + {{- include "asterisk.labels" . | nindent 4 }} +type: Opaque +stringData: + {{ .Values.asterisk.ari.passwordKey }}: {{ .Values.asterisk.ari.password | quote }} +{{- end }} diff --git a/infra/k8s/helm/asterisk/templates/asterisk-configmap.yaml b/infra/k8s/helm/asterisk/templates/asterisk-configmap.yaml index 70ebad3e..412ac1b1 100644 --- a/infra/k8s/helm/asterisk/templates/asterisk-configmap.yaml +++ b/infra/k8s/helm/asterisk/templates/asterisk-configmap.yaml @@ -72,3 +72,43 @@ data: [modules] autoload=yes noload => chan_sip.so + + # Platform's AMI user. Same [general] and the same read/write/deny/permit lines + # as docker/asterisk-config/manager.conf.template, no eventfilter (Platform's + # pool sweep needs every Hangup and an AMI write class of system, call or + # reporting to send Status). The container command renders + # /etc/asterisk/manager.conf from this template with the password from the + # Secret; the placeholder never reaches Asterisk. + manager.conf.template: | + [general] + enabled = yes + bindaddr = 0.0.0.0 + port = 5038 + + [{{ .Values.asterisk.ami.username }}] + secret = __AMI_PASSWORD__ + read = system,call,agent,user,config,originate,reporting,command + write = system,call,agent,user,config,originate,command + deny = 0.0.0.0/0.0.0.0 + permit = 0.0.0.0/0.0.0.0 + + # Platform's ARI user, as in docker/asterisk-config/ari.conf.template; the + # password is rendered in from the Secret the same way. + ari.conf.template: | + [general] + enabled = yes + pretty = yes + + [{{ .Values.asterisk.ari.username }}] + type = user + read_only = no + password = __ARI_PASSWORD__ + + # ARI is served by Asterisk's HTTP server, which the image binds to 127.0.0.1 + # only. Plain HTTP on 8088 inside the cluster, as the platform chart's + # api.asterisk.ari.baseUrl expects. + http.conf: | + [general] + enabled = yes + bindaddr = 0.0.0.0 + bindport = 8088 diff --git a/infra/k8s/helm/asterisk/templates/asterisk-pdb.yaml b/infra/k8s/helm/asterisk/templates/asterisk-pdb.yaml index 596202f0..1c97c232 100644 --- a/infra/k8s/helm/asterisk/templates/asterisk-pdb.yaml +++ b/infra/k8s/helm/asterisk/templates/asterisk-pdb.yaml @@ -1,3 +1,10 @@ +# With one Asterisk replica, minAvailable: 1 blocks every voluntary eviction of +# the only Asterisk pod. That is deliberate: a node drain would otherwise drop +# every live call. Draining its node needs a maintenance window (cordon; agents +# Away and queues and campaigns paused; wait for `core show channels count` = 0; +# then `kubectl delete pod asterisk-0` or `kubectl drain --disable-eviction`; +# uncordon) - see the upgrade note in CHANGELOG.md. AlwaysAllow lets an unhealthy +# (not Ready) pod be evicted, so a broken Asterisk never blocks a drain. apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -7,6 +14,7 @@ metadata: {{- include "asterisk.labels" . | nindent 4 }} spec: minAvailable: 1 + unhealthyPodEvictionPolicy: AlwaysAllow selector: matchLabels: app.kubernetes.io/name: asterisk diff --git a/infra/k8s/helm/asterisk/templates/asterisk-statefulset.yaml b/infra/k8s/helm/asterisk/templates/asterisk-statefulset.yaml index eeb4730f..31953da5 100644 --- a/infra/k8s/helm/asterisk/templates/asterisk-statefulset.yaml +++ b/infra/k8s/helm/asterisk/templates/asterisk-statefulset.yaml @@ -1,3 +1,6 @@ +{{- if gt (int .Values.asterisk.replicas) 1 }} +{{- fail (printf "asterisk.replicas=%v is not supported: Platform registers exactly one AMI server for this chart (the \"primary\" node in Verbara.Platform.Api Program.cs), so a second Asterisk would take calls Platform never sees. Keep asterisk.replicas at 1; more than one Asterisk is a follow-up that needs one Platform AMI node per pod." .Values.asterisk.replicas) }} +{{- end }} apiVersion: apps/v1 kind: StatefulSet metadata: @@ -36,7 +39,32 @@ spec: allowPrivilegeEscalation: false seccompProfile: type: RuntimeDefault - command: ["asterisk", "-fpvvv"] + # Renders Platform's AMI and ARI users from the mounted templates with the + # passwords from the Secrets, sets a per-pod systemname (unique channel ids + # if this ever runs more than one Asterisk), then execs Asterisk as before. + # awk reads the passwords from ENVIRON, so "/", "&" and "\" stay literal + # (no sed replacement). Re-run on every container start: idempotent. + command: + - sh + - -c + - | + set -eu + : "${AMI_PASSWORD:?AMI_PASSWORD is empty: set asterisk.ami.password or create the asterisk.ami.passwordSecret Secret}" + : "${ARI_PASSWORD:?ARI_PASSWORD is empty: set asterisk.ari.password or create the asterisk.ari.passwordSecret Secret}" + : "${POD_NAME:?POD_NAME is empty}" + awk '/^secret = /{print "secret = " ENVIRON["AMI_PASSWORD"]; next} {print}' \ + /etc/asterisk/manager.conf.template > /etc/asterisk/manager.conf + awk '/^password = /{print "password = " ENVIRON["ARI_PASSWORD"]; next} {print}' \ + /etc/asterisk/ari.conf.template > /etc/asterisk/ari.conf + chmod 600 /etc/asterisk/manager.conf /etc/asterisk/ari.conf + awk 'BEGIN { name = ENVIRON["POD_NAME"]; done = 0 } + /^[ \t]*systemname[ \t]*=/ { next } + { print } + /^\[options\]/ && !done { print "systemname = " name; done = 1 } + END { if (!done) { print ""; print "[options]"; print "systemname = " name } }' \ + /etc/asterisk/asterisk.conf > /etc/asterisk/asterisk.conf.new + mv /etc/asterisk/asterisk.conf.new /etc/asterisk/asterisk.conf + exec asterisk -fpvvv ports: - name: sip containerPort: 5060 @@ -48,6 +76,20 @@ spec: - name: prom containerPort: {{ .Values.asterisk.prometheus.port }} env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: AMI_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.asterisk.ami.passwordSecret }} + key: {{ .Values.asterisk.ami.passwordKey }} + - name: ARI_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.asterisk.ari.passwordSecret }} + key: {{ .Values.asterisk.ari.passwordKey }} - name: PG_HOST value: {{ .Values.asterisk.postgres.host }} - name: PG_PORT @@ -78,6 +120,15 @@ spec: - name: config mountPath: /etc/asterisk/modules.conf subPath: modules.conf + - name: config + mountPath: /etc/asterisk/manager.conf.template + subPath: manager.conf.template + - name: config + mountPath: /etc/asterisk/ari.conf.template + subPath: ari.conf.template + - name: config + mountPath: /etc/asterisk/http.conf + subPath: http.conf - name: voicemail mountPath: /var/spool/asterisk/voicemail - name: recordings diff --git a/infra/k8s/helm/asterisk/values.yaml b/infra/k8s/helm/asterisk/values.yaml index 922ef8ef..4928f18d 100644 --- a/infra/k8s/helm/asterisk/values.yaml +++ b/infra/k8s/helm/asterisk/values.yaml @@ -4,7 +4,29 @@ asterisk: image: repository: andrius/asterisk tag: "22" - replicas: 2 + # One Asterisk. Platform registers exactly one AMI server for this chart (the + # "primary" node, src/Verbara.Platform.Api/Program.cs), so a second pod would + # take calls Platform never sees. templates/asterisk-statefulset.yaml refuses + # any value above 1; more than one Asterisk is a follow-up that needs one + # Platform AMI node per pod. + replicas: 1 + # Platform's AMI user (manager.conf), the same read/write classes as the Compose + # template docker/asterisk-config/manager.conf.template. The password lives only + # in a Secret: pass asterisk.ami.password at install (scripts/k8s-apps.sh does, + # and passes the same value to the platform chart's api.asterisk.ami.password), + # or leave it empty and create the Secret yourself before installing. + ami: + username: platform + password: "" + passwordSecret: asterisk-ami-credentials + passwordKey: password + # Platform's ARI user (ari.conf), served on http.conf's 8088. Same mechanism as + # the AMI user: the password only in a Secret. + ari: + username: verbara + password: "" + passwordSecret: asterisk-ari-credentials + passwordKey: password resources: requests: cpu: 500m diff --git a/scripts/k8s-apps.sh b/scripts/k8s-apps.sh index c9b91657..071716fa 100755 --- a/scripts/k8s-apps.sh +++ b/scripts/k8s-apps.sh @@ -15,6 +15,13 @@ echo "" kubectl get nodes --no-headers | grep -q "Ready" || { echo "ERROR: No Ready nodes. Run scripts/k8s-up.sh first."; exit 1; } +# Platform's AMI and ARI passwords: one variable each, passed to BOTH charts, so the +# Asterisk users (asterisk chart) and Platform's Asterisk__Ami__Password / +# Asterisk__Ari__Password (platform chart) cannot drift. --set-literal keeps commas, +# backslashes and dots in the value as typed. +: "${VERBARA_AMI_PASSWORD:?ERROR: export VERBARA_AMI_PASSWORD (Platform's AMI password; the Asterisk chart needs it at install)}" +: "${VERBARA_ARI_PASSWORD:?ERROR: export VERBARA_ARI_PASSWORD (Platform's ARI password; the Asterisk chart needs it at install)}" + # --- 1. Storage: local-path-provisioner --- echo "[1/10] Installing local-path-provisioner..." if ! kubectl get storageclass local-path &>/dev/null; then @@ -45,9 +52,13 @@ echo " Done." # --- 4. Asterisk + Kamailio + RTPEngine --- echo "[4/10] Installing Asterisk Helm chart..." if helm status asterisk -n r55-asterisk &>/dev/null; then - helm upgrade asterisk "$INFRA/helm/asterisk" --values "$INFRA/helm/asterisk/values.yaml" + helm upgrade asterisk "$INFRA/helm/asterisk" --values "$INFRA/helm/asterisk/values.yaml" \ + --set-literal asterisk.ami.password="$VERBARA_AMI_PASSWORD" \ + --set-literal asterisk.ari.password="$VERBARA_ARI_PASSWORD" else - helm install asterisk "$INFRA/helm/asterisk" --values "$INFRA/helm/asterisk/values.yaml" + helm install asterisk "$INFRA/helm/asterisk" --values "$INFRA/helm/asterisk/values.yaml" \ + --set-literal asterisk.ami.password="$VERBARA_AMI_PASSWORD" \ + --set-literal asterisk.ari.password="$VERBARA_ARI_PASSWORD" fi kubectl -n r55-asterisk rollout status statefulset/asterisk --timeout=5m echo " Waiting for Kamailio + RTPEngine DaemonSets..." @@ -58,9 +69,13 @@ echo " Done." # --- 5. Platform.Api + Web --- echo "[5/10] Installing Platform Helm chart..." if helm status platform -n r55-platform &>/dev/null; then - helm upgrade platform "$INFRA/helm/platform" --values "$INFRA/helm/platform/values.yaml" + helm upgrade platform "$INFRA/helm/platform" --values "$INFRA/helm/platform/values.yaml" \ + --set-literal api.asterisk.ami.password="$VERBARA_AMI_PASSWORD" \ + --set-literal api.asterisk.ari.password="$VERBARA_ARI_PASSWORD" else - helm install platform "$INFRA/helm/platform" --values "$INFRA/helm/platform/values.yaml" + helm install platform "$INFRA/helm/platform" --values "$INFRA/helm/platform/values.yaml" \ + --set-literal api.asterisk.ami.password="$VERBARA_AMI_PASSWORD" \ + --set-literal api.asterisk.ari.password="$VERBARA_ARI_PASSWORD" fi kubectl -n r55-platform rollout status deploy/platform-api --timeout=5m kubectl -n r55-platform rollout status deploy/web --timeout=2m diff --git a/src/Verbara.Platform.Api/Services/DeferredPrimaryAmiConnection.cs b/src/Verbara.Platform.Api/Services/DeferredPrimaryAmiConnection.cs index e9836d1b..dbfb7f44 100644 --- a/src/Verbara.Platform.Api/Services/DeferredPrimaryAmiConnection.cs +++ b/src/Verbara.Platform.Api/Services/DeferredPrimaryAmiConnection.cs @@ -92,6 +92,12 @@ public event Action? StateChanged public IDisposable Subscribe(IObserver observer) => Primary.Subscribe(observer); + // Sdk 2.8.0 (#391) added this overload with a default body that subscribes through this wrapper's OnEvent + // with a token that is never cancelled. Forwarding hands the handler to the primary, whose token is + // cancelled when the connection closes. + public IDisposable Subscribe(Func handler) + => Primary.Subscribe(handler); + public ValueTask DisconnectAsync(CancellationToken cancellationToken = default) => Primary.DisconnectAsync(cancellationToken); diff --git a/tests/Verbara.Platform.Api.Tests/DeferredPrimaryAmiConnectionTests.cs b/tests/Verbara.Platform.Api.Tests/DeferredPrimaryAmiConnectionTests.cs index 9491f79b..418bdf55 100644 --- a/tests/Verbara.Platform.Api.Tests/DeferredPrimaryAmiConnectionTests.cs +++ b/tests/Verbara.Platform.Api.Tests/DeferredPrimaryAmiConnectionTests.cs @@ -141,4 +141,21 @@ public void StateChanged_ShouldStopDelivering_WhenUnsubscribedThroughWrapper() Assert.Same(first, Assert.Single(delivered)); } + + // sdk-2-8-0-pin-cascade: Sdk 2.8.0 (#391) added Subscribe(Func) + // with a default body that attaches the handler through the wrapper's own OnEvent with a token that is never + // cancelled. The wrapper must hand it to the primary instead, so the primary's fast close reaches the handler. + [Fact] + public void Subscribe_ShouldDelegateTokenHandlerToPrimary_WhenHandlerIsSubscribedThroughWrapper() + { + var (wrapper, primary) = WrapperOverSubstitutedPrimary(); + var subscription = Substitute.For(); + Func handler = (_, _) => ValueTask.CompletedTask; + primary.Subscribe(handler).Returns(subscription); + + var returned = wrapper.Subscribe(handler); + + Assert.Same(subscription, returned); + primary.Received(1).Subscribe(handler); + } }