Skip to content

Commit 99d8c6a

Browse files
authored
Configure Slack, Teams, SMS, push, SCIM, inbound email and OTel in the Helm chart (#717)
These settings had no chart values and needed config.extraEnv. They now have config.opentag, config.sms, config.push, config.deliveryPublicUrl, config.scim, config.inboundEmail and config.otel, with their secrets under secrets (or an existing Secret or store, by key). The install refuses what the server refuses at boot: a switch with no secret behind it, an OpenTag secret under 32 characters, an OpenTag URL or icon with the pairing off or of the wrong scheme, a partial set of Twilio settings, a push project id that is not a UUID, and a deliveryPublicUrl that is not http(s). The CI refusal step exercises them on every target. With none set, every CI target renders byte for byte as before.
1 parent 39489d8 commit 99d8c6a

7 files changed

Lines changed: 344 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,23 @@ jobs:
218218
--set networkPolicy.enabled=true \
219219
--set computers.extraEnv[0].name=EGRESS_PROXY_DEFAULT \
220220
--set-string computers.extraEnv[0].value=http://proxy.internal:3128
221+
# A delivery or directory switch with no secret behind it. Armed on every target: the chart
222+
# requires the value when it holds the secret, and requires the store to name the key when
223+
# a store does.
224+
refuses "OpenTag switched on with no shared secret" \
225+
--set config.opentag.enabled=true
226+
refuses "SCIM switched on with no bearer token" \
227+
--set config.scim.enabled=true
228+
# The rest are refusals the server makes at boot, mirrored so the value is named at install.
229+
refuses "an OpenTag URL with the pairing switched off" \
230+
--set-string config.opentag.url=https://opentag.example.com
231+
refuses "a partial set of Twilio settings" \
232+
--set config.sms.enabled=true --set-string secrets.twilioAuthToken=token \
233+
--set-string config.sms.accountSid=AC0 --set-string config.sms.verifyServiceSid=VA0
234+
refuses "a push project id that is not a UUID" \
235+
--set-string config.push.projectId=not-a-uuid
236+
refuses "a delivery public URL that is not http(s)" \
237+
--set-string config.deliveryPublicUrl=hooks.example.com
221238
# A warm pool nothing claims from. Only meaningful where the target asks for per-Bot
222239
# computers; on the others the mode is not sandbox and the refusal is not armed.
223240
if grep -qE '^ *mode: sandbox' charts/openbot/ci/${{ matrix.target }}-values.yaml; then

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
88

99
## Unreleased
1010

11+
### The Helm chart configures Slack, Teams, text messages, push, SCIM, inbound email and OpenTelemetry
12+
13+
These settings had no chart values and could only be passed through `config.extraEnv`. They now have
14+
their own: `config.opentag`, `config.sms`, `config.push`, `config.deliveryPublicUrl`, `config.scim`,
15+
`config.inboundEmail` and `config.otel`, with the OpenTag secret, the Twilio auth token, the Expo
16+
access token, the SCIM bearer tokens and the OpenTelemetry headers under `secrets` (or an existing
17+
Secret or store, by key). The install refuses what the server would refuse at boot, such as an
18+
OpenTag secret under 32 characters or a partial set of Twilio settings. With none of them set, the
19+
chart renders exactly as before, so a deployment already passing these through `config.extraEnv`
20+
keeps working unchanged until it moves them across.
21+
1122
### A coworker at its own endpoint can hand work to another Bot
1223

1324
A grant letting a remote Bot (a coworker at its own endpoint) hand work to another Bot was accepted and stored, but the grant read kept only built-in Bots, so the remote Bot was never

‎charts/openbot/README.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,41 @@ Left empty, this deployment has the Bots its tenant package declares as built-in
327327
package entry pointing at an endpoint that resolves to nothing is dropped rather than registered as a
328328
coworker nobody can talk to.
329329

330+
## Slack, Teams, text messages, push, SCIM, inbound email and OpenTelemetry
331+
332+
Each is off until its values are set, and each is the same setting [docs/configuration.md](../../docs/configuration.md)
333+
describes for a `.env`. Secrets go under `secrets`; with an existing Secret or an external store, use
334+
the key in the last column.
335+
336+
| Feature | Switch | Plain values | Secret key |
337+
| -------------------------------- | ------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------- |
338+
| Slack and Teams, through OpenTag | `config.opentag.enabled` | `url`, `botIconUrlTemplate` | `opentag-shared-secret` (required, 32+ characters) |
339+
| Text messages, through Twilio | `config.sms.enabled` | `accountSid`, `verifyServiceSid`, `fromNumber` (all three) | `twilio-auth-token` (required) |
340+
| Push and native-app sign-in | `config.push.projectId` (the EAS project UUID) | | `expo-access-token` (optional) |
341+
| Where Twilio calls back | `config.deliveryPublicUrl` | defaults to `config.publicUrl` | |
342+
| SCIM provisioning | `config.scim.enabled` | `connectionId` | `scim-bearer-token` (required), `scim-bearer-token-next` (optional) |
343+
| Inbound email triggers | `config.inboundEmail.domain` and `snsTopicArns` | both needed, or the route is not mounted | |
344+
| OpenTelemetry export | `config.otel.endpoint` or `logsEndpoint` | `serviceName`, `paused` | `otel-logs-headers`, `otel-headers` (optional) |
345+
346+
```yaml
347+
config:
348+
opentag:
349+
enabled: true
350+
url: https://opentag.example.com
351+
scim:
352+
enabled: true
353+
secrets:
354+
opentagSharedSecret: <openssl rand -hex 32>
355+
scimBearerToken: <openssl rand -hex 32>
356+
```
357+
358+
The install refuses what the server would refuse at boot: a switch with no secret behind it (or, on
359+
`externalSecrets`, no key named for it), an OpenTag secret under 32 characters, an OpenTag URL or icon
360+
with the pairing off, an OpenTag URL that is not http(s) or an icon that is not https, a partial set
361+
of Twilio settings, a push project id that is not a UUID, and a `deliveryPublicUrl` that is not
362+
http(s). With `networkPolicy.enabled`, a collector inside the cluster has to be named in
363+
`networkPolicy.extraEgress`; the providers themselves are reached on 443.
364+
330365
## Upgrading the server without the computers
331366

332367
`computers.mode: shared` runs one browser for every Bot, and on that shape the transcript's kept

‎charts/openbot/templates/_helpers.tpl‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,120 @@ and in whatever holds the release, which is not where `KEY_ENCRYPTION_KEY` belon
428428
name: {{ default (include "openbot.secretName" .) .Values.computers.existingTokenSecret }}
429429
key: computer-token
430430
optional: {{ eq .Values.computers.mode "external" }}
431+
{{- /*
432+
Delivery (Slack and Teams through OpenTag, text messages, push), SCIM, inbound email and
433+
OpenTelemetry export.
434+
435+
PARENTHESISED, every map, for the reason `config.handoff` is above: these are keys this chart did
436+
not have before, and `helm upgrade --reuse-values` leaves them absent on an existing deployment.
437+
438+
A secret the server requires is referenced whenever its switch is on, never `optional`, so a
439+
Secret that lacks it fails to start the pod with the key named rather than starting a server that
440+
then refuses. A secret the server treats as optional is referenced `optional: true` whenever its
441+
feature is, so it can live in `secrets.existingSecret` or a store without the chart reading it.
442+
*/}}
443+
{{- $opentag := .Values.config.opentag | default dict }}
444+
{{- if $opentag.enabled }}
445+
- name: OPENTAG_SHARED_SECRET
446+
valueFrom:
447+
secretKeyRef:
448+
name: {{ include "openbot.secretName" . }}
449+
key: opentag-shared-secret
450+
{{- with $opentag.url }}
451+
- name: OPENTAG_URL
452+
value: {{ . | quote }}
453+
{{- end }}
454+
{{- with $opentag.botIconUrlTemplate }}
455+
- name: OPENTAG_BOT_ICON_URL_TEMPLATE
456+
value: {{ . | quote }}
457+
{{- end }}
458+
{{- end }}
459+
{{- $sms := .Values.config.sms | default dict }}
460+
{{- if $sms.enabled }}
461+
- name: TWILIO_ACCOUNT_SID
462+
value: {{ $sms.accountSid | quote }}
463+
- name: TWILIO_AUTH_TOKEN
464+
valueFrom:
465+
secretKeyRef:
466+
name: {{ include "openbot.secretName" . }}
467+
key: twilio-auth-token
468+
- name: TWILIO_VERIFY_SERVICE_SID
469+
value: {{ $sms.verifyServiceSid | quote }}
470+
- name: TWILIO_FROM_NUMBER
471+
value: {{ $sms.fromNumber | quote }}
472+
{{- end }}
473+
{{- with (.Values.config.push | default dict).projectId }}
474+
- name: EXPO_PROJECT_ID
475+
value: {{ . | quote }}
476+
- name: EXPO_ACCESS_TOKEN
477+
valueFrom:
478+
secretKeyRef:
479+
name: {{ include "openbot.secretName" $ }}
480+
key: expo-access-token
481+
optional: true
482+
{{- end }}
483+
{{- with .Values.config.deliveryPublicUrl }}
484+
- name: DELIVERY_PUBLIC_URL
485+
value: {{ . | quote }}
486+
{{- end }}
487+
{{- $scim := .Values.config.scim | default dict }}
488+
{{- if $scim.enabled }}
489+
- name: SCIM_BEARER_TOKEN
490+
valueFrom:
491+
secretKeyRef:
492+
name: {{ include "openbot.secretName" . }}
493+
key: scim-bearer-token
494+
- name: SCIM_BEARER_TOKEN_NEXT
495+
valueFrom:
496+
secretKeyRef:
497+
name: {{ include "openbot.secretName" . }}
498+
key: scim-bearer-token-next
499+
optional: true
500+
{{- with $scim.connectionId }}
501+
- name: SCIM_CONNECTION_ID
502+
value: {{ . | quote }}
503+
{{- end }}
504+
{{- end }}
505+
{{- $inboundEmail := .Values.config.inboundEmail | default dict }}
506+
{{- with $inboundEmail.domain }}
507+
- name: OPENBOT_INBOUND_EMAIL_DOMAIN
508+
value: {{ . | quote }}
509+
{{- end }}
510+
{{- with $inboundEmail.snsTopicArns }}
511+
- name: OPENBOT_INBOUND_EMAIL_SNS_TOPIC_ARNS
512+
value: {{ . | quote }}
513+
{{- end }}
514+
{{- $otel := .Values.config.otel | default dict }}
515+
{{- if or $otel.endpoint $otel.logsEndpoint }}
516+
{{- with $otel.endpoint }}
517+
- name: OTEL_EXPORTER_OTLP_ENDPOINT
518+
value: {{ . | quote }}
519+
{{- end }}
520+
{{- with $otel.logsEndpoint }}
521+
- name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
522+
value: {{ . | quote }}
523+
{{- end }}
524+
{{- with $otel.serviceName }}
525+
- name: OTEL_SERVICE_NAME
526+
value: {{ . | quote }}
527+
{{- end }}
528+
{{- if $otel.paused }}
529+
- name: OPENBOT_OTEL_EXPORT
530+
value: "off"
531+
{{- end }}
532+
- name: OTEL_EXPORTER_OTLP_LOGS_HEADERS
533+
valueFrom:
534+
secretKeyRef:
535+
name: {{ include "openbot.secretName" . }}
536+
key: otel-logs-headers
537+
optional: true
538+
- name: OTEL_EXPORTER_OTLP_HEADERS
539+
valueFrom:
540+
secretKeyRef:
541+
name: {{ include "openbot.secretName" . }}
542+
key: otel-headers
543+
optional: true
544+
{{- end }}
431545
{{- /*
432546
One definition, for the same reason `openbot.databaseUrlEnv` is one (see its comment above): the
433547
API server needs this value to RECOGNISE the worker, and the routines CronJob needs the same value

‎charts/openbot/templates/secret.yaml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,29 @@ stringData:
5757
{{- if (.Values.routines).enabled }}
5858
worker-shared-secret: {{ required "secrets.workerSharedSecret is required when routines.enabled. Generate one with: openssl rand -base64 32" .Values.secrets.workerSharedSecret | quote }}
5959
{{- end }}
60+
{{- /*
61+
The same reasoning for the delivery and directory switches: `commonEnv` references these keys
62+
whenever the switch is on, so the switch without its secret fails here rather than in a pod.
63+
*/}}
64+
{{- if (.Values.config.opentag).enabled }}
65+
opentag-shared-secret: {{ required "secrets.opentagSharedSecret is required when config.opentag.enabled. Generate one with: openssl rand -hex 32" .Values.secrets.opentagSharedSecret | quote }}
66+
{{- end }}
67+
{{- if (.Values.config.sms).enabled }}
68+
twilio-auth-token: {{ required "secrets.twilioAuthToken is required when config.sms.enabled." .Values.secrets.twilioAuthToken | quote }}
69+
{{- end }}
70+
{{- if (.Values.config.scim).enabled }}
71+
scim-bearer-token: {{ required "secrets.scimBearerToken is required when config.scim.enabled. Generate one with: openssl rand -hex 32" .Values.secrets.scimBearerToken | quote }}
72+
{{- end }}
73+
{{- with .Values.secrets.scimBearerTokenNext }}
74+
scim-bearer-token-next: {{ . | quote }}
75+
{{- end }}
76+
{{- with .Values.secrets.expoAccessToken }}
77+
expo-access-token: {{ . | quote }}
78+
{{- end }}
79+
{{- with .Values.secrets.otelLogsHeaders }}
80+
otel-logs-headers: {{ . | quote }}
81+
{{- end }}
82+
{{- with .Values.secrets.otelHeaders }}
83+
otel-headers: {{ . | quote }}
84+
{{- end }}
6085
{{- end }}

‎charts/openbot/templates/validation.yaml‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,64 @@ This template renders nothing.
318318
{{- fail "routines.enabled but externalSecrets.data does not name worker-shared-secret. The API server reads it to recognise the worker and the CronJob reads it to be one, so every pod that mounts it fails to start." }}
319319
{{- end }}
320320
{{- end }}
321+
322+
{{- /*
323+
Delivery, the directory and OpenTelemetry export: the refusals the server makes at boot.
324+
325+
Each mirrors a throw in `configuredDeliveryProviders` (server/src/delivery/routes.ts), so the value
326+
to change is named by `helm install` rather than by a pod's crash loop. Nothing is refused here that
327+
the server would accept: inbound email with one half set is not mounted rather than refused, and
328+
is left alone here too.
329+
*/}}
330+
{{- $opentag := .Values.config.opentag | default dict }}
331+
{{- if and (not $opentag.enabled) (or $opentag.url $opentag.botIconUrlTemplate) }}
332+
{{- fail "config.opentag.url or config.opentag.botIconUrlTemplate is set but config.opentag.enabled is not. The server refuses either without the shared secret; set config.opentag.enabled and secrets.opentagSharedSecret." }}
333+
{{- end }}
334+
{{- if $opentag.enabled }}
335+
{{- if and $opentag.url (not (regexMatch "^https?://" $opentag.url)) }}
336+
{{- fail "config.opentag.url must be an http(s) URL." }}
337+
{{- end }}
338+
{{- if and $opentag.botIconUrlTemplate (not (hasPrefix "https://" $opentag.botIconUrlTemplate)) }}
339+
{{- fail "config.opentag.botIconUrlTemplate must be an https URL." }}
340+
{{- end }}
341+
{{- if and .Values.secrets.opentagSharedSecret (not .Values.secrets.existingSecret) (not .Values.externalSecrets.enabled) }}
342+
{{- if lt (len .Values.secrets.opentagSharedSecret) 32 }}
343+
{{- fail "secrets.opentagSharedSecret must be at least 32 characters. Generate one with: openssl rand -hex 32" }}
344+
{{- end }}
345+
{{- end }}
346+
{{- end }}
347+
{{- $sms := .Values.config.sms | default dict }}
348+
{{- if $sms.enabled }}
349+
{{- if or (not $sms.accountSid) (not $sms.verifyServiceSid) (not $sms.fromNumber) }}
350+
{{- fail "config.sms.enabled needs config.sms.accountSid, config.sms.verifyServiceSid and config.sms.fromNumber. The server takes all four Twilio settings or none." }}
351+
{{- end }}
352+
{{- else if or $sms.accountSid $sms.verifyServiceSid $sms.fromNumber }}
353+
{{- fail "config.sms has Twilio settings but config.sms.enabled is not set. The server takes all four Twilio settings or none; set config.sms.enabled and secrets.twilioAuthToken, or clear them." }}
354+
{{- end }}
355+
{{- with (.Values.config.push | default dict).projectId }}
356+
{{- if not (regexMatch "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" .) }}
357+
{{- fail "config.push.projectId must be the native app's EAS project UUID." }}
358+
{{- end }}
359+
{{- end }}
360+
{{- with .Values.config.deliveryPublicUrl }}
361+
{{- if not (regexMatch "^https?://" .) }}
362+
{{- fail "config.deliveryPublicUrl must be an http(s) URL." }}
363+
{{- end }}
364+
{{- end }}
365+
{{- /*
366+
The same secrets, for a deployment whose secrets come from a store: the value is not readable at
367+
template time, but the list of keys is, checked the way `worker-shared-secret` is above.
368+
*/}}
369+
{{- if .Values.externalSecrets.enabled }}
370+
{{- $named := list }}
371+
{{- range .Values.externalSecrets.data }}{{- $named = append $named .secretKey }}{{- end }}
372+
{{- if and $opentag.enabled (not (has "opentag-shared-secret" $named)) }}
373+
{{- fail "config.opentag.enabled but externalSecrets.data does not name opentag-shared-secret, so the server pod cannot start." }}
374+
{{- end }}
375+
{{- if and $sms.enabled (not (has "twilio-auth-token" $named)) }}
376+
{{- fail "config.sms.enabled but externalSecrets.data does not name twilio-auth-token, so the server pod cannot start." }}
377+
{{- end }}
378+
{{- if and (.Values.config.scim | default dict).enabled (not (has "scim-bearer-token" $named)) }}
379+
{{- fail "config.scim.enabled but externalSecrets.data does not name scim-bearer-token, so the server pod cannot start." }}
380+
{{- end }}
381+
{{- end }}

‎charts/openbot/values.yaml‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,73 @@ config:
195195
# The token travels in `secrets.managedAgentToken`. Required whenever a url is set: the server
196196
# refuses to start with one and not the other, because an unauthenticated Bot endpoint is an
197197
# open door to whatever that Bot can reach.
198+
199+
# Slack and Microsoft Teams, through the OpenTag pairing.
200+
#
201+
# `enabled` is the switch, rather than the secret's presence, because a secret held in
202+
# `secrets.existingSecret` or a store is not readable at template time. The shared secret travels in
203+
# `secrets.opentagSharedSecret`; the server refuses one shorter than 32 characters, and refuses a
204+
# `url` or icon template with no secret at all.
205+
opentag:
206+
enabled: false
207+
# OpenTag's base URL, for messages sent when no Slack turn is open. Without it those sends fail
208+
# visibly in the outbox and Slack triggers are refused. Must be http(s).
209+
url: ""
210+
# Optional https PNG URL with `{seed}` or `{agentId}`, used as the Bot's Slack icon.
211+
botIconUrlTemplate: ""
212+
213+
# Text messages, through Twilio. The server takes all four Twilio settings or none, so `enabled`
214+
# needs the three below and `secrets.twilioAuthToken`.
215+
sms:
216+
enabled: false
217+
accountSid: ""
218+
verifyServiceSid: ""
219+
fromNumber: ""
220+
221+
# Push notifications and sign-in from the native app, through Expo.
222+
#
223+
# Setting `projectId`, the app's EAS project UUID, switches both on; the server refuses anything
224+
# that is not a UUID. `secrets.expoAccessToken` is optional and sent to Expo's push service when set.
225+
push:
226+
projectId: ""
227+
228+
# The public origin Twilio calls back on, when it is not `publicUrl`. Twilio signs each request over
229+
# the URL it was given, so this has to be the address Twilio was configured with. Must be http(s).
230+
deliveryPublicUrl: ""
231+
232+
# SCIM provisioning at `<publicUrl>/api/auth/scim/v2`.
233+
#
234+
# `enabled` needs `secrets.scimBearerToken`, the token the directory sends. A second token in
235+
# `secrets.scimBearerTokenNext` is accepted beside it, so the directory can move to a new one before
236+
# the old one is removed.
237+
scim:
238+
enabled: false
239+
# The name of the directory connection. Empty means the server's default, `directory`.
240+
connectionId: ""
241+
242+
# Inbound email triggers: an SES receipt rule delivering through SNS to
243+
# `<publicUrl>/api/events/email/sns`. Both are needed; with either missing the route is not mounted.
244+
inboundEmail:
245+
# The domain the receipt rule covers, such as `in.example.com`.
246+
domain: ""
247+
# Comma-separated ARNs of the SNS topics allowed to deliver mail.
248+
snsTopicArns: ""
249+
250+
# Every audit row as an OTLP log record over HTTP, for a SIEM or an OpenTelemetry Collector.
251+
#
252+
# Setting either endpoint switches export on. Headers, which usually carry a collector's API key,
253+
# are secrets: `secrets.otelHeaders` and `secrets.otelLogsHeaders`, as `key=value` pairs separated
254+
# by commas. With `networkPolicy.enabled`, a collector inside the cluster has to be named in
255+
# `networkPolicy.extraEgress`.
256+
otel:
257+
# Collector base URL; the server appends `/v1/logs`.
258+
endpoint: ""
259+
# Where log records go, used exactly as written. Takes precedence over `endpoint`.
260+
logsEndpoint: ""
261+
# The `service.name` on every record. Empty means the server's default, `openbot`.
262+
serviceName: ""
263+
# Stops export while an endpoint is still set.
264+
paused: false
198265
logLevel: ""
199266
# Free-form additions, for anything this chart has no opinion about.
200267
extraEnv: []
@@ -484,6 +551,20 @@ secrets:
484551
googleClientSecret: ""
485552
microsoftClientSecret: ""
486553
oktaClientSecret: ""
554+
# Required when `config.opentag.enabled`. At least 32 characters. Generate one with:
555+
# openssl rand -hex 32
556+
opentagSharedSecret: ""
557+
# Required when `config.sms.enabled`.
558+
twilioAuthToken: ""
559+
# Optional, with `config.push.projectId`.
560+
expoAccessToken: ""
561+
# Required when `config.scim.enabled`; the next one is optional, for rotation.
562+
scimBearerToken: ""
563+
scimBearerTokenNext: ""
564+
# Optional, with `config.otel`. Headers for the logs endpoint, and the general ones used when the
565+
# logs ones are unset.
566+
otelLogsHeaders: ""
567+
otelHeaders: ""
487568

488569
externalSecrets:
489570
enabled: false

0 commit comments

Comments
 (0)